[feat](trx-frontend-http): put the tuned frequency in the address bar
CI / frontend (pull_request) Successful in 4m3s
CI / reuse (pull_request) Successful in 2s
CI / lint (push) Successful in 2m16s
CI / lint (pull_request) Successful in 2m16s
CI / test (pull_request) Successful in 8m11s
CI / test (push) Successful in 7m20s
CI / frontend (push) Successful in 3m10s
CI / reuse (push) Successful in 2s
CI / frontend (pull_request) Successful in 4m3s
CI / reuse (pull_request) Successful in 2s
CI / lint (push) Successful in 2m16s
CI / lint (pull_request) Successful in 2m16s
CI / test (pull_request) Successful in 8m11s
CI / test (push) Successful in 7m20s
CI / frontend (push) Successful in 3m10s
CI / reuse (push) Successful in 2s
A receiver spreads by being linked to, and there was nothing to link to: the routes carried the tab and nothing else, so "listen to this" could only ever mean a screenshot and a frequency typed out in a message. The query string now carries the dial -- rig, frequency, mode and bandwidth -- in both directions. Opening a link selects the rig, sets the mode, tunes, then applies the bandwidth: a mode change brings its own default bandwidth with it, so an explicit bw has to land after it. Frequencies are read the way someone writes them by hand (7074k, 14.074M) and written back as whole Hz, so what comes out of the address bar is the same link in canonical form. After that the address bar keeps up with the dial, which is what makes it copyable at any moment rather than only at load. It is rewritten with replaceState -- tuning is not navigation, and a swept dial would otherwise bury the back button. A link button in the top bar copies the current link; it folds into the overflow menu when the bar is tight. Applying a link changes the radio, so an rx session says so instead of failing control calls one at a time. A tab listening to a virtual channel leaves the address alone rather than publishing a frequency the rig is not on, and bw is skipped in both directions on rigs without filter control, which would only refuse it. The fixture pinned every state frame to 100 MHz plus jitter to keep frames distinct, so no test could observe tuning at all. The jitter moves to the S-meter and the fixture echoes set_freq/set_mode/ set_bandwidth, as it already did for squelch. Signed-off-by: Stan Grams <sjg@haxx.space>
This commit was merged in pull request #42.
This commit is contained in:
@@ -747,7 +747,12 @@ function elementById(id) {
|
||||
api.applyLayout(savedLayoutName(), { persist: false });
|
||||
}
|
||||
}
|
||||
const overflowOrder = [".operator-layout-picker", ".header-style-pick", "#theme-toggle"];
|
||||
const overflowOrder = [
|
||||
".operator-layout-picker",
|
||||
".header-style-pick",
|
||||
"#header-share-btn",
|
||||
"#theme-toggle"
|
||||
];
|
||||
function anchorMenu(button, menu) {
|
||||
const rect = button.getBoundingClientRect();
|
||||
if (menu.parentElement !== document.body) document.body.appendChild(menu);
|
||||
@@ -1555,6 +1560,44 @@ function updateTabHistory(name, replace = false) {
|
||||
if (replace) window.history.replaceState({}, "", nextUrl);
|
||||
else window.history.pushState({}, "", nextUrl);
|
||||
}
|
||||
var FREQ_MULTIPLIER = { k: 1e3, m: 1e6, g: 1e9 };
|
||||
function parseFrequencyParam(raw) {
|
||||
if (typeof raw !== "string") return null;
|
||||
const text = raw.trim().toLowerCase().replace(/hz$/, "").trim();
|
||||
const match = /^(\d+(?:\.\d+)?)\s*([kmg]?)$/.exec(text);
|
||||
if (!match) return null;
|
||||
const value = Number(match[1]) * (FREQ_MULTIPLIER[match[2] ?? ""] ?? 1);
|
||||
if (!Number.isFinite(value) || value <= 0) return null;
|
||||
return Math.round(value);
|
||||
}
|
||||
function parseModeParam(raw) {
|
||||
const mode = typeof raw === "string" ? raw.trim().toUpperCase() : "";
|
||||
return /^[A-Z]{2,4}$/.test(mode) ? mode : null;
|
||||
}
|
||||
function parseTuneLink(search) {
|
||||
const params = new URLSearchParams(search);
|
||||
const rig = (params.get("rig") || "").trim();
|
||||
return {
|
||||
rig: rig || null,
|
||||
freqHz: parseFrequencyParam(params.get("f")),
|
||||
mode: parseModeParam(params.get("mode")),
|
||||
bandwidthHz: parseFrequencyParam(params.get("bw"))
|
||||
};
|
||||
}
|
||||
function tuneLinkSearch(search, link) {
|
||||
const params = new URLSearchParams(search);
|
||||
const set = (key, value) => {
|
||||
if (value == null || value === "") params.delete(key);
|
||||
else params.set(key, value);
|
||||
};
|
||||
const hz = (value) => typeof value === "number" && Number.isFinite(value) && value > 0 ? String(Math.round(value)) : null;
|
||||
set("rig", link.rig);
|
||||
set("f", hz(link.freqHz));
|
||||
set("mode", parseModeParam(link.mode));
|
||||
set("bw", hz(link.bandwidthHz));
|
||||
const text = params.toString();
|
||||
return text ? `?${text}` : "";
|
||||
}
|
||||
|
||||
// src/features/radio/auto-bandwidth.ts
|
||||
function clampPercent(value) {
|
||||
@@ -1978,6 +2021,7 @@ function applyAuthRestrictions() {
|
||||
function applyCapabilities(caps) {
|
||||
if (!caps) return;
|
||||
lastHasTx = !!caps.tx;
|
||||
bandwidthControlSupported = !!caps.filter_controls;
|
||||
if (signalVisualBlockEl) signalVisualBlockEl.style.display = "";
|
||||
const pttBtn2 = document.getElementById("ptt-btn");
|
||||
const txPowerCol = document.getElementById("tx-power-col");
|
||||
@@ -2325,6 +2369,7 @@ var jogAngle = 0;
|
||||
var lastClientCount = null;
|
||||
var lastLocked = false;
|
||||
var sdrSquelchSupported = false;
|
||||
var bandwidthControlSupported = false;
|
||||
var previousTuneState = null;
|
||||
function savePreviousTuneState() {
|
||||
previousTuneState = {
|
||||
@@ -4410,6 +4455,85 @@ window.buildAisVesselUrl = function(mmsi) {
|
||||
if (!aisVesselUrlBase || !isFiniteNumber(Number(mmsi))) return null;
|
||||
return `${aisVesselUrlBase}${String(mmsi)}`;
|
||||
};
|
||||
var incomingTuneLink = parseTuneLink(window.location.search);
|
||||
var tuneLinkPhase = "pending";
|
||||
var tuneLinkSyncTimer = null;
|
||||
function currentTuneLink() {
|
||||
const bandwidthHz = bandwidthControlSupported && isFiniteNumber(currentBandwidthHz) && currentBandwidthHz > 0 ? currentBandwidthHz : null;
|
||||
return {
|
||||
rig: lastActiveRigId || null,
|
||||
freqHz: isFiniteNumber(lastFreqHz) ? lastFreqHz : null,
|
||||
mode: lastModeName || null,
|
||||
bandwidthHz
|
||||
};
|
||||
}
|
||||
function tuneLinkUrl() {
|
||||
const search = tuneLinkSearch(window.location.search, currentTuneLink());
|
||||
return `${window.location.origin}${window.location.pathname}${search}${window.location.hash}`;
|
||||
}
|
||||
function scheduleTuneLinkSync() {
|
||||
if (tuneLinkPhase !== "live") return;
|
||||
if (window.trx?.modules.vchan?.isOnVirtual() === true) return;
|
||||
if (tuneLinkSyncTimer != null) return;
|
||||
tuneLinkSyncTimer = window.setTimeout(() => {
|
||||
tuneLinkSyncTimer = null;
|
||||
const search = tuneLinkSearch(window.location.search, currentTuneLink());
|
||||
if (search === window.location.search) return;
|
||||
window.history.replaceState({}, "", `${window.location.pathname}${search}${window.location.hash}`);
|
||||
}, 500);
|
||||
}
|
||||
async function applyTuneLink(link) {
|
||||
const wanted = link.rig || link.mode || link.freqHz != null || link.bandwidthHz != null;
|
||||
if (!wanted) return;
|
||||
if (authRole === "rx") {
|
||||
showHint("Read-only session — link not applied", 2500);
|
||||
return;
|
||||
}
|
||||
if (link.rig && headerRigSwitchSelect && link.rig !== lastActiveRigId && lastRigIds.includes(link.rig)) {
|
||||
headerRigSwitchSelect.value = link.rig;
|
||||
await switchRigFromSelect(headerRigSwitchSelect);
|
||||
}
|
||||
if (link.mode && link.mode !== lastModeName && modeEl) {
|
||||
const known = Array.from(modeEl.options).some((option) => option.value === link.mode);
|
||||
if (known) {
|
||||
modeEl.value = link.mode;
|
||||
await applyModeFromPicker();
|
||||
} else {
|
||||
showHint(`Rig has no ${link.mode} mode`, 2500);
|
||||
}
|
||||
}
|
||||
if (link.freqHz != null) {
|
||||
try {
|
||||
setRigFrequency(link.freqHz);
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
if (link.bandwidthHz != null && bandwidthControlSupported && spectrumBwInput) {
|
||||
spectrumBwInput.value = String(link.bandwidthHz / 1e3);
|
||||
await applyBandwidthFromInput();
|
||||
}
|
||||
}
|
||||
function followTuneLinkOnce() {
|
||||
if (tuneLinkPhase !== "pending") return;
|
||||
tuneLinkPhase = "applying";
|
||||
void applyTuneLink(incomingTuneLink).finally(() => {
|
||||
tuneLinkPhase = "live";
|
||||
scheduleTuneLinkSync();
|
||||
});
|
||||
}
|
||||
async function copyTuneLink() {
|
||||
const url = tuneLinkUrl();
|
||||
try {
|
||||
await navigator.clipboard.writeText(url);
|
||||
showHint("Link copied", 1500);
|
||||
} catch {
|
||||
window.trxUi?.notify("Clipboard unavailable — the address bar holds the link", { kind: "error" });
|
||||
}
|
||||
}
|
||||
var shareLinkBtn = document.getElementById("header-share-btn");
|
||||
if (shareLinkBtn) shareLinkBtn.addEventListener("click", () => {
|
||||
void copyTuneLink();
|
||||
});
|
||||
function render(update) {
|
||||
if (!update) return;
|
||||
if (update.server_version) serverVersion = update.server_version;
|
||||
@@ -4922,6 +5046,8 @@ function render(update) {
|
||||
swrBar.style.width = "0%";
|
||||
swrValue.textContent = "SWR --";
|
||||
}
|
||||
if (update.status) followTuneLinkOnce();
|
||||
scheduleTuneLinkSync();
|
||||
}
|
||||
function scheduleReconnect(delayMs = 1e3) {
|
||||
if (reconnectTimer) return;
|
||||
|
||||
Reference in New Issue
Block a user