mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
feat(radio): answer the internet radio controls through the radio store
The Radio panel's and Cockpit's Enable, launcher, station tag, transport and volume controls call RadioControls through the radio store instead of DOM listeners. A refused station tag gets the kept tag back from act(), which now returns what the answering action returned. The Cockpit's station steps keep rotate: false. The unused element fields go from the shell.
This commit is contained in:
1 parent
61d11ce369
commit
7f7e66e793
18 files changed
+249
-141
No files matched your search
+4
-3
@@ -58,9 +58,10 @@
|
|||||||
list renders. Street Level's fit and panorama filters and Recent Imagery's
|
list renders. Street Level's fit and panorama filters and Recent Imagery's
|
||||||
mode are `UiSegmented` too: the Street Level store's owner answers
|
mode are `UiSegmented` too: the Street Level store's owner answers
|
||||||
`setPano` and `setRenderMode`, and `RecentImageryReadout` takes
|
`setPano` and `setRenderMode`, and `RecentImageryReadout` takes
|
||||||
`onSetMode`. The local RTL-SDR card's controls call `LocalSdrControls`
|
`onSetMode`. The Radio panel's and Cockpit's internet radio controls call
|
||||||
through the radio store's new `connect`/`act`, and the card labels its gain
|
`RadioControls`, and the local RTL-SDR card's controls `LocalSdrControls`,
|
||||||
options itself. `syncScopeControls` (`./ui/display`) and
|
through the radio store's new `connect`/`act`; the card labels its gain
|
||||||
|
options itself. `act` returns what the answering action returned. `syncScopeControls` (`./ui/display`) and
|
||||||
`shouldHideCollapsedRightPanels` (`./ui/layout`) are deprecated: the Display
|
`shouldHideCollapsedRightPanels` (`./ui/layout`) are deprecated: the Display
|
||||||
view and the rail now own what they did. `./ui/input` imports Vue
|
view and the rail now own what they did. `./ui/input` imports Vue
|
||||||
(samehkhamis).
|
(samehkhamis).
|
||||||
|
|||||||
@@ -146,10 +146,12 @@ a feature supplying only its accent.
|
|||||||
component compiler; the other groups follow one owner at a time),
|
component compiler; the other groups follow one owner at a time),
|
||||||
Context and Radio (in progress: `RadioPanel` renders the directory row,
|
Context and Radio (in progress: `RadioPanel` renders the directory row,
|
||||||
station card, transport, volume and playback line from the `gev-radio`
|
station card, transport, volume and playback line from the `gev-radio`
|
||||||
store that RadioControls publishes to, and `SdrCard` the local RTL-SDR
|
store that RadioControls publishes to and whose controls it answers
|
||||||
card that LocalSdrControls publishes beside it and answers through the
|
through the store's `connect`/`act` (the shared `createOwners()`; a
|
||||||
store's `connect`/`act` (the shared `createOwners()`); fields a person edits take
|
refused station tag gets the kept tag back from `act`), and `SdrCard` the
|
||||||
their value through `v-field-value`, which never writes a focused field;
|
local RTL-SDR card that LocalSdrControls publishes beside it and answers
|
||||||
|
the same way; fields a person edits take their value through
|
||||||
|
`v-field-value`, which never writes a focused field;
|
||||||
`RadioTuner` renders the directory tuner, where RadioControls keeps the
|
`RadioTuner` renders the directory tuner, where RadioControls keeps the
|
||||||
drag and its slot and publishes the band, readouts, slider and needle,
|
drag and its slot and publishes the band, readouts, slider and needle,
|
||||||
and the component lays out the tick tape against the dial's measured
|
and the component lays out the tick tape against the dial's measured
|
||||||
@@ -266,8 +268,9 @@ a feature supplying only its accent.
|
|||||||
visibility when it lays them out.
|
visibility when it lays them out.
|
||||||
`CockpitUtilityControls` renders the Display
|
`CockpitUtilityControls` renders the Display
|
||||||
and Radio launchers, which popover is open and the compact Radio
|
and Radio launchers, which popover is open and the compact Radio
|
||||||
controls from the `gev-radio` store that RadioControls publishes to; the
|
controls from the `gev-radio` store that RadioControls publishes to and
|
||||||
Display popover's slots stay static for the Cockpit display portal).
|
answers through; the Display popover's slots stay static for the Cockpit
|
||||||
|
display portal).
|
||||||
Layers that write into the right rail directly gain a slot instead.
|
Layers that write into the right rail directly gain a slot instead.
|
||||||
|
|
||||||
4. **Cleanup.** The build-time template expansion is retired: `index.html`
|
4. **Cleanup.** The build-time template expansion is retired: `index.html`
|
||||||
|
|||||||
@@ -914,6 +914,7 @@
|
|||||||
"src/layers/localAdsb/index.test.mjs",
|
"src/layers/localAdsb/index.test.mjs",
|
||||||
"src/ui/localSdrControls.test.mjs",
|
"src/ui/localSdrControls.test.mjs",
|
||||||
"src/ui/sdrCardActions.test.mjs",
|
"src/ui/sdrCardActions.test.mjs",
|
||||||
|
"src/ui/radioPanelActions.test.mjs",
|
||||||
"src/ui/localSdrMarkup.test.mjs",
|
"src/ui/localSdrMarkup.test.mjs",
|
||||||
"src/ui/localSdrPresentation.test.mjs",
|
"src/ui/localSdrPresentation.test.mjs",
|
||||||
"src/data/localReceiversProxy.test.mjs",
|
"src/data/localReceiversProxy.test.mjs",
|
||||||
|
|||||||
+15
-12
@@ -885,21 +885,24 @@ test('Cockpit Radio station changes preserve first-person camera ownership', ()
|
|||||||
);
|
);
|
||||||
assert.ok(cycleHelper, 'shared Radio cycle helper is missing');
|
assert.ok(cycleHelper, 'shared Radio cycle helper is missing');
|
||||||
assert.match(cycleHelper[1], /cycleStation\(direction, \{[\s\S]*?rotate,/);
|
assert.match(cycleHelper[1], /cycleStation\(direction, \{[\s\S]*?rotate,/);
|
||||||
|
assert.match(radioBindings, /\n cycleRadio,\n/);
|
||||||
|
const panel = fs.readFileSync(
|
||||||
|
new URL('./ui/views/panels/RadioPanel.vue', import.meta.url),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
const cockpit = fs.readFileSync(
|
||||||
|
new URL('./ui/views/panels/CockpitUtilityControls.vue', import.meta.url),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
assert.match(panel, /id="radio-prev-btn" @click="act\('cycleRadio', -1\)"/);
|
||||||
|
assert.match(panel, /id="radio-next-btn" @click="act\('cycleRadio', 1\)"/);
|
||||||
assert.match(
|
assert.match(
|
||||||
radioBindings,
|
cockpit,
|
||||||
/listen\(this\._radioPrevBtn, 'click', \(\) => cycleRadio\(-1\)\)/,
|
/id="cockpit-radio-prev-btn" @click="act\('cycleRadio', -1, \{ rotate: false \}\)"/,
|
||||||
);
|
);
|
||||||
assert.match(
|
assert.match(
|
||||||
radioBindings,
|
cockpit,
|
||||||
/listen\(this\._radioNextBtn, 'click', \(\) => cycleRadio\(1\)\)/,
|
/id="cockpit-radio-next-btn" @click="act\('cycleRadio', 1, \{ rotate: false \}\)"/,
|
||||||
);
|
|
||||||
assert.match(
|
|
||||||
radioBindings,
|
|
||||||
/listen\(this\._cockpitRadioPrevBtn, 'click', \(\) =>\s*cycleRadio\(-1, \{ rotate: false \}\),?\s*\)/,
|
|
||||||
);
|
|
||||||
assert.match(
|
|
||||||
radioBindings,
|
|
||||||
/listen\(this\._cockpitRadioNextBtn, 'click', \(\) =>\s*cycleRadio\(1, \{ rotate: false \}\),?\s*\)/,
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ test('every user-facing Context exit route settles through the failure surface',
|
|||||||
test('the Radio chip catches lifecycle rejection and semantic false through the toast wrapper', () => {
|
test('the Radio chip catches lifecycle rejection and semantic false through the toast wrapper', () => {
|
||||||
const radioControls = radioBindings.slice(
|
const radioControls = radioBindings.slice(
|
||||||
radioBindings.indexOf('const toggleRadio = async (trigger) => {'),
|
radioBindings.indexOf('const toggleRadio = async (trigger) => {'),
|
||||||
radioBindings.indexOf("this.listen(this._radioFilter, 'change'"),
|
radioBindings.indexOf('this._storeToken = this.store?.connect?.('),
|
||||||
);
|
);
|
||||||
assert.match(radioControls, /await this\.actions\.runUserAction\(/);
|
assert.match(radioControls, /await this\.actions\.runUserAction\(/);
|
||||||
assert.match(radioControls, /Radio could not \$\{enabling \? 'start' : 'stop'\} cleanly/);
|
assert.match(radioControls, /Radio could not \$\{enabling \? 'start' : 'stop'\} cleanly/);
|
||||||
@@ -178,7 +178,7 @@ test('the Radio chip catches lifecycle rejection and semantic false through the
|
|||||||
test('only the expanded Radio Enable gesture requests the contained post-enable reveal', () => {
|
test('only the expanded Radio Enable gesture requests the contained post-enable reveal', () => {
|
||||||
const radioControls = radioBindings.slice(
|
const radioControls = radioBindings.slice(
|
||||||
radioBindings.indexOf('const toggleRadio = async (trigger) => {'),
|
radioBindings.indexOf('const toggleRadio = async (trigger) => {'),
|
||||||
radioBindings.indexOf("this.listen(this._radioFilter, 'change'"),
|
radioBindings.indexOf('this._storeToken = this.store?.connect?.('),
|
||||||
);
|
);
|
||||||
assert.match(radioControls, /revealAfterEnable = enabling && trigger === this\._radioEnableBtn/);
|
assert.match(radioControls, /revealAfterEnable = enabling && trigger === this\._radioEnableBtn/);
|
||||||
assert.match(radioControls, /if \(revealAfterEnable\)\s*await this\._revealRadioControlsAfterExplicitEnable\(trigger\)/);
|
assert.match(radioControls, /if \(revealAfterEnable\)\s*await this\._revealRadioControlsAfterExplicitEnable\(trigger\)/);
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ test('Context tabs draw a visible keyboard-focus outline including active tabs',
|
|||||||
|
|
||||||
test('Context async action buttons remain focused while busy', () => {
|
test('Context async action buttons remain focused while busy', () => {
|
||||||
const init = _initGlobalContextPanel.toString();
|
const init = _initGlobalContextPanel.toString();
|
||||||
const radio = radioBindings.slice(radioBindings.indexOf('const toggleRadio = async (trigger) => {'), radioBindings.indexOf('this.listen(this._cockpitRadioToggleBtn,'));
|
const radio = radioBindings.slice(radioBindings.indexOf('const toggleRadio = async (trigger) => {'), radioBindings.indexOf('this._storeToken = this.store?.connect?.('));
|
||||||
const radioPanel = readFileSync(new URL('./ui/views/panels/RadioPanel.vue', import.meta.url), 'utf8');
|
const radioPanel = readFileSync(new URL('./ui/views/panels/RadioPanel.vue', import.meta.url), 'utf8');
|
||||||
const clear = clearSelectedLayers.toString();
|
const clear = clearSelectedLayers.toString();
|
||||||
|
|
||||||
|
|||||||
@@ -357,7 +357,7 @@ test('panel collapse is presentation-only and Radio exposes explicit voice playb
|
|||||||
const enableMethod = radioBindings;
|
const enableMethod = radioBindings;
|
||||||
assert.doesNotMatch(enableMethod, /playSelectedRadio|togglePlayback\(\).*radio-enable/i);
|
assert.doesNotMatch(enableMethod, /playSelectedRadio|togglePlayback\(\).*radio-enable/i);
|
||||||
assert.doesNotMatch(enableMethod, /contextRadio/);
|
assert.doesNotMatch(enableMethod, /contextRadio/);
|
||||||
assert.match(enableMethod, /toggleRadio\(this\._radioEnableBtn\)/);
|
assert.match(enableMethod, /toggleRadio: \(where\) =>[\s\S]*?: this\._radioEnableBtn,/);
|
||||||
// The collapse buttons render aria-expanded from the state the chrome presents.
|
// The collapse buttons render aria-expanded from the state the chrome presents.
|
||||||
assert.match(ui, /this\._presentPanel\(panelEl, \{ collapsed: nextCollapsed, auto: null \}\)/);
|
assert.match(ui, /this\._presentPanel\(panelEl, \{ collapsed: nextCollapsed, auto: null \}\)/);
|
||||||
assert.doesNotMatch(method, /panelId === 'global-context-panel'[\s\S]*?this\._radioState\?\.enabled[\s\S]*?setPanelCollapsed\('radio-panel', false\)/);
|
assert.doesNotMatch(method, /panelId === 'global-context-panel'[\s\S]*?this\._radioState\?\.enabled[\s\S]*?setPanelCollapsed\('radio-panel', false\)/);
|
||||||
|
|||||||
@@ -1055,25 +1055,16 @@ export class StyleManager extends ShellFacade {
|
|||||||
elements: {
|
elements: {
|
||||||
_cockpitDisplayToggleBtn: this._cockpitDisplayToggleBtn,
|
_cockpitDisplayToggleBtn: this._cockpitDisplayToggleBtn,
|
||||||
_cockpitRadioEnableBtn: this._cockpitRadioEnableBtn,
|
_cockpitRadioEnableBtn: this._cockpitRadioEnableBtn,
|
||||||
_cockpitRadioNextBtn: this._cockpitRadioNextBtn,
|
|
||||||
_cockpitRadioPlayBtn: this._cockpitRadioPlayBtn,
|
|
||||||
_cockpitRadioPrevBtn: this._cockpitRadioPrevBtn,
|
|
||||||
_cockpitRadioToggleBtn: this._cockpitRadioToggleBtn,
|
_cockpitRadioToggleBtn: this._cockpitRadioToggleBtn,
|
||||||
_cockpitRadioVolume: this._cockpitRadioVolume,
|
|
||||||
_cockpitUtilityControls: this._cockpitUtilityControls,
|
_cockpitUtilityControls: this._cockpitUtilityControls,
|
||||||
_radioEnableBtn: this._radioEnableBtn,
|
_radioEnableBtn: this._radioEnableBtn,
|
||||||
_radioFilter: this._radioFilter,
|
|
||||||
_radioLayerState: this._radioLayerState,
|
_radioLayerState: this._radioLayerState,
|
||||||
_radioNextBtn: this._radioNextBtn,
|
|
||||||
_radioPanel: this._radioPanel,
|
_radioPanel: this._radioPanel,
|
||||||
_radioPlayBtn: this._radioPlayBtn,
|
|
||||||
_radioPlaybackState: this._radioPlaybackState,
|
_radioPlaybackState: this._radioPlaybackState,
|
||||||
_radioPrevBtn: this._radioPrevBtn,
|
|
||||||
_radioStationHomepage: this._radioStationHomepage,
|
_radioStationHomepage: this._radioStationHomepage,
|
||||||
_radioStationMeta: this._radioStationMeta,
|
_radioStationMeta: this._radioStationMeta,
|
||||||
_radioStationName: this._radioStationName,
|
_radioStationName: this._radioStationName,
|
||||||
_radioStationTags: this._radioStationTags,
|
_radioStationTags: this._radioStationTags,
|
||||||
_radioStopBtn: this._radioStopBtn,
|
|
||||||
_radioTunerSlider: this._radioTunerSlider,
|
_radioTunerSlider: this._radioTunerSlider,
|
||||||
_radioVolume: this._radioVolume,
|
_radioVolume: this._radioVolume,
|
||||||
_radioVolumeValue: this._radioVolumeValue,
|
_radioVolumeValue: this._radioVolumeValue,
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ function fixture(t) {
|
|||||||
let enabled = false;
|
let enabled = false;
|
||||||
let toggles = 0;
|
let toggles = 0;
|
||||||
let finishToggle = null;
|
let finishToggle = null;
|
||||||
|
const ui = { presentationActive: true, filter: 'all' };
|
||||||
const actions = {
|
const actions = {
|
||||||
isRegistered: () => true,
|
isRegistered: () => true,
|
||||||
isEnabled: () => enabled,
|
isEnabled: () => enabled,
|
||||||
@@ -38,7 +39,10 @@ function fixture(t) {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
setParams: (params) => calls.push(['params', params]),
|
setParams: (params) => {
|
||||||
|
calls.push(['params', params]);
|
||||||
|
return params.filter !== 'refused';
|
||||||
|
},
|
||||||
setPanelCollapsed() {},
|
setPanelCollapsed() {},
|
||||||
setSignalCollapsed: (collapsed) => calls.push(['signal', collapsed]),
|
setSignalCollapsed: (collapsed) => calls.push(['signal', collapsed]),
|
||||||
revealStyleParameters: () => calls.push(['reveal']),
|
revealStyleParameters: () => calls.push(['reveal']),
|
||||||
@@ -60,15 +64,15 @@ function fixture(t) {
|
|||||||
_cockpitDisplayToggleBtn: $('cockpit-display-toggle-btn'),
|
_cockpitDisplayToggleBtn: $('cockpit-display-toggle-btn'),
|
||||||
_cockpitRadioToggleBtn: $('cockpit-radio-toggle-btn'),
|
_cockpitRadioToggleBtn: $('cockpit-radio-toggle-btn'),
|
||||||
_cockpitRadioEnableBtn: $('cockpit-radio-enable-btn'),
|
_cockpitRadioEnableBtn: $('cockpit-radio-enable-btn'),
|
||||||
_cockpitRadioPrevBtn: $('cockpit-radio-prev-btn'),
|
|
||||||
_cockpitRadioPlayBtn: $('cockpit-radio-play-btn'),
|
|
||||||
_cockpitRadioNextBtn: $('cockpit-radio-next-btn'),
|
|
||||||
_cockpitRadioVolume: $('cockpit-radio-volume'),
|
|
||||||
},
|
},
|
||||||
radio: {
|
radio: {
|
||||||
subscribe: () => () => {},
|
subscribe: () => () => {},
|
||||||
endTuning() {},
|
endTuning() {},
|
||||||
getTunerStations: () => [],
|
getTunerStations: () => [],
|
||||||
|
getUIState: () => ui,
|
||||||
|
cycleStation: (direction, { rotate }) =>
|
||||||
|
calls.push(['cycle', direction, rotate]),
|
||||||
|
togglePlayback: () => calls.push(['playback']),
|
||||||
},
|
},
|
||||||
actions,
|
actions,
|
||||||
canvas: document.createElement('canvas'),
|
canvas: document.createElement('canvas'),
|
||||||
@@ -95,6 +99,7 @@ function fixture(t) {
|
|||||||
$,
|
$,
|
||||||
controls,
|
controls,
|
||||||
calls,
|
calls,
|
||||||
|
ui,
|
||||||
render,
|
render,
|
||||||
outside,
|
outside,
|
||||||
finishToggle: () => finishToggle(),
|
finishToggle: () => finishToggle(),
|
||||||
@@ -236,6 +241,43 @@ test('dragging the volume shows the value at once and a render leaves the held s
|
|||||||
assert.equal(volume.value, '42');
|
assert.equal(volume.value, '42');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('the compact transport steps stations without rotating the camera', (t) => {
|
||||||
|
const { $, calls } = fixture(t);
|
||||||
|
for (const id of [
|
||||||
|
'cockpit-radio-prev-btn',
|
||||||
|
'cockpit-radio-play-btn',
|
||||||
|
'cockpit-radio-next-btn',
|
||||||
|
]) {
|
||||||
|
$(id).disabled = false;
|
||||||
|
$(id).click();
|
||||||
|
}
|
||||||
|
assert.deepEqual(calls, [
|
||||||
|
['cycle', -1, false],
|
||||||
|
['playback'],
|
||||||
|
['cycle', 1, false],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the station filter answers with the tag the directory keeps', (t) => {
|
||||||
|
const { controls, calls, ui } = fixture(t);
|
||||||
|
assert.equal(controls.store.act('setRadioFilter', 'jazz'), 'jazz');
|
||||||
|
assert.equal(controls.store.act('setRadioFilter', 'refused'), 'all');
|
||||||
|
ui.presentationActive = false;
|
||||||
|
assert.equal(controls.store.act('setRadioFilter', 'rock'), 'all');
|
||||||
|
assert.deepEqual(calls, [
|
||||||
|
['params', { filter: 'jazz' }],
|
||||||
|
['params', { filter: 'refused' }],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a destroyed RadioControls answers no controls', (t) => {
|
||||||
|
const { controls, calls } = fixture(t);
|
||||||
|
controls.destroy();
|
||||||
|
assert.equal(controls.store.act('setRadioFilter', 'jazz'), undefined);
|
||||||
|
controls.store.act('cycleRadio', 1);
|
||||||
|
assert.deepEqual(calls, []);
|
||||||
|
});
|
||||||
|
|
||||||
test('the Cockpit Enable button is marked busy while its own toggle runs', async (t) => {
|
test('the Cockpit Enable button is marked busy while its own toggle runs', async (t) => {
|
||||||
const { $, render, finishToggle, toggles } = fixture(t);
|
const { $, render, finishToggle, toggles } = fixture(t);
|
||||||
await render({ enabled: false });
|
await render({ enabled: false });
|
||||||
|
|||||||
+38
-67
@@ -312,19 +312,6 @@ export function bindRadioControls() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
this.listen(
|
|
||||||
this._radioEnableBtn,
|
|
||||||
'click',
|
|
||||||
() => void toggleRadio(this._radioEnableBtn),
|
|
||||||
);
|
|
||||||
this.listen(
|
|
||||||
this._cockpitRadioEnableBtn,
|
|
||||||
'click',
|
|
||||||
() => void toggleRadio(this._cockpitRadioEnableBtn),
|
|
||||||
);
|
|
||||||
this.listen(this._cockpitRadioToggleBtn, 'click', () => {
|
|
||||||
setCockpitDisclosure('radio', !this._cockpitDisclosureOpen('radio'));
|
|
||||||
});
|
|
||||||
this.listen(
|
this.listen(
|
||||||
document,
|
document,
|
||||||
'pointerdown',
|
'pointerdown',
|
||||||
@@ -395,61 +382,45 @@ export function bindRadioControls() {
|
|||||||
},
|
},
|
||||||
tunerListenerOptions,
|
tunerListenerOptions,
|
||||||
);
|
);
|
||||||
this.listen(this._radioFilter, 'change', () => {
|
// The Radio panel's and Cockpit's controls call these through the store.
|
||||||
const presentation = this.radio.getUIState();
|
|
||||||
if (!presentation.presentationActive) {
|
|
||||||
this._radioFilter.value = presentation.filter;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this._radioTunerDragging) finishTuner(false);
|
|
||||||
if (
|
|
||||||
!this.actions.setParams(
|
|
||||||
{
|
|
||||||
filter: this._radioFilter.value,
|
|
||||||
},
|
|
||||||
{ origin: 'user' },
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
this._radioFilter.value = this.radio.getUIState().filter;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._radioTunerBandPinnedForNavigation = false;
|
|
||||||
this._radioTunerPool = [];
|
|
||||||
refreshTunerBand({ force: true });
|
|
||||||
});
|
|
||||||
this.listen(this._radioPrevBtn, 'click', () => cycleRadio(-1));
|
|
||||||
this.listen(this._radioNextBtn, 'click', () => cycleRadio(1));
|
|
||||||
this.listen(
|
|
||||||
this._radioPlayBtn,
|
|
||||||
'click',
|
|
||||||
() => void this.radio.togglePlayback({ origin: 'user' }),
|
|
||||||
);
|
|
||||||
this.listen(this._radioStopBtn, 'click', () =>
|
|
||||||
this.radio.stopPlayback({ origin: 'user' }),
|
|
||||||
);
|
|
||||||
this.listen(this._radioVolume, 'input', () => {
|
|
||||||
const value = Number(this._radioVolume.value);
|
|
||||||
showRadioVolume.call(this, value);
|
|
||||||
this.actions.setParams({ volume: value / 100 }, { 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, so its station steps pass `rotate: false`: they
|
||||||
// rotation/fallback flights that would compete with its preUpdate pose.
|
// must never start the map-view rotation/fallback flights that would
|
||||||
this.listen(this._cockpitRadioPrevBtn, 'click', () =>
|
// compete with its preUpdate pose.
|
||||||
cycleRadio(-1, { rotate: false }),
|
this._storeToken = this.store?.connect?.({
|
||||||
);
|
/** @param {'panel'|'cockpit'} where Which Enable button was pressed. */
|
||||||
this.listen(this._cockpitRadioNextBtn, 'click', () =>
|
toggleRadio: (where) =>
|
||||||
cycleRadio(1, { rotate: false }),
|
void toggleRadio(
|
||||||
);
|
where === 'cockpit'
|
||||||
this.listen(
|
? this._cockpitRadioEnableBtn
|
||||||
this._cockpitRadioPlayBtn,
|
: this._radioEnableBtn,
|
||||||
'click',
|
),
|
||||||
() => void this.radio.togglePlayback({ origin: 'user' }),
|
toggleCockpitRadio: () =>
|
||||||
);
|
setCockpitDisclosure('radio', !this._cockpitDisclosureOpen('radio')),
|
||||||
this.listen(this._cockpitRadioVolume, 'input', () => {
|
/**
|
||||||
const value = Number(this._cockpitRadioVolume.value);
|
* @param {string} filter
|
||||||
showRadioVolume.call(this, value);
|
* @returns {string} The station tag the filter shows afterwards.
|
||||||
this.actions.setParams({ volume: value / 100 }, { origin: 'user' });
|
*/
|
||||||
|
setRadioFilter: (filter) => {
|
||||||
|
if (!this.radio.getUIState().presentationActive)
|
||||||
|
return this.radio.getUIState().filter;
|
||||||
|
if (this._radioTunerDragging) finishTuner(false);
|
||||||
|
if (!this.actions.setParams({ filter }, { origin: 'user' }))
|
||||||
|
return this.radio.getUIState().filter;
|
||||||
|
this._radioTunerBandPinnedForNavigation = false;
|
||||||
|
this._radioTunerPool = [];
|
||||||
|
refreshTunerBand({ force: true });
|
||||||
|
return filter;
|
||||||
|
},
|
||||||
|
cycleRadio,
|
||||||
|
toggleRadioPlayback: () =>
|
||||||
|
void this.radio.togglePlayback({ origin: 'user' }),
|
||||||
|
stopRadio: () => this.radio.stopPlayback({ origin: 'user' }),
|
||||||
|
setRadioVolume: (percent) => {
|
||||||
|
const value = Number(percent);
|
||||||
|
showRadioVolume.call(this, value);
|
||||||
|
this.actions.setParams({ volume: value / 100 }, { origin: 'user' });
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const updateTunerFromPointer = (event) => {
|
const updateTunerFromPointer = (event) => {
|
||||||
const rect = this._radioTunerSlider?.getBoundingClientRect();
|
const rect = this._radioTunerSlider?.getBoundingClientRect();
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ export class RadioControls {
|
|||||||
if (this.destroyed) return;
|
if (this.destroyed) return;
|
||||||
this.destroyed = true;
|
this.destroyed = true;
|
||||||
this.listeners.abort();
|
this.listeners.abort();
|
||||||
|
if (this._storeToken) this.store.detach(this._storeToken);
|
||||||
this._radioUnsubscribe?.();
|
this._radioUnsubscribe?.();
|
||||||
this._radioUnsubscribe = null;
|
this._radioUnsubscribe = null;
|
||||||
const pointer = this._radioTunerPointerId;
|
const pointer = this._radioTunerPointerId;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import { RadioControls } from './radioControls.js';
|
import { RadioControls } from './radioControls.js';
|
||||||
|
import { createOwners } from './stores/owners.js';
|
||||||
|
|
||||||
class Element extends EventTarget {
|
class Element extends EventTarget {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -39,7 +40,12 @@ function fixture() {
|
|||||||
globalThis.window = new EventTarget();
|
globalThis.window = new EventTarget();
|
||||||
const panel = new Element();
|
const panel = new Element();
|
||||||
const enable = new Element();
|
const enable = new Element();
|
||||||
const next = new Element();
|
const store = {
|
||||||
|
...createOwners(),
|
||||||
|
publish() {},
|
||||||
|
publishUtility() {},
|
||||||
|
publishTuner() {},
|
||||||
|
};
|
||||||
const calls = [];
|
const calls = [];
|
||||||
const subscribers = [];
|
const subscribers = [];
|
||||||
const radio = {
|
const radio = {
|
||||||
@@ -72,16 +78,16 @@ function fixture() {
|
|||||||
elements: {
|
elements: {
|
||||||
_radioPanel: panel,
|
_radioPanel: panel,
|
||||||
_radioEnableBtn: enable,
|
_radioEnableBtn: enable,
|
||||||
_radioNextBtn: next,
|
|
||||||
},
|
},
|
||||||
radio,
|
radio,
|
||||||
actions,
|
actions,
|
||||||
canvas: new Element(),
|
canvas: new Element(),
|
||||||
|
store,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
controls,
|
controls,
|
||||||
enable,
|
enable,
|
||||||
next,
|
store,
|
||||||
actions,
|
actions,
|
||||||
calls,
|
calls,
|
||||||
subscribers,
|
subscribers,
|
||||||
@@ -93,17 +99,17 @@ function fixture() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
test('Radio listeners and subscriptions are revoked once before tuning teardown', () => {
|
test('Radio listeners, actions and subscriptions are revoked once before tuning teardown', () => {
|
||||||
const f = fixture();
|
const f = fixture();
|
||||||
try {
|
try {
|
||||||
f.controls.connect();
|
f.controls.connect();
|
||||||
f.next.dispatchEvent(new Event('click'));
|
f.store.act('cycleRadio', 1);
|
||||||
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.controls._setCockpitDisclosure('display', true);
|
f.controls._setCockpitDisclosure('display', true);
|
||||||
f.next.dispatchEvent(new Event('click'));
|
f.store.act('cycleRadio', 1);
|
||||||
f.document.dispatchEvent(new Event('gev:radio-selected'));
|
f.document.dispatchEvent(new Event('gev:radio-selected'));
|
||||||
f.subscribers[0]({ enabled: true });
|
f.subscribers[0]({ enabled: true });
|
||||||
f.controls.connect();
|
f.controls.connect();
|
||||||
@@ -133,7 +139,7 @@ test('a delayed Radio enable cannot reveal or refocus controls after destruction
|
|||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
f.actions.setEnabled = () => pending;
|
f.actions.setEnabled = () => pending;
|
||||||
f.enable.dispatchEvent(new Event('click'));
|
f.store.act('toggleRadio', 'panel');
|
||||||
assert.equal(f.enable.getAttribute('aria-busy'), 'true');
|
assert.equal(f.enable.getAttribute('aria-busy'), 'true');
|
||||||
f.controls.destroy();
|
f.controls.destroy();
|
||||||
const attrs = new Map(f.enable.attrs);
|
const attrs = new Map(f.enable.attrs);
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
// src/ui/radioPanelActions.test.mjs — the Radio panel's controls call
|
||||||
|
// RadioControls through the radio store, and a refused station tag puts the
|
||||||
|
// select back on the tag the directory keeps.
|
||||||
|
import '../testSupport/registerDom.mjs';
|
||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { createApp } from 'vue';
|
||||||
|
import { createPinia } from 'pinia';
|
||||||
|
import { useRadioStore } from './stores/radio.js';
|
||||||
|
import RadioPanel from './views/panels/RadioPanel.vue';
|
||||||
|
|
||||||
|
function mount(t, { filter = (tag) => tag } = {}) {
|
||||||
|
document.body.replaceChildren();
|
||||||
|
const pinia = createPinia();
|
||||||
|
const calls = [];
|
||||||
|
useRadioStore(pinia).connect(
|
||||||
|
new Proxy(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
get:
|
||||||
|
(_, name) =>
|
||||||
|
(...args) => {
|
||||||
|
calls.push([name, ...args]);
|
||||||
|
return name === 'setRadioFilter' ? filter(...args) : undefined;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const app = createApp(RadioPanel).use(pinia);
|
||||||
|
app.mount(document.body.appendChild(document.createElement('div')));
|
||||||
|
t.after(() => app.unmount());
|
||||||
|
return calls;
|
||||||
|
}
|
||||||
|
const $ = (id) => document.getElementById(id);
|
||||||
|
const pickTag = (tag) => {
|
||||||
|
const select = $('radio-filter');
|
||||||
|
select.disabled = false;
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = tag;
|
||||||
|
select.append(option);
|
||||||
|
select.value = tag;
|
||||||
|
select.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
return select.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
test('the panel’s Enable, transport and volume call RadioControls', (t) => {
|
||||||
|
const calls = mount(t);
|
||||||
|
for (const id of [
|
||||||
|
'radio-enable-btn',
|
||||||
|
'radio-prev-btn',
|
||||||
|
'radio-play-btn',
|
||||||
|
'radio-next-btn',
|
||||||
|
'radio-stop-btn',
|
||||||
|
]) {
|
||||||
|
$(id).disabled = false;
|
||||||
|
$(id).click();
|
||||||
|
}
|
||||||
|
$('radio-volume').disabled = false;
|
||||||
|
$('radio-volume').value = '35';
|
||||||
|
$('radio-volume').dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
assert.deepEqual(calls, [
|
||||||
|
['toggleRadio', 'panel'],
|
||||||
|
['cycleRadio', -1],
|
||||||
|
['toggleRadioPlayback'],
|
||||||
|
['cycleRadio', 1],
|
||||||
|
['stopRadio'],
|
||||||
|
['setRadioVolume', '35'],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('an accepted station tag stays picked', (t) => {
|
||||||
|
const calls = mount(t);
|
||||||
|
assert.equal(pickTag('jazz'), 'jazz');
|
||||||
|
assert.deepEqual(calls, [['setRadioFilter', 'jazz']]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a refused station tag puts the select back on the kept tag', (t) => {
|
||||||
|
mount(t, { filter: () => 'all' });
|
||||||
|
assert.equal(pickTag('jazz'), 'all');
|
||||||
|
});
|
||||||
@@ -23,21 +23,12 @@ export function readShellElements(document = globalThis.document) {
|
|||||||
_cockpitDisplayPanel: document.getElementById('cockpit-display-panel'),
|
_cockpitDisplayPanel: document.getElementById('cockpit-display-panel'),
|
||||||
_cockpitRadioToggleBtn: document.getElementById('cockpit-radio-toggle-btn'),
|
_cockpitRadioToggleBtn: document.getElementById('cockpit-radio-toggle-btn'),
|
||||||
_cockpitRadioEnableBtn: document.getElementById('cockpit-radio-enable-btn'),
|
_cockpitRadioEnableBtn: document.getElementById('cockpit-radio-enable-btn'),
|
||||||
_cockpitRadioPrevBtn: document.getElementById('cockpit-radio-prev-btn'),
|
|
||||||
_cockpitRadioPlayBtn: document.getElementById('cockpit-radio-play-btn'),
|
|
||||||
_cockpitRadioNextBtn: document.getElementById('cockpit-radio-next-btn'),
|
|
||||||
_cockpitRadioVolume: document.getElementById('cockpit-radio-volume'),
|
|
||||||
_radioLayerState: document.getElementById('radio-layer-state'),
|
_radioLayerState: document.getElementById('radio-layer-state'),
|
||||||
_radioEnableBtn: document.getElementById('radio-enable-btn'),
|
_radioEnableBtn: document.getElementById('radio-enable-btn'),
|
||||||
_radioFilter: document.getElementById('radio-filter'),
|
|
||||||
_radioStationName: document.getElementById('radio-station-name'),
|
_radioStationName: document.getElementById('radio-station-name'),
|
||||||
_radioStationMeta: document.getElementById('radio-station-meta'),
|
_radioStationMeta: document.getElementById('radio-station-meta'),
|
||||||
_radioStationTags: document.getElementById('radio-station-tags'),
|
_radioStationTags: document.getElementById('radio-station-tags'),
|
||||||
_radioTunerSlider: document.getElementById('radio-tuner-slider'),
|
_radioTunerSlider: document.getElementById('radio-tuner-slider'),
|
||||||
_radioPrevBtn: document.getElementById('radio-prev-btn'),
|
|
||||||
_radioPlayBtn: document.getElementById('radio-play-btn'),
|
|
||||||
_radioNextBtn: document.getElementById('radio-next-btn'),
|
|
||||||
_radioStopBtn: document.getElementById('radio-stop-btn'),
|
|
||||||
_radioVolume: document.getElementById('radio-volume'),
|
_radioVolume: document.getElementById('radio-volume'),
|
||||||
_radioVolumeValue: document.getElementById('radio-volume-value'),
|
_radioVolumeValue: document.getElementById('radio-volume-value'),
|
||||||
_radioPlaybackState: document.getElementById('radio-playback-state'),
|
_radioPlaybackState: document.getElementById('radio-playback-state'),
|
||||||
|
|||||||
@@ -31,11 +31,12 @@ export function createOwners() {
|
|||||||
* name, if any.
|
* name, if any.
|
||||||
* @param {string} name
|
* @param {string} name
|
||||||
* @param {...unknown} args
|
* @param {...unknown} args
|
||||||
|
* @returns {unknown} What the action returned, if one answered.
|
||||||
*/
|
*/
|
||||||
function act(name, ...args) {
|
function act(name, ...args) {
|
||||||
for (let index = owners.length - 1; index >= 0; index -= 1) {
|
for (let index = owners.length - 1; index >= 0; index -= 1) {
|
||||||
const action = owners[index].actions[name];
|
const action = owners[index].actions[name];
|
||||||
if (action) return void action(...args);
|
if (action) return action(...args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,3 +26,10 @@ test('the newest owner with an action answers it, and detach hands it back', ()
|
|||||||
['first.play', 2],
|
['first.play', 2],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('act returns what the answering action returned', () => {
|
||||||
|
const { connect, act } = createOwners();
|
||||||
|
connect({ pick: (tag) => `kept ${tag}` });
|
||||||
|
assert.equal(act('pick', 'jazz'), 'kept jazz');
|
||||||
|
assert.equal(act('missing'), undefined);
|
||||||
|
});
|
||||||
@@ -9,9 +9,12 @@ import { useRadioStore } from '../../stores/radio.js';
|
|||||||
// The Cockpit's Display and Radio launchers and their popovers. RadioControls
|
// The Cockpit's Display and Radio launchers and their popovers. RadioControls
|
||||||
// presents which popover is open and the compact Radio controls; until it
|
// presents which popover is open and the compact Radio controls; until it
|
||||||
// does, both are closed and the controls show their initial state. The
|
// does, both are closed and the controls show their initial state. The
|
||||||
// controls keep its listeners. The Display popover's slots are static: the
|
// controls call RadioControls through the store. The Display popover's slots
|
||||||
// Cockpit display portal moves the real Display controls into them.
|
// are static: the Cockpit display portal moves the real Display controls into
|
||||||
const { view: radio, utility } = storeToRefs(useRadioStore());
|
// them.
|
||||||
|
const store = useRadioStore();
|
||||||
|
const { act } = store;
|
||||||
|
const { view: radio, utility } = storeToRefs(store);
|
||||||
const open = computed(() => utility.value ?? { display: false, radio: false });
|
const open = computed(() => utility.value ?? { display: false, radio: false });
|
||||||
const cockpit = computed(() => radio.value?.cockpit ?? null);
|
const cockpit = computed(() => radio.value?.cockpit ?? null);
|
||||||
const displayLabel = computed(() =>
|
const displayLabel = computed(() =>
|
||||||
@@ -45,19 +48,19 @@ const radioLabel = computed(() =>
|
|||||||
<div class="cockpit-utility-launcher" data-cockpit-launcher="radio">
|
<div class="cockpit-utility-launcher" data-cockpit-launcher="radio">
|
||||||
<span data-i18n>RADIO</span>
|
<span data-i18n>RADIO</span>
|
||||||
<span class="cockpit-utility-divider" aria-hidden="true"></span>
|
<span class="cockpit-utility-divider" aria-hidden="true"></span>
|
||||||
<button id="cockpit-radio-toggle-btn" class="cockpit-utility-glyph" type="button" :aria-expanded="String(open.radio)" aria-controls="cockpit-radio-panel" :aria-label="radioLabel" :title="radioLabel">{{ open.radio ? '▶' : '◀' }}</button>
|
<button id="cockpit-radio-toggle-btn" @click="act('toggleCockpitRadio')" class="cockpit-utility-glyph" type="button" :aria-expanded="String(open.radio)" aria-controls="cockpit-radio-panel" :aria-label="radioLabel" :title="radioLabel">{{ open.radio ? '▶' : '◀' }}</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="cockpit-radio-panel" class="cockpit-utility-popover cockpit-radio-compact" role="group" aria-label="Cockpit compact Radio controls" :hidden="!open.radio" data-i18n-aria-label>
|
<div id="cockpit-radio-panel" class="cockpit-utility-popover cockpit-radio-compact" role="group" aria-label="Cockpit compact Radio controls" :hidden="!open.radio" data-i18n-aria-label>
|
||||||
<strong id="cockpit-radio-station" class="cockpit-radio-station">{{ cockpit ? cockpit.station : t('READY') }}</strong>
|
<strong id="cockpit-radio-station" class="cockpit-radio-station">{{ cockpit ? cockpit.station : t('READY') }}</strong>
|
||||||
<button id="cockpit-radio-enable-btn" class="panel-layer-toggle" v-class-toggle="{ active: cockpit?.enable.active }" type="button" :aria-pressed="String(cockpit?.enable.active ?? false)" :aria-label="cockpit?.enable.label" :aria-disabled="cockpit ? String(cockpit.enable.busy) : undefined" :aria-busy="cockpit ? String(cockpit.enable.busy) : undefined">{{ cockpit ? cockpit.enable.text : t('ENABLE') }}</button>
|
<button id="cockpit-radio-enable-btn" @click="act('toggleRadio', 'cockpit')" class="panel-layer-toggle" v-class-toggle="{ active: cockpit?.enable.active }" type="button" :aria-pressed="String(cockpit?.enable.active ?? false)" :aria-label="cockpit?.enable.label" :aria-disabled="cockpit ? String(cockpit.enable.busy) : undefined" :aria-busy="cockpit ? String(cockpit.enable.busy) : undefined">{{ cockpit ? cockpit.enable.text : t('ENABLE') }}</button>
|
||||||
<div class="cockpit-radio-transport">
|
<div class="cockpit-radio-transport">
|
||||||
<button id="cockpit-radio-prev-btn" type="button" aria-label="Previous filtered radio station" :disabled="radio ? radio.transport.stepDisabled : true" data-i18n-aria-label>◀◀</button>
|
<button id="cockpit-radio-prev-btn" @click="act('cycleRadio', -1, { rotate: false })" type="button" aria-label="Previous filtered radio station" :disabled="radio ? radio.transport.stepDisabled : true" data-i18n-aria-label>◀◀</button>
|
||||||
<button id="cockpit-radio-play-btn" type="button" v-class-toggle="{ active: radio?.transport.play.active }" :aria-label="radio ? radio.transport.play.label : t('Play selected radio station')" :title="cockpit?.play.title" :disabled="radio ? radio.transport.play.disabled : true">{{ cockpit ? cockpit.play.glyph : '▶' }}</button>
|
<button id="cockpit-radio-play-btn" @click="act('toggleRadioPlayback')" type="button" v-class-toggle="{ active: radio?.transport.play.active }" :aria-label="radio ? radio.transport.play.label : t('Play selected radio station')" :title="cockpit?.play.title" :disabled="radio ? radio.transport.play.disabled : true">{{ cockpit ? cockpit.play.glyph : '▶' }}</button>
|
||||||
<button id="cockpit-radio-next-btn" type="button" aria-label="Next filtered radio station" :disabled="radio ? radio.transport.stepDisabled : true" data-i18n-aria-label>▶▶</button>
|
<button id="cockpit-radio-next-btn" @click="act('cycleRadio', 1, { rotate: false })" type="button" aria-label="Next filtered radio station" :disabled="radio ? radio.transport.stepDisabled : true" data-i18n-aria-label>▶▶</button>
|
||||||
</div>
|
</div>
|
||||||
<label class="cockpit-radio-volume" for="cockpit-radio-volume">
|
<label class="cockpit-radio-volume" for="cockpit-radio-volume">
|
||||||
<span data-i18n>VOLUME</span>
|
<span data-i18n>VOLUME</span>
|
||||||
<input id="cockpit-radio-volume" class="gev-quantitative-slider" type="range" min="0" max="100" value="80" aria-label="Cockpit Radio volume" :disabled="cockpit ? cockpit.volume.disabled : true" v-field-value="cockpit ? cockpit.volume.value : 80" data-i18n-aria-label />
|
<input id="cockpit-radio-volume" @input="act('setRadioVolume', $event.target.value)" class="gev-quantitative-slider" type="range" min="0" max="100" value="80" aria-label="Cockpit Radio volume" :disabled="cockpit ? cockpit.volume.disabled : true" v-field-value="cockpit ? cockpit.volume.value : 80" data-i18n-aria-label />
|
||||||
<output id="cockpit-radio-volume-value" class="gev-slider-value" for="cockpit-radio-volume">{{ cockpit ? `${cockpit.volume.value}%` : '80%' }}</output>
|
<output id="cockpit-radio-volume-value" class="gev-slider-value" for="cockpit-radio-volume">{{ cockpit ? `${cockpit.volume.value}%` : '80%' }}</output>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,8 +13,15 @@ import SdrCard from './SdrCard.vue';
|
|||||||
// The Radio panel inside Context. RadioControls presents the directory row,
|
// The Radio panel inside Context. RadioControls presents the directory row,
|
||||||
// station card, transport, volume and playback line and publishes them;
|
// station card, transport, volume and playback line and publishes them;
|
||||||
// until it does, the panel shows its initial markup. RadioTuner renders the
|
// until it does, the panel shows its initial markup. RadioTuner renders the
|
||||||
// directory tuner and SdrCard the local RTL-SDR card.
|
// directory tuner and SdrCard the local RTL-SDR card. Its controls call
|
||||||
const { view: radio, sdr } = storeToRefs(useRadioStore());
|
// RadioControls through the store.
|
||||||
|
const store = useRadioStore();
|
||||||
|
const { act } = store;
|
||||||
|
const { view: radio, sdr } = storeToRefs(store);
|
||||||
|
// A refused station tag puts the select back on the tag the filter keeps.
|
||||||
|
const setFilter = (select) => {
|
||||||
|
select.value = act('setRadioFilter', select.value) ?? select.value;
|
||||||
|
};
|
||||||
// Only whether the receiver is connected reaches the panel; the card renders
|
// Only whether the receiver is connected reaches the panel; the card renders
|
||||||
// its own frames.
|
// its own frames.
|
||||||
const sdrConnected = computed(() => sdr.value?.connectionActive ?? null);
|
const sdrConnected = computed(() => sdr.value?.connectionActive ?? null);
|
||||||
@@ -31,9 +38,9 @@ const radioWords = {
|
|||||||
<RailPanel id="radio-panel" tag="section" start-collapsed v-class-toggle="{ ...(radio && { 'radio-enabled': radio.enabled, 'lifecycle-uncertain': radio.uncertain }), ...(sdrConnected !== null && { 'sdr-connected': sdrConnected }) }" aria-label="Internet radio and local SDR" data-i18n-aria-label :title="t('RADIO')" :toggle-words="radioWords" :glow="false" scroll>
|
<RailPanel id="radio-panel" tag="section" start-collapsed v-class-toggle="{ ...(radio && { 'radio-enabled': radio.enabled, 'lifecycle-uncertain': radio.uncertain }), ...(sdrConnected !== null && { 'sdr-connected': sdrConnected }) }" aria-label="Internet radio and local SDR" data-i18n-aria-label :title="t('RADIO')" :toggle-words="radioWords" :glow="false" scroll>
|
||||||
<template #badge><span id="radio-layer-state" class="radio-layer-state" :class="{ active: radio?.layerState.active }">{{ radio ? radio.layerState.text : t('OFF') }}</span></template>
|
<template #badge><span id="radio-layer-state" class="radio-layer-state" :class="{ active: radio?.layerState.active }">{{ radio ? radio.layerState.text : t('OFF') }}</span></template>
|
||||||
<div class="radio-directory-row">
|
<div class="radio-directory-row">
|
||||||
<button id="radio-enable-btn" class="panel-layer-toggle" :class="{ active: radio?.enable.active }" type="button" :aria-pressed="String(radio?.enable.active ?? false)" :aria-label="radio?.enable.label" :aria-disabled="radio ? String(radio.enable.busy) : undefined" :aria-busy="radio ? String(radio.enable.busy) : undefined">{{ radio ? radio.enable.text : t('ENABLE') }}</button>
|
<button id="radio-enable-btn" @click="act('toggleRadio', 'panel')" class="panel-layer-toggle" :class="{ active: radio?.enable.active }" type="button" :aria-pressed="String(radio?.enable.active ?? false)" :aria-label="radio?.enable.label" :aria-disabled="radio ? String(radio.enable.busy) : undefined" :aria-busy="radio ? String(radio.enable.busy) : undefined">{{ radio ? radio.enable.text : t('ENABLE') }}</button>
|
||||||
<label class="radio-filter-label" for="radio-filter" data-i18n>STATION TAG</label>
|
<label class="radio-filter-label" for="radio-filter" data-i18n>STATION TAG</label>
|
||||||
<select class="gev-field" id="radio-filter" aria-label="Filter stations by station tag" :disabled="radio ? radio.filter.disabled : true" v-bind="radio?.filter.color ? { style: { color: radio.filter.color } } : {}" v-range-value="radio ? radio.filter.value : 'all'" data-i18n-aria-label>
|
<select class="gev-field" id="radio-filter" @change="setFilter($event.target)" aria-label="Filter stations by station tag" :disabled="radio ? radio.filter.disabled : true" v-bind="radio?.filter.color ? { style: { color: radio.filter.color } } : {}" v-range-value="radio ? radio.filter.value : 'all'" data-i18n-aria-label>
|
||||||
<option v-if="!radio?.filter.options" value="all">{{ t('All', { context: 'radio tag' }) }}</option><template v-else><option v-for="option in radio.filter.options" :key="option.id" :value="option.id" :data-radio-color="option.color" :style="{ color: option.color }" :aria-label="option.label">● {{ option.label }}</option></template>
|
<option v-if="!radio?.filter.options" value="all">{{ t('All', { context: 'radio tag' }) }}</option><template v-else><option v-for="option in radio.filter.options" :key="option.id" :value="option.id" :data-radio-color="option.color" :style="{ color: option.color }" :aria-label="option.label">● {{ option.label }}</option></template>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,14 +51,14 @@ const radioWords = {
|
|||||||
</div>
|
</div>
|
||||||
<RadioTuner />
|
<RadioTuner />
|
||||||
<div class="radio-transport" role="group" aria-label="Radio playback" data-i18n-aria-label>
|
<div class="radio-transport" role="group" aria-label="Radio playback" data-i18n-aria-label>
|
||||||
<button id="radio-prev-btn" type="button" aria-label="Previous filtered station" :disabled="radio ? radio.transport.stepDisabled : true" data-i18n-aria-label data-i18n>PREV</button>
|
<button id="radio-prev-btn" @click="act('cycleRadio', -1)" type="button" aria-label="Previous filtered station" :disabled="radio ? radio.transport.stepDisabled : true" data-i18n-aria-label data-i18n>PREV</button>
|
||||||
<button id="radio-play-btn" type="button" v-class-toggle="{ active: radio?.transport.play.active }" :aria-label="radio ? radio.transport.play.label : t('Play selected station')" :disabled="radio ? radio.transport.play.disabled : true">{{ radio ? radio.transport.play.text : t('PLAY') }}</button>
|
<button id="radio-play-btn" @click="act('toggleRadioPlayback')" type="button" v-class-toggle="{ active: radio?.transport.play.active }" :aria-label="radio ? radio.transport.play.label : t('Play selected station')" :disabled="radio ? radio.transport.play.disabled : true">{{ radio ? radio.transport.play.text : t('PLAY') }}</button>
|
||||||
<button id="radio-next-btn" type="button" aria-label="Next filtered station" :disabled="radio ? radio.transport.stepDisabled : true" data-i18n-aria-label data-i18n>NEXT</button>
|
<button id="radio-next-btn" @click="act('cycleRadio', 1)" type="button" aria-label="Next filtered station" :disabled="radio ? radio.transport.stepDisabled : true" data-i18n-aria-label data-i18n>NEXT</button>
|
||||||
<button id="radio-stop-btn" type="button" aria-label="Stop radio playback" :disabled="radio ? radio.transport.stopDisabled : true" data-i18n-aria-label data-i18n>STOP</button>
|
<button id="radio-stop-btn" @click="act('stopRadio')" type="button" aria-label="Stop radio playback" :disabled="radio ? radio.transport.stopDisabled : true" data-i18n-aria-label data-i18n>STOP</button>
|
||||||
</div>
|
</div>
|
||||||
<label class="radio-volume-row" for="radio-volume">
|
<label class="radio-volume-row" for="radio-volume">
|
||||||
<span data-i18n>VOLUME</span>
|
<span data-i18n>VOLUME</span>
|
||||||
<input id="radio-volume" class="gev-quantitative-slider" type="range" min="0" max="100" value="80" aria-label="Radio volume" :disabled="radio?.volume.disabled" v-field-value="radio ? radio.volume.value : 80" data-i18n-aria-label />
|
<input id="radio-volume" @input="act('setRadioVolume', $event.target.value)" class="gev-quantitative-slider" type="range" min="0" max="100" value="80" aria-label="Radio volume" :disabled="radio?.volume.disabled" v-field-value="radio ? radio.volume.value : 80" data-i18n-aria-label />
|
||||||
<output id="radio-volume-value" class="gev-slider-value" for="radio-volume">{{ radio ? `${radio.volume.value}%` : '80%' }}</output>
|
<output id="radio-volume-value" class="gev-slider-value" for="radio-volume">{{ radio ? `${radio.volume.value}%` : '80%' }}</output>
|
||||||
</label>
|
</label>
|
||||||
<div id="radio-playback-state" class="radio-playback-state" :class="{ error: radio?.playback.error }" role="status" aria-live="polite">{{ radio ? radio.playback.text : t('Radio off') }}</div>
|
<div id="radio-playback-state" class="radio-playback-state" :class="{ error: radio?.playback.error }" role="status" aria-live="polite">{{ radio ? radio.playback.text : t('Radio off') }}</div>
|
||||||
|
|||||||
Reference in new issue
Block a user