Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53736ef750 | ||
|
|
6566eae2c7 | ||
|
|
f3eeddff7c | ||
|
|
1540ca5b4e | ||
|
|
e449704fd2 | ||
|
|
af74430551 | ||
|
|
d07dbdc645 | ||
|
|
d07a627508 | ||
|
|
9fbf90ee91 | ||
|
|
d393b3cefc | ||
|
|
35a492ec95 |
@@ -23,6 +23,7 @@ window.onDecoderRegistryReady = function (fn) {
|
|||||||
for (const fn of _decoderRegistryReadyCallbacks) fn();
|
for (const fn of _decoderRegistryReadyCallbacks) fn();
|
||||||
_decoderRegistryReadyCallbacks.length = 0;
|
_decoderRegistryReadyCallbacks.length = 0;
|
||||||
hideUnsupportedDecoderTabs();
|
hideUnsupportedDecoderTabs();
|
||||||
|
refreshOperatorLayoutCapabilities();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to fetch decoder registry:", e);
|
console.error("Failed to fetch decoder registry:", e);
|
||||||
@@ -333,8 +334,19 @@ function applyCapabilities(caps) {
|
|||||||
const txAudioBtn = document.getElementById("tx-audio-btn");
|
const txAudioBtn = document.getElementById("tx-audio-btn");
|
||||||
const txVolSlider = document.getElementById("tx-vol");
|
const txVolSlider = document.getElementById("tx-vol");
|
||||||
const txVolControl = txVolSlider ? txVolSlider.closest(".vol-label") : null;
|
const txVolControl = txVolSlider ? txVolSlider.closest(".vol-label") : null;
|
||||||
if (txPowerCol) txPowerCol.style.display = caps.tx ? "" : "none";
|
const hasPowerControl = !caps.filter_controls;
|
||||||
|
if (txPowerCol) {
|
||||||
|
txPowerCol.style.display = (caps.tx || hasPowerControl || caps.lockable) ? "" : "none";
|
||||||
|
const label = txPowerCol.querySelector(".label span");
|
||||||
|
if (label) {
|
||||||
|
label.textContent = caps.tx && hasPowerControl ? "Transmit / Power"
|
||||||
|
: caps.tx ? "Transmit / Tuning"
|
||||||
|
: hasPowerControl ? "Power / Tuning" : "Tuning";
|
||||||
|
}
|
||||||
|
}
|
||||||
if (pttBtn) pttBtn.style.display = caps.tx ? "" : "none";
|
if (pttBtn) pttBtn.style.display = caps.tx ? "" : "none";
|
||||||
|
if (powerBtn) powerBtn.style.display = hasPowerControl ? "" : "none";
|
||||||
|
if (lockBtn) lockBtn.style.display = caps.lockable ? "" : "none";
|
||||||
if (txMetersRow) txMetersRow.style.display = caps.tx ? "" : "none";
|
if (txMetersRow) txMetersRow.style.display = caps.tx ? "" : "none";
|
||||||
if (txAudioBtn) txAudioBtn.style.display = caps.tx ? "" : "none";
|
if (txAudioBtn) txAudioBtn.style.display = caps.tx ? "" : "none";
|
||||||
if (txVolControl) txVolControl.style.display = caps.tx ? "" : "none";
|
if (txVolControl) txVolControl.style.display = caps.tx ? "" : "none";
|
||||||
@@ -344,7 +356,7 @@ function applyCapabilities(caps) {
|
|||||||
|
|
||||||
// TX limit row
|
// TX limit row
|
||||||
const txLimitRow = document.getElementById("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
|
// VFO row
|
||||||
const vfoRow = document.getElementById("vfo-row");
|
const vfoRow = document.getElementById("vfo-row");
|
||||||
@@ -436,6 +448,7 @@ const signalSplitValueEl = document.getElementById("signal-split-value");
|
|||||||
const overviewPeakHoldEl = document.getElementById("overview-peak-hold");
|
const overviewPeakHoldEl = document.getElementById("overview-peak-hold");
|
||||||
const themeToggleBtn = document.getElementById("theme-toggle");
|
const themeToggleBtn = document.getElementById("theme-toggle");
|
||||||
const headerRigSwitchSelect = document.getElementById("header-rig-switch-select");
|
const headerRigSwitchSelect = document.getElementById("header-rig-switch-select");
|
||||||
|
const headerRigSummary = document.getElementById("header-rig-summary");
|
||||||
const headerStylePickSelect = document.getElementById("header-style-pick-select");
|
const headerStylePickSelect = document.getElementById("header-style-pick-select");
|
||||||
const rdsPsOverlay = document.getElementById("rds-ps-overlay");
|
const rdsPsOverlay = document.getElementById("rds-ps-overlay");
|
||||||
const tabMainEl = document.getElementById("tab-main");
|
const tabMainEl = document.getElementById("tab-main");
|
||||||
@@ -895,6 +908,7 @@ async function restorePreviousTuneState() {
|
|||||||
let lastRigIds = [];
|
let lastRigIds = [];
|
||||||
let lastRigDisplayNames = {};
|
let lastRigDisplayNames = {};
|
||||||
let lastActiveRigId = null;
|
let lastActiveRigId = null;
|
||||||
|
let rigSwitchInProgress = false;
|
||||||
let lastCityLabel = "";
|
let lastCityLabel = "";
|
||||||
let sseSessionId = null;
|
let sseSessionId = null;
|
||||||
const originalTitle = document.title;
|
const originalTitle = document.title;
|
||||||
@@ -1239,6 +1253,20 @@ function populateRigPicker(selectEl, rigIds, activeRigId, disabled) {
|
|||||||
selectEl.disabled = 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) {
|
function updateRigSubtitle(activeRigId) {
|
||||||
if (!rigSubtitle) return;
|
if (!rigSubtitle) return;
|
||||||
const name = (activeRigId && lastRigDisplayNames[activeRigId]) || activeRigId || "--";
|
const name = (activeRigId && lastRigDisplayNames[activeRigId]) || activeRigId || "--";
|
||||||
@@ -1275,6 +1303,8 @@ function applyRigList(activeRigId, rigIds, displayNames) {
|
|||||||
const disableSwitch = lastRigIds.length === 0 || !authRole || authRole === "rx";
|
const disableSwitch = lastRigIds.length === 0 || !authRole || authRole === "rx";
|
||||||
populateRigPicker(headerRigSwitchSelect, lastRigIds, lastActiveRigId, disableSwitch);
|
populateRigPicker(headerRigSwitchSelect, lastRigIds, lastActiveRigId, disableSwitch);
|
||||||
updateRigSubtitle(lastActiveRigId);
|
updateRigSubtitle(lastActiveRigId);
|
||||||
|
updateRigIdentitySummary(lastActiveRigId);
|
||||||
|
window.trxUi?.setActiveRig(lastActiveRigId);
|
||||||
if (rigListChanged) {
|
if (rigListChanged) {
|
||||||
if (typeof setSchedulerRig === "function") setSchedulerRig(lastActiveRigId);
|
if (typeof setSchedulerRig === "function") setSchedulerRig(lastActiveRigId);
|
||||||
if (typeof setBackgroundDecodeRig === "function") setBackgroundDecodeRig(lastActiveRigId);
|
if (typeof setBackgroundDecodeRig === "function") setBackgroundDecodeRig(lastActiveRigId);
|
||||||
@@ -1305,6 +1335,7 @@ async function refreshRigList() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
serverRigs = rigs;
|
serverRigs = rigs;
|
||||||
|
refreshOperatorLayoutCapabilities();
|
||||||
serverActiveRigId = data.active_remote || null;
|
serverActiveRigId = data.active_remote || null;
|
||||||
applyRigList(data.active_remote, rigIds, displayNames);
|
applyRigList(data.active_remote, rigIds, displayNames);
|
||||||
window.trx.modules.map?.syncAprsReceiverMarker();
|
window.trx.modules.map?.syncAprsReceiverMarker();
|
||||||
@@ -1313,6 +1344,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) {
|
function showHint(msg, duration) {
|
||||||
powerHint.textContent = msg;
|
powerHint.textContent = msg;
|
||||||
if (hintTimer) clearTimeout(hintTimer);
|
if (hintTimer) clearTimeout(hintTimer);
|
||||||
@@ -3859,12 +3903,16 @@ function scheduleUiFrameJob(key, job) {
|
|||||||
|
|
||||||
window.trxScheduleUiFrameJob = scheduleUiFrameJob;
|
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.
|
// Auto-append remote so each tab targets its own rig.
|
||||||
// Skip when the caller already included remote (e.g. /select_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("?") ? "&" : "?";
|
const sep = path.includes("?") ? "&" : "?";
|
||||||
path = `${path}${sep}remote=${encodeURIComponent(lastActiveRigId)}`;
|
path = `${path}${sep}remote=${encodeURIComponent(targetRigId)}`;
|
||||||
}
|
}
|
||||||
const resp = await fetch(path, { method: "POST" });
|
const resp = await fetch(path, { method: "POST" });
|
||||||
if (authEnabled && resp.status === 401) {
|
if (authEnabled && resp.status === 401) {
|
||||||
@@ -3909,36 +3957,45 @@ async function switchRigFromSelect(selectEl) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const prevRig = lastActiveRigId;
|
const prevRig = lastActiveRigId;
|
||||||
lastActiveRigId = selectEl.value;
|
const nextRig = selectEl.value;
|
||||||
if (prevRig && prevRig !== lastActiveRigId) {
|
if (nextRig === prevRig || rigSwitchInProgress) return;
|
||||||
resetDecoderStateOnRigSwitch();
|
rigSwitchInProgress = true;
|
||||||
}
|
setControlPending(selectEl, true);
|
||||||
updateRigSubtitle(lastActiveRigId);
|
selectEl.closest(".header-rig-switch")?.classList.add("is-switching");
|
||||||
if (typeof setSchedulerRig === "function") setSchedulerRig(lastActiveRigId);
|
updateRigIdentitySummary(nextRig, true);
|
||||||
if (typeof setBackgroundDecodeRig === "function") setBackgroundDecodeRig(lastActiveRigId);
|
showHint(`Switching to ${lastRigDisplayNames[nextRig] || nextRig}…`);
|
||||||
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.
|
|
||||||
try {
|
try {
|
||||||
const sidParam = sseSessionId ? `&session_id=${encodeURIComponent(sseSessionId)}` : "";
|
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();
|
connect();
|
||||||
|
stopSpectrumStreaming();
|
||||||
|
startSpectrumStreaming();
|
||||||
|
stopMeterStreaming();
|
||||||
|
startMeterStreaming();
|
||||||
|
if (rxActive) {
|
||||||
|
stopRxAudio();
|
||||||
|
startRxAudio();
|
||||||
|
}
|
||||||
|
showHint(`Rig: ${lastRigDisplayNames[lastActiveRigId] || lastActiveRigId}`, 1500);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("select_rig failed:", 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) {
|
if (headerRigSwitchSelect) {
|
||||||
@@ -4602,6 +4659,7 @@ function _initMapWhenReady() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function navigateToTab(name, options = {}) {
|
function navigateToTab(name, options = {}) {
|
||||||
|
window.trxUi?.closeMobileOverlays?.();
|
||||||
const { updateHistory = true, replaceHistory = false } = options;
|
const { updateHistory = true, replaceHistory = false } = options;
|
||||||
if (authEnabled && !authRole && name !== "main") {
|
if (authEnabled && !authRole && name !== "main") {
|
||||||
showAuthGate(false);
|
showAuthGate(false);
|
||||||
|
|||||||
@@ -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>
|
<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">
|
<div class="header-rig-switch">
|
||||||
<select id="header-rig-switch-select" aria-label="Select active rig"></select>
|
<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>
|
||||||
<div class="header-style-pick">
|
<div class="header-style-pick">
|
||||||
<select id="header-style-pick-select" aria-label="Select UI style">
|
<select id="header-style-pick-select" aria-label="Select UI style">
|
||||||
|
|||||||
@@ -1231,7 +1231,7 @@ small { color: var(--text-muted); }
|
|||||||
}
|
}
|
||||||
.top-bar-actions {
|
.top-bar-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
gap: 0.45rem 0.6rem;
|
gap: 0.45rem 0.6rem;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -1404,10 +1404,21 @@ small { color: var(--text-muted); }
|
|||||||
padding: 0.9rem 0.2rem 0;
|
padding: 0.9rem 0.2rem 0;
|
||||||
}
|
}
|
||||||
.header-rig-switch {
|
.header-rig-switch {
|
||||||
display: flex;
|
display: grid;
|
||||||
align-items: center;
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
align-items: start;
|
||||||
gap: 0.35rem;
|
gap: 0.35rem;
|
||||||
}
|
}
|
||||||
|
.header-rig-summary {
|
||||||
|
display: block;
|
||||||
|
max-width: 20rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: var(--fs-xs);
|
||||||
|
line-height: 1.35;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
.header-rig-switch select {
|
.header-rig-switch select {
|
||||||
min-width: 8rem;
|
min-width: 8rem;
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
@@ -1417,6 +1428,7 @@ small { color: var(--text-muted); }
|
|||||||
background: var(--input-bg);
|
background: var(--input-bg);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
|
align-self: start;
|
||||||
}
|
}
|
||||||
.header-rig-switch button {
|
.header-rig-switch button {
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
@@ -2937,9 +2949,10 @@ button.is-active {
|
|||||||
#ptt-btn.is-active { background: var(--accent-red) !important; border-color: var(--accent-red) !important; color: white !important; }
|
#ptt-btn.is-active { background: var(--accent-red) !important; border-color: var(--accent-red) !important; color: white !important; }
|
||||||
.is-busy { cursor: progress !important; opacity: 0.68; }
|
.is-busy { cursor: progress !important; opacity: 0.68; }
|
||||||
.operator-layout-picker {
|
.operator-layout-picker {
|
||||||
display: inline-flex;
|
display: inline-grid;
|
||||||
|
grid-template-columns: auto auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 2rem;
|
min-height: 2rem;
|
||||||
padding-left: 0.6rem;
|
padding-left: 0.6rem;
|
||||||
border: 1px solid var(--border-light);
|
border: 1px solid var(--border-light);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
@@ -2949,7 +2962,7 @@ button.is-active {
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
overflow: hidden;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
.operator-layout-picker::before { content: "Layout"; }
|
.operator-layout-picker::before { content: "Layout"; }
|
||||||
.operator-layout-picker select {
|
.operator-layout-picker select {
|
||||||
@@ -2967,6 +2980,17 @@ button.is-active {
|
|||||||
}
|
}
|
||||||
.operator-layout-picker:focus-within { border-color: var(--accent-green); box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent-green) 20%, transparent); }
|
.operator-layout-picker:focus-within { border-color: var(--accent-green); box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent-green) 20%, transparent); }
|
||||||
.operator-layout-picker select:focus-visible { outline: 0; }
|
.operator-layout-picker select:focus-visible { outline: 0; }
|
||||||
|
.operator-layout-description {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
max-width: 22rem;
|
||||||
|
padding: 0 0.55rem 0.35rem 0;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: var(--fs-xs);
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0;
|
||||||
|
line-height: 1.35;
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
.advanced-radio-controls {
|
.advanced-radio-controls {
|
||||||
margin-top: var(--space-3);
|
margin-top: var(--space-3);
|
||||||
border: 1px solid color-mix(in srgb, var(--border-light) 75%, transparent);
|
border: 1px solid color-mix(in srgb, var(--border-light) 75%, transparent);
|
||||||
@@ -3003,6 +3027,50 @@ button.is-active {
|
|||||||
.advanced-radio-body { display: grid; gap: var(--space-3); padding: var(--space-3); }
|
.advanced-radio-body { display: grid; gap: var(--space-3); padding: var(--space-3); }
|
||||||
.advanced-radio-body > * { margin: 0; }
|
.advanced-radio-body > * { margin: 0; }
|
||||||
body[data-operator-layout="digital"] .controls-col-wfm { display: none !important; }
|
body[data-operator-layout="digital"] .controls-col-wfm { display: none !important; }
|
||||||
|
body[data-operator-layout="broadcast"] #tx-power-col,
|
||||||
|
body[data-operator-layout="broadcast"] #tx-meters,
|
||||||
|
body[data-operator-layout="broadcast"] #tx-limit-row,
|
||||||
|
body[data-operator-layout="broadcast"] #vfo-row,
|
||||||
|
body[data-operator-layout="broadcast"] #sam-controls-col,
|
||||||
|
body[data-operator-layout="broadcast"] .advanced-radio-controls {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
body[data-operator-layout="broadcast"] .controls-row {
|
||||||
|
grid-template-columns: minmax(8rem, 0.65fr) auto minmax(20rem, 2fr);
|
||||||
|
}
|
||||||
|
body[data-operator-layout="broadcast"] #wfm-controls-col,
|
||||||
|
body[data-operator-layout="broadcast"] #audio-row,
|
||||||
|
body[data-operator-layout="broadcast"] #spectrum-bw-row {
|
||||||
|
border-color: color-mix(in srgb, var(--accent-green) 42%, var(--border-light));
|
||||||
|
background: color-mix(in srgb, var(--accent-green) 7%, transparent);
|
||||||
|
}
|
||||||
|
body[data-operator-layout="broadcast"] #wfm-controls-col {
|
||||||
|
padding: var(--space-2) var(--space-3);
|
||||||
|
border: 1px solid color-mix(in srgb, var(--accent-green) 42%, var(--border-light));
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
body[data-operator-layout="broadcast"] #audio-row {
|
||||||
|
padding: var(--space-3);
|
||||||
|
border: 1px solid color-mix(in srgb, var(--accent-green) 42%, var(--border-light));
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
body[data-operator-layout="broadcast"] #spectrum-bw-row {
|
||||||
|
padding: var(--space-2);
|
||||||
|
border: 1px solid color-mix(in srgb, var(--accent-green) 42%, var(--border-light));
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
body[data-operator-layout="broadcast"] #ais-bar-overlay,
|
||||||
|
body[data-operator-layout="broadcast"] #vdes-bar-overlay,
|
||||||
|
body[data-operator-layout="broadcast"] #ft8-bar-overlay,
|
||||||
|
body[data-operator-layout="broadcast"] #aprs-bar-overlay,
|
||||||
|
body[data-operator-layout="broadcast"] #hf-aprs-bar-overlay,
|
||||||
|
body[data-operator-layout="broadcast"] #cw-bar-overlay {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
body[data-operator-layout="broadcast"] .controls-row { grid-template-columns: 1fr auto; }
|
||||||
|
body[data-operator-layout="broadcast"] #wfm-controls-col { grid-column: 1 / -1; }
|
||||||
|
}
|
||||||
.mobile-more-btn, .mobile-more-menu, .decoder-tab-select { display: none; }
|
.mobile-more-btn, .mobile-more-menu, .decoder-tab-select { display: none; }
|
||||||
.mobile-more-menu {
|
.mobile-more-menu {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { readFile } from "node:fs/promises";
|
||||||
|
import vm from "node:vm";
|
||||||
|
|
||||||
|
class ClassList {
|
||||||
|
constructor() { this.values = new Set(); }
|
||||||
|
add(...names) { names.forEach(name => this.values.add(name)); }
|
||||||
|
remove(...names) { names.forEach(name => this.values.delete(name)); }
|
||||||
|
contains(name) { return this.values.has(name); }
|
||||||
|
toggle(name, force) {
|
||||||
|
const enabled = force === undefined ? !this.contains(name) : force;
|
||||||
|
if (enabled) this.add(name); else this.remove(name);
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Element {
|
||||||
|
constructor(tagName, document) {
|
||||||
|
this.tagName = tagName.toUpperCase();
|
||||||
|
this.ownerDocument = document;
|
||||||
|
this.children = [];
|
||||||
|
this.dataset = {};
|
||||||
|
this.attributes = new Map();
|
||||||
|
this.classList = new ClassList();
|
||||||
|
this.listeners = new Map();
|
||||||
|
this.style = {};
|
||||||
|
this.options = [];
|
||||||
|
this.value = "";
|
||||||
|
this.textContent = "";
|
||||||
|
}
|
||||||
|
set id(value) { this._id = value; if (value) this.ownerDocument.elements.set(value, this); }
|
||||||
|
get id() { return this._id || ""; }
|
||||||
|
set className(value) { this.classList = new ClassList(); value.split(/\s+/).filter(Boolean).forEach(name => this.classList.add(name)); }
|
||||||
|
set innerHTML(value) {
|
||||||
|
this._innerHTML = value;
|
||||||
|
if (value.includes('value="confirm"')) {
|
||||||
|
const title = new Element("h2", this.ownerDocument); title.id = "ui-confirm-title";
|
||||||
|
const message = new Element("p", this.ownerDocument); message.id = "ui-confirm-message";
|
||||||
|
const confirm = new Element("button", this.ownerDocument); confirm.value = "confirm";
|
||||||
|
this.append(title, message, confirm);
|
||||||
|
this._confirmButton = confirm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
get innerHTML() { return this._innerHTML || ""; }
|
||||||
|
appendChild(child) { this.children.push(child); child.parentElement = this; return child; }
|
||||||
|
append(...children) { children.forEach(child => this.appendChild(child)); }
|
||||||
|
insertBefore(child) { return this.appendChild(child); }
|
||||||
|
remove() { if (this.parentElement) this.parentElement.children = this.parentElement.children.filter(child => child !== this); }
|
||||||
|
replaceChildren(...children) { this.children = []; this.options = []; this.append(...children); }
|
||||||
|
setAttribute(name, value) { this.attributes.set(name, String(value)); }
|
||||||
|
getAttribute(name) { return this.attributes.get(name); }
|
||||||
|
addEventListener(type, listener) { this.listeners.set(type, listener); }
|
||||||
|
dispatch(type, event = {}) { this.listeners.get(type)?.({ target: this, preventDefault() {}, ...event }); }
|
||||||
|
querySelector(selector) {
|
||||||
|
if (selector === '[value="confirm"]') return this._confirmButton || null;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
querySelectorAll(selector) {
|
||||||
|
if (selector === ".tab[data-tab]") return this.children.filter(child => child.dataset.tab);
|
||||||
|
if (selector === ".sub-tab[data-subtab]") return this.children.filter(child => child.dataset.subtab);
|
||||||
|
if (selector === '[role="tab"]') return this.children.filter(child => child.getAttribute("role") === "tab");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
add(option) { this.options.push(option); if (!this.value) this.value = option.value; }
|
||||||
|
showModal() { this.open = true; }
|
||||||
|
close(value) { this.returnValue = value; this.open = false; this.dispatch("close"); }
|
||||||
|
focus() { this.focused = true; }
|
||||||
|
click() { this.clicked = true; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class DocumentFixture {
|
||||||
|
constructor() {
|
||||||
|
this.readyState = "loading";
|
||||||
|
this.elements = new Map();
|
||||||
|
this.body = new Element("body", this);
|
||||||
|
}
|
||||||
|
createElement(tagName) { return new Element(tagName, this); }
|
||||||
|
getElementById(id) { return this.elements.get(id) || null; }
|
||||||
|
querySelector() { return null; }
|
||||||
|
querySelectorAll() { return []; }
|
||||||
|
addEventListener() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const document = new DocumentFixture();
|
||||||
|
const storage = new Map();
|
||||||
|
const localStorage = {
|
||||||
|
getItem(key) { return storage.has(key) ? storage.get(key) : null; },
|
||||||
|
setItem(key, value) { storage.set(key, String(value)); },
|
||||||
|
};
|
||||||
|
const window = { document, localStorage, addEventListener() {} };
|
||||||
|
const context = vm.createContext({
|
||||||
|
window, document, localStorage,
|
||||||
|
Option: class Option { constructor(label, value) { this.label = label; this.value = value; } },
|
||||||
|
MutationObserver: class MutationObserver { observe() {} },
|
||||||
|
requestAnimationFrame(callback) { callback(); },
|
||||||
|
setTimeout() { return 1; },
|
||||||
|
clearTimeout() {},
|
||||||
|
console,
|
||||||
|
});
|
||||||
|
|
||||||
|
const source = await readFile(new URL("../ui-core.js", import.meta.url), "utf8");
|
||||||
|
new vm.Script(source, { filename: "ui-core.js" }).runInContext(context);
|
||||||
|
const ui = window.trxUi;
|
||||||
|
|
||||||
|
const toast = ui.notify("Saved", { kind: "success" });
|
||||||
|
assert.equal(toast.getAttribute("role"), "status");
|
||||||
|
assert.equal(toast.classList.contains("toast-success"), true);
|
||||||
|
assert.equal(document.getElementById("toast-region").children.length, 1);
|
||||||
|
|
||||||
|
const confirmation = ui.confirm({ title: "Delete?", message: "Permanent", confirmLabel: "Delete" });
|
||||||
|
const dialog = document.getElementById("ui-confirm-dialog");
|
||||||
|
assert.equal(dialog.open, true);
|
||||||
|
assert.equal(document.getElementById("ui-confirm-title").textContent, "Delete?");
|
||||||
|
dialog.close("confirm");
|
||||||
|
assert.equal(await confirmation, true);
|
||||||
|
|
||||||
|
const tabBar = new Element("div", document);
|
||||||
|
const firstTab = new Element("button", document); firstTab.dataset.tab = "main"; firstTab.classList.add("active");
|
||||||
|
const secondTab = new Element("button", document); secondTab.dataset.tab = "map";
|
||||||
|
tabBar.append(firstTab, secondTab);
|
||||||
|
const mainPanel = new Element("section", document); mainPanel.id = "tab-main";
|
||||||
|
const mapPanel = new Element("section", document); mapPanel.id = "tab-map";
|
||||||
|
ui.prepareTabList(tabBar, "primary");
|
||||||
|
assert.equal(firstTab.getAttribute("aria-selected"), "true");
|
||||||
|
tabBar.dispatch("keydown", { target: firstTab, key: "ArrowRight" });
|
||||||
|
assert.equal(secondTab.focused, true);
|
||||||
|
assert.equal(secondTab.clicked, true);
|
||||||
|
|
||||||
|
localStorage.setItem("trxOperatorLayout:rig-a", "broadcast");
|
||||||
|
ui.setActiveRig("rig-a");
|
||||||
|
assert.equal(document.body.dataset.operatorLayout, "compact");
|
||||||
|
ui.setLayoutCapabilities({ broadcast: true });
|
||||||
|
ui.setActiveRig("rig-a");
|
||||||
|
assert.equal(document.body.dataset.operatorLayout, "broadcast");
|
||||||
|
ui.setActiveRig("rig-b");
|
||||||
|
ui.applyLayout("digital");
|
||||||
|
assert.equal(document.body.dataset.operatorLayout, "compact");
|
||||||
|
assert.equal(localStorage.getItem("trxOperatorLayout:rig-b"), "compact");
|
||||||
|
|
||||||
|
console.log("ui-core component tests passed");
|
||||||
@@ -126,18 +126,83 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const layouts = {
|
const layouts = {
|
||||||
compact: { label: "Compact", advanced: false, preferredTab: "main" },
|
compact: { label: "Compact", description: "Essential tuning and audio controls", advanced: false, preferredTab: "main" },
|
||||||
broadcast: { label: "Broadcast", advanced: false, preferredTab: "main" },
|
broadcast: { label: "Broadcast", description: "WFM, RDS, stereo and interference monitoring", unavailable: "Broadcast requires an enumerated WFM-capable receiver", advanced: false, preferredTab: "main", capability: "broadcast" },
|
||||||
digital: { label: "Digital", advanced: false, preferredTab: "digital-modes" },
|
digital: { label: "Digital", description: "Decoder status, messages and background decoding", unavailable: "Digital requires a compatible rig mode and an available decoder", advanced: false, preferredTab: "digital-modes", capability: "digital" },
|
||||||
full: { label: "Full controls", advanced: true, preferredTab: "main" },
|
full: { label: "Full controls", description: "Every control supported by the selected rig", advanced: true, preferredTab: "main" },
|
||||||
|
};
|
||||||
|
const layoutCapabilities = { broadcast: false, digital: false };
|
||||||
|
let activeRigId = null;
|
||||||
|
|
||||||
|
function layoutStorageKey() {
|
||||||
|
return activeRigId ? `trxOperatorLayout:${activeRigId}` : "trxOperatorLayout";
|
||||||
|
}
|
||||||
|
|
||||||
|
function savedLayoutName() {
|
||||||
|
return localStorage.getItem(layoutStorageKey()) || localStorage.getItem("trxOperatorLayout") || "compact";
|
||||||
|
}
|
||||||
|
|
||||||
|
function layoutAvailable(layout) {
|
||||||
|
return !layout.capability || layoutCapabilities[layout.capability] === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function unavailableLayoutMessage() {
|
||||||
|
const unavailable = Object.values(layouts).filter(layout => !layoutAvailable(layout) && layout.unavailable);
|
||||||
|
return unavailable.length ? `Unavailable: ${unavailable.map(layout => layout.unavailable).join("; ")}.` : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateLayoutDescription(layout) {
|
||||||
|
const description = document.getElementById("operator-layout-description");
|
||||||
|
if (!description) return;
|
||||||
|
description.textContent = [layout.description, unavailableLayoutMessage()].filter(Boolean).join(" · ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshLayoutOptions() {
|
||||||
|
const select = document.getElementById("operator-layout-select");
|
||||||
|
if (!select) return;
|
||||||
|
const previous = select.value || document.body.dataset.operatorLayout || "compact";
|
||||||
|
select.replaceChildren();
|
||||||
|
Object.entries(layouts).forEach(([value, layout]) => {
|
||||||
|
if (!layoutAvailable(layout)) return;
|
||||||
|
select.add(new Option(layout.label, value));
|
||||||
|
});
|
||||||
|
const available = Array.from(select.options).some(option => option.value === previous);
|
||||||
|
select.value = available ? previous : "compact";
|
||||||
|
if (!available && previous !== "compact") api.applyLayout("compact", { persist: false });
|
||||||
|
select.title = unavailableLayoutMessage();
|
||||||
|
updateLayoutDescription(layouts[select.value] || layouts.compact);
|
||||||
|
}
|
||||||
|
|
||||||
|
api.setLayoutCapabilities = function setLayoutCapabilities(capabilities = {}) {
|
||||||
|
Object.keys(layoutCapabilities).forEach((name) => {
|
||||||
|
if (name in capabilities) layoutCapabilities[name] = Boolean(capabilities[name]);
|
||||||
|
});
|
||||||
|
refreshLayoutOptions();
|
||||||
|
const select = document.getElementById("operator-layout-select");
|
||||||
|
const saved = savedLayoutName();
|
||||||
|
if (select && Array.from(select.options).some(option => option.value === saved)) {
|
||||||
|
select.value = saved;
|
||||||
|
api.applyLayout(saved, { persist: false });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
api.setActiveRig = function setActiveRig(rigId) {
|
||||||
|
activeRigId = typeof rigId === "string" && rigId ? rigId : null;
|
||||||
|
const saved = savedLayoutName();
|
||||||
|
const select = document.getElementById("operator-layout-select");
|
||||||
|
if (select) select.value = Array.from(select.options).some(option => option.value === saved) ? saved : "compact";
|
||||||
|
api.applyLayout(select?.value || saved, { persist: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
api.applyLayout = function applyLayout(name, options = {}) {
|
api.applyLayout = function applyLayout(name, options = {}) {
|
||||||
const layout = layouts[name] || layouts.compact;
|
const requestedLayout = layouts[name];
|
||||||
document.body.dataset.operatorLayout = name in layouts ? name : "compact";
|
const permittedName = requestedLayout && layoutAvailable(requestedLayout) ? name : "compact";
|
||||||
localStorage.setItem("trxOperatorLayout", document.body.dataset.operatorLayout);
|
const layout = layouts[permittedName];
|
||||||
|
document.body.dataset.operatorLayout = permittedName in layouts ? permittedName : "compact";
|
||||||
|
if (options.persist !== false) localStorage.setItem(layoutStorageKey(), document.body.dataset.operatorLayout);
|
||||||
const details = document.getElementById("advanced-radio-controls");
|
const details = document.getElementById("advanced-radio-controls");
|
||||||
if (details) details.open = layout.advanced;
|
if (details) details.open = layout.advanced;
|
||||||
|
updateLayoutDescription(layout);
|
||||||
if (options.navigate && typeof window.navigateToTab === "function") {
|
if (options.navigate && typeof window.navigateToTab === "function") {
|
||||||
window.navigateToTab(layout.preferredTab);
|
window.navigateToTab(layout.preferredTab);
|
||||||
}
|
}
|
||||||
@@ -148,12 +213,14 @@
|
|||||||
if (actions && !document.getElementById("operator-layout-select")) {
|
if (actions && !document.getElementById("operator-layout-select")) {
|
||||||
const label = document.createElement("label");
|
const label = document.createElement("label");
|
||||||
label.className = "operator-layout-picker";
|
label.className = "operator-layout-picker";
|
||||||
label.innerHTML = '<span class="visually-hidden">Operator layout</span><select id="operator-layout-select" aria-label="Operator layout"></select>';
|
label.innerHTML = '<span class="visually-hidden">Operator layout</span><select id="operator-layout-select" aria-label="Operator layout" aria-describedby="operator-layout-description"></select><span id="operator-layout-description" class="operator-layout-description"></span>';
|
||||||
const select = label.querySelector("select");
|
const select = label.querySelector("select");
|
||||||
Object.entries(layouts).forEach(([value, layout]) => select.add(new Option(layout.label, value)));
|
const savedLayout = savedLayoutName();
|
||||||
select.value = localStorage.getItem("trxOperatorLayout") || "compact";
|
|
||||||
select.addEventListener("change", () => api.applyLayout(select.value, { navigate: true }));
|
|
||||||
actions.insertBefore(label, actions.firstChild);
|
actions.insertBefore(label, actions.firstChild);
|
||||||
|
select.value = savedLayout;
|
||||||
|
refreshLayoutOptions();
|
||||||
|
if (savedLayout !== "broadcast" && layouts[savedLayout]) select.value = savedLayout;
|
||||||
|
select.addEventListener("change", () => api.applyLayout(select.value, { navigate: true }));
|
||||||
api.applyLayout(select.value);
|
api.applyLayout(select.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,7 +236,7 @@
|
|||||||
if (element) body.appendChild(element);
|
if (element) body.appendChild(element);
|
||||||
});
|
});
|
||||||
tray.appendChild(details);
|
tray.appendChild(details);
|
||||||
api.applyLayout(localStorage.getItem("trxOperatorLayout") || "compact");
|
api.applyLayout(savedLayoutName(), { persist: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,6 +254,14 @@
|
|||||||
menu.id = "mobile-more-menu";
|
menu.id = "mobile-more-menu";
|
||||||
menu.className = "mobile-more-menu";
|
menu.className = "mobile-more-menu";
|
||||||
menu.setAttribute("role", "menu");
|
menu.setAttribute("role", "menu");
|
||||||
|
more.setAttribute("aria-controls", menu.id);
|
||||||
|
const closeMore = (restoreFocus = false) => {
|
||||||
|
if (!menu.classList.contains("is-open")) return;
|
||||||
|
menu.classList.remove("is-open");
|
||||||
|
more.setAttribute("aria-expanded", "false");
|
||||||
|
if (restoreFocus) more.focus();
|
||||||
|
};
|
||||||
|
api.closeMobileOverlays = closeMore;
|
||||||
["statistics", "recorder", "settings", "about"].forEach((tabName) => {
|
["statistics", "recorder", "settings", "about"].forEach((tabName) => {
|
||||||
const source = nav.querySelector(`[data-tab="${tabName}"]`);
|
const source = nav.querySelector(`[data-tab="${tabName}"]`);
|
||||||
if (!source) return;
|
if (!source) return;
|
||||||
@@ -197,15 +272,23 @@
|
|||||||
item.textContent = source.textContent.trim();
|
item.textContent = source.textContent.trim();
|
||||||
item.addEventListener("click", () => {
|
item.addEventListener("click", () => {
|
||||||
if (typeof window.navigateToTab === "function") window.navigateToTab(tabName);
|
if (typeof window.navigateToTab === "function") window.navigateToTab(tabName);
|
||||||
menu.classList.remove("is-open");
|
closeMore();
|
||||||
more.setAttribute("aria-expanded", "false");
|
|
||||||
});
|
});
|
||||||
menu.appendChild(item);
|
menu.appendChild(item);
|
||||||
});
|
});
|
||||||
more.addEventListener("click", () => {
|
more.addEventListener("click", () => {
|
||||||
const open = menu.classList.toggle("is-open");
|
const open = menu.classList.toggle("is-open");
|
||||||
more.setAttribute("aria-expanded", String(open));
|
more.setAttribute("aria-expanded", String(open));
|
||||||
|
if (open) menu.querySelector('[role="menuitem"]')?.focus();
|
||||||
});
|
});
|
||||||
|
document.addEventListener("click", (event) => {
|
||||||
|
if (!menu.contains(event.target) && !more.contains(event.target)) closeMore();
|
||||||
|
});
|
||||||
|
document.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Escape") closeMore(true);
|
||||||
|
});
|
||||||
|
window.addEventListener("resize", () => closeMore());
|
||||||
|
window.addEventListener("popstate", () => closeMore());
|
||||||
nav.append(more, menu);
|
nav.append(more, menu);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -388,6 +388,9 @@ struct RigListItem {
|
|||||||
display_name: Option<String>,
|
display_name: Option<String>,
|
||||||
manufacturer: String,
|
manufacturer: String,
|
||||||
model: String,
|
model: String,
|
||||||
|
supported_modes: Vec<trx_core::RigMode>,
|
||||||
|
tx: bool,
|
||||||
|
filter_controls: bool,
|
||||||
initialized: bool,
|
initialized: bool,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
latitude: Option<f64>,
|
latitude: Option<f64>,
|
||||||
@@ -422,6 +425,9 @@ fn map_rig_entry(entry: &RemoteRigEntry) -> RigListItem {
|
|||||||
display_name: entry.display_name.clone(),
|
display_name: entry.display_name.clone(),
|
||||||
manufacturer: entry.state.info.manufacturer.clone(),
|
manufacturer: entry.state.info.manufacturer.clone(),
|
||||||
model: entry.state.info.model.clone(),
|
model: entry.state.info.model.clone(),
|
||||||
|
supported_modes: entry.state.info.capabilities.supported_modes.clone(),
|
||||||
|
tx: entry.state.info.capabilities.tx,
|
||||||
|
filter_controls: entry.state.info.capabilities.filter_controls,
|
||||||
initialized: entry.state.initialized,
|
initialized: entry.state.initialized,
|
||||||
latitude: entry.state.server_latitude,
|
latitude: entry.state.server_latitude,
|
||||||
longitude: entry.state.server_longitude,
|
longitude: entry.state.server_longitude,
|
||||||
|
|||||||
Reference in New Issue
Block a user