fix(ui): make rig switching atomic and visible
This commit is contained in:
@@ -443,6 +443,7 @@ const signalSplitValueEl = document.getElementById("signal-split-value");
|
||||
const overviewPeakHoldEl = document.getElementById("overview-peak-hold");
|
||||
const themeToggleBtn = document.getElementById("theme-toggle");
|
||||
const headerRigSwitchSelect = document.getElementById("header-rig-switch-select");
|
||||
const headerRigSummary = document.getElementById("header-rig-summary");
|
||||
const headerStylePickSelect = document.getElementById("header-style-pick-select");
|
||||
const rdsPsOverlay = document.getElementById("rds-ps-overlay");
|
||||
const tabMainEl = document.getElementById("tab-main");
|
||||
@@ -902,6 +903,7 @@ async function restorePreviousTuneState() {
|
||||
let lastRigIds = [];
|
||||
let lastRigDisplayNames = {};
|
||||
let lastActiveRigId = null;
|
||||
let rigSwitchInProgress = false;
|
||||
let lastCityLabel = "";
|
||||
let sseSessionId = null;
|
||||
const originalTitle = document.title;
|
||||
@@ -1246,6 +1248,20 @@ function populateRigPicker(selectEl, rigIds, activeRigId, disabled) {
|
||||
selectEl.disabled = disabled;
|
||||
}
|
||||
|
||||
function updateRigIdentitySummary(rigId, pending = false) {
|
||||
if (!headerRigSummary) return;
|
||||
const rig = serverRigs.find((entry) => entry?.remote === rigId);
|
||||
if (!rig) {
|
||||
headerRigSummary.textContent = pending ? "Switching rigs…" : "No rig details available";
|
||||
return;
|
||||
}
|
||||
const hardware = [rig.manufacturer, rig.model].map(value => String(value || "").trim()).filter(Boolean).join(" ") || rig.remote;
|
||||
const modes = Array.isArray(rig.supported_modes) ? rig.supported_modes.map(normalizeMode).filter(Boolean) : [];
|
||||
const features = [rig.tx ? "TX" : "RX", rig.filter_controls ? "SDR filters" : null, ...modes.slice(0, 5)];
|
||||
if (modes.length > 5) features.push(`+${modes.length - 5} modes`);
|
||||
headerRigSummary.textContent = `${pending ? "Switching to " : ""}${hardware} · ${features.filter(Boolean).join(" · ")}`;
|
||||
}
|
||||
|
||||
function updateRigSubtitle(activeRigId) {
|
||||
if (!rigSubtitle) return;
|
||||
const name = (activeRigId && lastRigDisplayNames[activeRigId]) || activeRigId || "--";
|
||||
@@ -1282,6 +1298,7 @@ function applyRigList(activeRigId, rigIds, displayNames) {
|
||||
const disableSwitch = lastRigIds.length === 0 || !authRole || authRole === "rx";
|
||||
populateRigPicker(headerRigSwitchSelect, lastRigIds, lastActiveRigId, disableSwitch);
|
||||
updateRigSubtitle(lastActiveRigId);
|
||||
updateRigIdentitySummary(lastActiveRigId);
|
||||
window.trxUi?.setActiveRig(lastActiveRigId);
|
||||
if (rigListChanged) {
|
||||
if (typeof setSchedulerRig === "function") setSchedulerRig(lastActiveRigId);
|
||||
@@ -3881,12 +3898,16 @@ function scheduleUiFrameJob(key, job) {
|
||||
|
||||
window.trxScheduleUiFrameJob = scheduleUiFrameJob;
|
||||
|
||||
async function postPath(path) {
|
||||
async function postPath(path, options = {}) {
|
||||
if (rigSwitchInProgress && !options.allowDuringRigSwitch) {
|
||||
throw new Error("Wait for the rig switch to finish");
|
||||
}
|
||||
const targetRigId = options.remote === undefined ? lastActiveRigId : options.remote;
|
||||
// Auto-append remote so each tab targets its own rig.
|
||||
// Skip when the caller already included remote (e.g. /select_rig).
|
||||
if (lastActiveRigId && !path.includes("remote=")) {
|
||||
if (targetRigId && !path.includes("remote=")) {
|
||||
const sep = path.includes("?") ? "&" : "?";
|
||||
path = `${path}${sep}remote=${encodeURIComponent(lastActiveRigId)}`;
|
||||
path = `${path}${sep}remote=${encodeURIComponent(targetRigId)}`;
|
||||
}
|
||||
const resp = await fetch(path, { method: "POST" });
|
||||
if (authEnabled && resp.status === 401) {
|
||||
@@ -3931,36 +3952,45 @@ async function switchRigFromSelect(selectEl) {
|
||||
return;
|
||||
}
|
||||
const prevRig = lastActiveRigId;
|
||||
lastActiveRigId = selectEl.value;
|
||||
if (prevRig && prevRig !== lastActiveRigId) {
|
||||
resetDecoderStateOnRigSwitch();
|
||||
}
|
||||
updateRigSubtitle(lastActiveRigId);
|
||||
if (typeof setSchedulerRig === "function") setSchedulerRig(lastActiveRigId);
|
||||
if (typeof setBackgroundDecodeRig === "function") setBackgroundDecodeRig(lastActiveRigId);
|
||||
if (typeof bmFetch === "function") bmFetch(document.getElementById("bm-category-filter")?.value || "");
|
||||
window.trx.modules.map?.syncAprsReceiverMarker();
|
||||
// Switch this session's rig and reconnect SSE to the new rig's
|
||||
// state channel.
|
||||
const nextRig = selectEl.value;
|
||||
if (nextRig === prevRig || rigSwitchInProgress) return;
|
||||
rigSwitchInProgress = true;
|
||||
setControlPending(selectEl, true);
|
||||
selectEl.closest(".header-rig-switch")?.classList.add("is-switching");
|
||||
updateRigIdentitySummary(nextRig, true);
|
||||
showHint(`Switching to ${lastRigDisplayNames[nextRig] || nextRig}…`);
|
||||
try {
|
||||
const sidParam = sseSessionId ? `&session_id=${encodeURIComponent(sseSessionId)}` : "";
|
||||
await postPath(`/select_rig?remote=${encodeURIComponent(selectEl.value)}${sidParam}`);
|
||||
await postPath(`/select_rig?remote=${encodeURIComponent(nextRig)}${sidParam}`, { allowDuringRigSwitch: true, remote: null });
|
||||
lastActiveRigId = nextRig;
|
||||
resetDecoderStateOnRigSwitch();
|
||||
updateRigSubtitle(lastActiveRigId);
|
||||
updateRigIdentitySummary(lastActiveRigId);
|
||||
window.trxUi?.setActiveRig(lastActiveRigId);
|
||||
if (typeof setSchedulerRig === "function") setSchedulerRig(lastActiveRigId);
|
||||
if (typeof setBackgroundDecodeRig === "function") setBackgroundDecodeRig(lastActiveRigId);
|
||||
if (typeof bmFetch === "function") bmFetch(document.getElementById("bm-category-filter")?.value || "");
|
||||
window.trx.modules.map?.syncAprsReceiverMarker();
|
||||
connect();
|
||||
stopSpectrumStreaming();
|
||||
startSpectrumStreaming();
|
||||
stopMeterStreaming();
|
||||
startMeterStreaming();
|
||||
if (rxActive) {
|
||||
stopRxAudio();
|
||||
startRxAudio();
|
||||
}
|
||||
showHint(`Rig: ${lastRigDisplayNames[lastActiveRigId] || lastActiveRigId}`, 1500);
|
||||
} catch (err) {
|
||||
console.error("select_rig failed:", err);
|
||||
selectEl.value = prevRig || "";
|
||||
updateRigIdentitySummary(prevRig);
|
||||
window.trxUi?.notify("Rig could not be switched", { kind: "error" });
|
||||
} finally {
|
||||
rigSwitchInProgress = false;
|
||||
setControlPending(selectEl, false);
|
||||
selectEl.closest(".header-rig-switch")?.classList.remove("is-switching");
|
||||
}
|
||||
// Reconnect spectrum SSE to the new rig's spectrum channel.
|
||||
stopSpectrumStreaming();
|
||||
startSpectrumStreaming();
|
||||
// Reconnect meter SSE to the new rig's meter channel.
|
||||
stopMeterStreaming();
|
||||
startMeterStreaming();
|
||||
// Reconnect audio to the new rig if audio is active.
|
||||
if (rxActive) {
|
||||
stopRxAudio();
|
||||
startRxAudio();
|
||||
}
|
||||
showHint(`Rig: ${lastActiveRigId}`, 1500);
|
||||
}
|
||||
|
||||
if (headerRigSwitchSelect) {
|
||||
|
||||
@@ -85,6 +85,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<button id="header-rec-btn" class="header-bar-btn header-rec-btn" type="button" aria-label="Toggle recording" title="Toggle recording">REC</button>
|
||||
<div class="header-rig-switch">
|
||||
<select id="header-rig-switch-select" aria-label="Select active rig"></select>
|
||||
<span id="header-rig-summary" class="header-rig-summary" aria-live="polite"></span>
|
||||
</div>
|
||||
<div class="header-style-pick">
|
||||
<select id="header-style-pick-select" aria-label="Select UI style">
|
||||
|
||||
@@ -1407,6 +1407,33 @@ small { color: var(--text-muted); }
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
position: relative;
|
||||
}
|
||||
.header-rig-summary {
|
||||
position: absolute;
|
||||
top: calc(100% + 0.3rem);
|
||||
left: 0;
|
||||
z-index: 20;
|
||||
width: max-content;
|
||||
max-width: min(28rem, calc(100vw - 2rem));
|
||||
padding: 0.25rem 0.45rem;
|
||||
border: 1px solid color-mix(in srgb, var(--border-light) 75%, transparent);
|
||||
border-radius: var(--radius-sm);
|
||||
background: color-mix(in srgb, var(--card-bg) 96%, transparent);
|
||||
color: var(--text-muted);
|
||||
box-shadow: 0 7px 20px color-mix(in srgb, #000 24%, transparent);
|
||||
font-size: var(--fs-xs);
|
||||
line-height: 1.35;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
transition: opacity var(--dur-fast) var(--ease-out), visibility var(--dur-fast);
|
||||
}
|
||||
.header-rig-switch:hover .header-rig-summary,
|
||||
.header-rig-switch:focus-within .header-rig-summary,
|
||||
.header-rig-switch.is-switching .header-rig-summary {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
.header-rig-switch select {
|
||||
min-width: 8rem;
|
||||
|
||||
Reference in New Issue
Block a user