From d393b3cefcf2cafcadb8099a9f3bb82b4df90b40 Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Sat, 1 Aug 2026 11:11:47 +0200 Subject: [PATCH] feat(ui): drive workspaces from rig capabilities --- .../trx-frontend-http/assets/web/app.js | 29 +++++++++++++++---- .../trx-frontend-http/assets/web/ui-core.js | 21 ++++++++------ 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js index 7114fd45..7f435438 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js @@ -23,6 +23,7 @@ window.onDecoderRegistryReady = function (fn) { for (const fn of _decoderRegistryReadyCallbacks) fn(); _decoderRegistryReadyCallbacks.length = 0; hideUnsupportedDecoderTabs(); + refreshOperatorLayoutCapabilities(); } } catch (e) { console.error("Failed to fetch decoder registry:", e); @@ -333,8 +334,14 @@ function applyCapabilities(caps) { const txAudioBtn = document.getElementById("tx-audio-btn"); const txVolSlider = document.getElementById("tx-vol"); const txVolControl = txVolSlider ? txVolSlider.closest(".vol-label") : null; - if (txPowerCol) txPowerCol.style.display = caps.tx ? "" : "none"; + if (txPowerCol) { + txPowerCol.style.display = ""; + const label = txPowerCol.querySelector(".label span"); + if (label) label.textContent = caps.tx ? "Transmit / Power" : "Power / Tuning"; + } if (pttBtn) pttBtn.style.display = caps.tx ? "" : "none"; + if (powerBtn) powerBtn.style.display = ""; + if (lockBtn) lockBtn.style.display = caps.lockable ? "" : "none"; if (txMetersRow) txMetersRow.style.display = caps.tx ? "" : "none"; if (txAudioBtn) txAudioBtn.style.display = caps.tx ? "" : "none"; if (txVolControl) txVolControl.style.display = caps.tx ? "" : "none"; @@ -344,7 +351,7 @@ function applyCapabilities(caps) { // TX limit row const txLimitRow = document.getElementById("tx-limit-row"); - if (txLimitRow && !caps.tx_limit) txLimitRow.style.display = "none"; + if (txLimitRow) txLimitRow.style.display = caps.tx_limit ? "" : "none"; // VFO row const vfoRow = document.getElementById("vfo-row"); @@ -1305,10 +1312,7 @@ async function refreshRigList() { } }); serverRigs = rigs; - window.trxUi?.setBroadcastLayoutAvailable(rigs.some((rig) => - Array.isArray(rig?.supported_modes) - && rig.supported_modes.map(normalizeMode).includes("WFM") - )); + refreshOperatorLayoutCapabilities(); serverActiveRigId = data.active_remote || null; applyRigList(data.active_remote, rigIds, displayNames); window.trx.modules.map?.syncAprsReceiverMarker(); @@ -1317,6 +1321,19 @@ async function refreshRigList() { } } +function refreshOperatorLayoutCapabilities() { + const rigModes = serverRigs.map((rig) => + Array.isArray(rig?.supported_modes) ? rig.supported_modes.map(normalizeMode).filter(Boolean) : [] + ); + const decoderModes = new Set(decoderRegistry.flatMap((decoder) => + Array.isArray(decoder?.active_modes) ? decoder.active_modes.map(normalizeMode).filter(Boolean) : [] + )); + window.trxUi?.setLayoutCapabilities({ + broadcast: rigModes.some((modes) => modes.includes("WFM")), + digital: decoderModes.size > 0 && rigModes.some((modes) => modes.some((mode) => decoderModes.has(mode))), + }); +} + function showHint(msg, duration) { powerHint.textContent = msg; if (hintTimer) clearTimeout(hintTimer); diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/ui-core.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/ui-core.js index fb72d6cd..f29ebb73 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/ui-core.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/ui-core.js @@ -128,13 +128,13 @@ const layouts = { compact: { label: "Compact", advanced: false, preferredTab: "main" }, broadcast: { label: "Broadcast", advanced: false, preferredTab: "main", capability: "broadcast" }, - digital: { label: "Digital", advanced: false, preferredTab: "digital-modes" }, + digital: { label: "Digital", advanced: false, preferredTab: "digital-modes", capability: "digital" }, full: { label: "Full controls", advanced: true, preferredTab: "main" }, }; - let hasBroadcastRig = false; + const layoutCapabilities = { broadcast: false, digital: false }; - function broadcastLayoutAvailable() { - return hasBroadcastRig; + function layoutAvailable(layout) { + return !layout.capability || layoutCapabilities[layout.capability] === true; } function refreshLayoutOptions() { @@ -143,7 +143,7 @@ const previous = select.value || document.body.dataset.operatorLayout || "compact"; select.replaceChildren(); Object.entries(layouts).forEach(([value, layout]) => { - if (layout.capability === "broadcast" && !broadcastLayoutAvailable()) return; + if (!layoutAvailable(layout)) return; select.add(new Option(layout.label, value)); }); const available = Array.from(select.options).some(option => option.value === previous); @@ -151,14 +151,17 @@ if (!available && previous === "broadcast") api.applyLayout("compact"); } - api.setBroadcastLayoutAvailable = function setBroadcastLayoutAvailable(available) { - hasBroadcastRig = Boolean(available); + api.setLayoutCapabilities = function setLayoutCapabilities(capabilities = {}) { + Object.keys(layoutCapabilities).forEach((name) => { + if (name in capabilities) layoutCapabilities[name] = Boolean(capabilities[name]); + }); refreshLayoutOptions(); }; api.applyLayout = function applyLayout(name, options = {}) { - const permittedName = name === "broadcast" && !broadcastLayoutAvailable() ? "compact" : name; - const layout = layouts[permittedName] || layouts.compact; + const requestedLayout = layouts[name]; + const permittedName = requestedLayout && layoutAvailable(requestedLayout) ? name : "compact"; + const layout = layouts[permittedName]; document.body.dataset.operatorLayout = permittedName in layouts ? permittedName : "compact"; localStorage.setItem("trxOperatorLayout", document.body.dataset.operatorLayout); const details = document.getElementById("advanced-radio-controls");