From ccb7edd0d1f3235bf7cf9626a651e3a6161db838 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Sat, 31 Oct 2026 00:20:07 -0700 Subject: [PATCH] feat(ui): render the Radio panel's directory, station card and transport from a store RadioPanel, lifted from ContextView, renders the layer state, Enable, the tag filter, station card, transport, volume and playback line from the gev-radio store that RadioControls publishes to. The tuner, the local SDR card and the Cockpit radio controls keep their element bindings. Enable's busy mark and the dragged volume go through the owner so a render never leaves them stale, and the markup's tag option stands until the categories first change. ./ui/radio now imports Vue and Pinia. --- CHANGELOG.md | 4 + docs/VUE-MIGRATION.md | 6 +- scripts/package-boundaries.json | 11 +- scripts/qa-radio.mjs | 8 +- src/contextTabKeyboard.test.mjs | 12 +- src/i18n/zh-CN-display.test.mjs | 9 +- src/ui/radioBindings.js | 21 +- src/ui/radioControls.js | 20 +- src/ui/radioPanel.test.mjs | 219 ++++++++++++++++ src/ui/radioPresentation.js | 243 +++++++++--------- .../radioPresentation.playbackError.test.mjs | 5 +- src/ui/radioPresentationTransition.test.mjs | 8 +- src/ui/stores/radio.js | 20 ++ src/ui/views/ContextView.vue | 133 +--------- src/ui/views/panels/RadioPanel.vue | 144 +++++++++++ 15 files changed, 593 insertions(+), 270 deletions(-) create mode 100644 src/ui/radioPanel.test.mjs create mode 100644 src/ui/stores/radio.js create mode 100644 src/ui/views/panels/RadioPanel.vue diff --git a/CHANGELOG.md b/CHANGELOG.md index c0b42a7bc..22f5257d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,10 @@ (`./ui/layout`) are deprecated: the Display view and the rail now own what they did. `./ui/input` imports Vue (samehkhamis). +- `./ui/cctv` and `./ui/radio` import Vue and Pinia: their controllers + publish the panel's state to a store that the CCTV and Radio views render, + instead of writing the panel's markup themselves (samehkhamis). + ## [0.3.0] — 2026-10-27 — Street Level, On Your Phone, in Your Language - Show a Street Level view facing 359.5° or more as 0°. The viewer caption diff --git a/docs/VUE-MIGRATION.md b/docs/VUE-MIGRATION.md index 8f2f7ac4f..cc7d11622 100644 --- a/docs/VUE-MIGRATION.md +++ b/docs/VUE-MIGRATION.md @@ -102,7 +102,11 @@ They contain no application state. style parameter rows render through Vue behind `createStyleParameters()`, as a render function so the portable `./ui/input` entry needs no component compiler; the other groups follow one owner at a time), - Context and Radio, the command dock, + Context and Radio (in progress: `RadioPanel` renders the directory row, + station card, transport, volume and playback line from the `gev-radio` + store that RadioControls publishes to; the tuner, the local SDR card and + the Cockpit radio controls keep their owners' element bindings), the + command dock, the phone shell, then Cockpit. Layers that write into the right rail directly gain a slot instead. diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index 84cec6de3..a78030afc 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -24,6 +24,7 @@ "src/ui/views/CockpitView.vue", "src/ui/views/CommandDockView.vue", "src/ui/views/ContextView.vue", + "src/ui/views/panels/RadioPanel.vue", "src/ui/views/DisplayControlsView.vue", "src/ui/views/LayerPanelsView.vue", "src/ui/views/ProviderSettingsView.vue", @@ -50,11 +51,13 @@ "src/ui/cctv/CctvCalValue.vue", "src/ui/cctv/CctvPicker.vue", "src/ui/stores/cctv.js", + "src/ui/stores/radio.js", "src/ui/stores/streetLevel.js", "src/ui/rail/RailChips.vue", "src/ui/views/panels/DisplayPanel.vue", "src/ui/stores/display.js", - "src/ui/rail/rangeValue.js" + "src/ui/rail/rangeValue.js", + "src/ui/rail/classToggle.js" ], "external": ["vue", "pinia"] }, @@ -622,6 +625,8 @@ "src/ui/radioControls.js", "src/ui/radioBindings.js", "src/ui/radioPresentation.js", + "src/ui/vueRuntime.js", + "src/ui/stores/radio.js", "src/ui/radioTunerModel.js", "src/i18n/index.js", "src/i18n/locale.js", @@ -636,7 +641,7 @@ "src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-Hant.json" ], - "external": [] + "external": ["vue", "pinia"] }, "cctv-controls": { "exports": ["./ui/cctv"], @@ -959,6 +964,7 @@ "src/ui/weather/WeatherReadout.vue", "src/ui/stores/streetLevel.js", "src/ui/stores/cctv.js", + "src/ui/stores/radio.js", "src/ui/stores/display.js" ], "external": ["cesium", "vue", "pinia"] @@ -2927,6 +2933,7 @@ "src/ui/recentImagery/RecentImageryReadout.vue", "src/ui/stores/streetLevel.js", "src/ui/stores/cctv.js", + "src/ui/stores/radio.js", "src/ui/stores/display.js" ], "external": [ diff --git a/scripts/qa-radio.mjs b/scripts/qa-radio.mjs index a71c69099..366557185 100644 --- a/scripts/qa-radio.mjs +++ b/scripts/qa-radio.mjs @@ -2957,7 +2957,7 @@ async function main() { await page.evaluate(async () => { await window.__godsEyeView.dataManager.layers.get('radio').module.update(); }); - const tunerRefreshRelease = await page.evaluate((target) => { + const tunerRefreshRelease = await page.evaluate(async (target) => { const gev = window.__godsEyeView; const slider = document.getElementById('radio-tuner-slider'); const displayedBeforeRelease = document.getElementById('radio-tuner-station').textContent; @@ -2970,6 +2970,10 @@ async function main() { clientY: target.clientY, })); const state = gev.dataManager.layers.get('radio').module.getUIState(); + // The playback line renders from the Radio store once Vue flushes. + const message = await Promise.resolve().then( + () => document.getElementById('radio-playback-state').textContent, + ); return { displayedBeforeRelease, frozenGeneration, @@ -2978,7 +2982,7 @@ async function main() { plays: window.__qaRadioPlayCalls.length, value: document.getElementById('radio-tuner-value').textContent, stationText: document.getElementById('radio-tuner-station').textContent, - message: document.getElementById('radio-playback-state').textContent, + message, targetId: target.id, }; }, tunerRefreshTarget); diff --git a/src/contextTabKeyboard.test.mjs b/src/contextTabKeyboard.test.mjs index cf221756a..07386647e 100644 --- a/src/contextTabKeyboard.test.mjs +++ b/src/contextTabKeyboard.test.mjs @@ -130,16 +130,20 @@ test('Context tabs draw a visible keyboard-focus outline including active tabs', test('Context async action buttons remain focused while busy', () => { const init = _initGlobalContextPanel.toString(); const radio = radioBindings.slice(radioBindings.indexOf('const toggleRadio = async (trigger) => {'), radioBindings.indexOf('this.listen(this._cockpitRadioToggleBtn,')); - const radioSync = radioPresentation.slice(0, radioPresentation.indexOf('if (this._radioFilter)')); + const radioSync = radioPresentation.slice(0, radioPresentation.indexOf('this._radioView = {')); + const radioPanel = readFileSync(new URL('./ui/views/panels/RadioPanel.vue', import.meta.url), 'utf8'); const clear = clearSelectedLayers.toString(); assert.match(init, /button\.getAttribute\('aria-busy'\) === 'true'/); assert.doesNotMatch(init, /button\.disabled\s*=\s*true/); assert.match(radio, /trigger\.getAttribute\('aria-busy'\) === 'true'/); assert.doesNotMatch(radio, /trigger\.disabled\s*=\s*true/); - for (const name of ['_radioEnableBtn', '_cockpitRadioEnableBtn']) { - assert.match(radioSync, new RegExp(`${name}\\.disabled = false`)); - } + assert.match(radioSync, /_cockpitRadioEnableBtn\.disabled = false/); + // The Radio view never disables its Enable button; it marks it busy. + const enable = radioPanel.match(/