[feat](trx-frontend-http): add SQL slider and spectrum screenshot hotkey
Co-authored-by: OpenAI Codex <codex@openai.com> Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -298,6 +298,10 @@ function applyCapabilities(caps) {
|
|||||||
}
|
}
|
||||||
scheduleSpectrumLayout();
|
scheduleSpectrumLayout();
|
||||||
}
|
}
|
||||||
|
if (!caps.filter_controls) {
|
||||||
|
sdrSquelchSupported = false;
|
||||||
|
}
|
||||||
|
updateSdrSquelchControlVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
const freqEl = document.getElementById("freq");
|
const freqEl = document.getElementById("freq");
|
||||||
@@ -400,6 +404,7 @@ function vfoColor(idx) {
|
|||||||
let jogAngle = 0;
|
let jogAngle = 0;
|
||||||
let lastClientCount = null;
|
let lastClientCount = null;
|
||||||
let lastLocked = false;
|
let lastLocked = false;
|
||||||
|
let sdrSquelchSupported = false;
|
||||||
let lastRigIds = [];
|
let lastRigIds = [];
|
||||||
let lastRigDisplayNames = {};
|
let lastRigDisplayNames = {};
|
||||||
let lastActiveRigId = null;
|
let lastActiveRigId = null;
|
||||||
@@ -2362,6 +2367,16 @@ function render(update) {
|
|||||||
wfmStFlagEl.classList.toggle("wfm-st-flag-stereo", detected);
|
wfmStFlagEl.classList.toggle("wfm-st-flag-stereo", detected);
|
||||||
wfmStFlagEl.classList.toggle("wfm-st-flag-mono", !detected);
|
wfmStFlagEl.classList.toggle("wfm-st-flag-mono", !detected);
|
||||||
}
|
}
|
||||||
|
const hasSdrSquelchEnabled = typeof update.filter.sdr_squelch_enabled === "boolean";
|
||||||
|
const hasSdrSquelchThreshold = typeof update.filter.sdr_squelch_threshold_db === "number";
|
||||||
|
if (hasSdrSquelchEnabled || hasSdrSquelchThreshold) {
|
||||||
|
sdrSquelchSupported = true;
|
||||||
|
syncSdrSquelchFromServer(
|
||||||
|
hasSdrSquelchEnabled ? update.filter.sdr_squelch_enabled : true,
|
||||||
|
hasSdrSquelchThreshold ? update.filter.sdr_squelch_threshold_db : -120,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
updateSdrSquelchControlVisibility();
|
||||||
}
|
}
|
||||||
if (sdrGainControlsEl && typeof update.show_sdr_gain_control === "boolean") {
|
if (sdrGainControlsEl && typeof update.show_sdr_gain_control === "boolean") {
|
||||||
sdrGainControlsEl.style.display = update.show_sdr_gain_control ? "" : "none";
|
sdrGainControlsEl.style.display = update.show_sdr_gain_control ? "" : "none";
|
||||||
@@ -2381,6 +2396,7 @@ function render(update) {
|
|||||||
}
|
}
|
||||||
lastModeName = modeUpper;
|
lastModeName = modeUpper;
|
||||||
updateWfmControls();
|
updateWfmControls();
|
||||||
|
updateSdrSquelchControlVisibility();
|
||||||
// When filter panel is active (SDR backend), update the BW slider range
|
// When filter panel is active (SDR backend), update the BW slider range
|
||||||
// to match the new mode — but only if the server hasn't already sent a
|
// to match the new mode — but only if the server hasn't already sent a
|
||||||
// filter state that overrides it.
|
// filter state that overrides it.
|
||||||
@@ -5312,6 +5328,12 @@ const sdrGainControlsEl = document.getElementById("sdr-gain-controls");
|
|||||||
const sdrGainEl = document.getElementById("sdr-gain-db");
|
const sdrGainEl = document.getElementById("sdr-gain-db");
|
||||||
const sdrGainSetBtn = document.getElementById("sdr-gain-set");
|
const sdrGainSetBtn = document.getElementById("sdr-gain-set");
|
||||||
const wfmStFlagEl = document.getElementById("wfm-st-flag");
|
const wfmStFlagEl = document.getElementById("wfm-st-flag");
|
||||||
|
const sdrSquelchWrapEl = document.getElementById("sdr-squelch-wrap");
|
||||||
|
const sdrSquelchEl = document.getElementById("sdr-squelch");
|
||||||
|
const sdrSquelchPctEl = document.getElementById("sdr-squelch-pct");
|
||||||
|
const SDR_SQUELCH_MIN_DB = -120;
|
||||||
|
const SDR_SQUELCH_MAX_DB = -30;
|
||||||
|
let syncFromServerSdrSquelch = false;
|
||||||
|
|
||||||
// Hide audio row if audio is not configured on the server
|
// Hide audio row if audio is not configured on the server
|
||||||
fetch("/audio", { method: "GET" }).then((r) => {
|
fetch("/audio", { method: "GET" }).then((r) => {
|
||||||
@@ -5383,6 +5405,74 @@ function normalizeWfmDenoiseLevel(value) {
|
|||||||
return "auto";
|
return "auto";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clampSdrSquelchPercent(value) {
|
||||||
|
if (!Number.isFinite(value)) return 0;
|
||||||
|
return Math.max(0, Math.min(100, Math.round(value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function sdrSquelchPercentToServer(percent) {
|
||||||
|
const pct = clampSdrSquelchPercent(percent);
|
||||||
|
if (pct <= 0) {
|
||||||
|
return { enabled: false, thresholdDb: SDR_SQUELCH_MIN_DB };
|
||||||
|
}
|
||||||
|
const ratio = pct / 100;
|
||||||
|
const thresholdDb = SDR_SQUELCH_MIN_DB + ratio * (SDR_SQUELCH_MAX_DB - SDR_SQUELCH_MIN_DB);
|
||||||
|
return { enabled: true, thresholdDb };
|
||||||
|
}
|
||||||
|
|
||||||
|
function sdrSquelchServerToPercent(enabled, thresholdDb) {
|
||||||
|
if (!enabled) return 0;
|
||||||
|
if (!Number.isFinite(thresholdDb)) return 0;
|
||||||
|
const ratio = (thresholdDb - SDR_SQUELCH_MIN_DB) / (SDR_SQUELCH_MAX_DB - SDR_SQUELCH_MIN_DB);
|
||||||
|
return clampSdrSquelchPercent(ratio * 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSdrSquelchPctLabel() {
|
||||||
|
if (!sdrSquelchEl || !sdrSquelchPctEl) return;
|
||||||
|
const pct = clampSdrSquelchPercent(Number(sdrSquelchEl.value));
|
||||||
|
sdrSquelchPctEl.textContent = pct <= 0 ? "Open" : `${pct}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSdrSquelchControlVisibility() {
|
||||||
|
if (!sdrSquelchWrapEl) return;
|
||||||
|
const mode = (modeEl && modeEl.value ? modeEl.value : "").toUpperCase();
|
||||||
|
sdrSquelchWrapEl.style.display = sdrSquelchSupported && mode !== "WFM" ? "" : "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncSdrSquelchFromServer(enabled, thresholdDb) {
|
||||||
|
if (!sdrSquelchEl) return;
|
||||||
|
if (document.activeElement === sdrSquelchEl) return;
|
||||||
|
const pct = sdrSquelchServerToPercent(enabled, thresholdDb);
|
||||||
|
syncFromServerSdrSquelch = true;
|
||||||
|
sdrSquelchEl.value = String(pct);
|
||||||
|
updateSdrSquelchPctLabel();
|
||||||
|
syncFromServerSdrSquelch = false;
|
||||||
|
saveSetting("sdrSquelchPct", pct);
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitSdrSquelchPercent(percent) {
|
||||||
|
if (!sdrSquelchSupported) return;
|
||||||
|
const { enabled, thresholdDb } = sdrSquelchPercentToServer(percent);
|
||||||
|
postPath(
|
||||||
|
`/set_sdr_squelch?enabled=${enabled ? "true" : "false"}&threshold_db=${encodeURIComponent(thresholdDb.toFixed(2))}`,
|
||||||
|
).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sdrSquelchEl) {
|
||||||
|
const savedPct = clampSdrSquelchPercent(Number(loadSetting("sdrSquelchPct", 0)));
|
||||||
|
sdrSquelchEl.value = String(savedPct);
|
||||||
|
updateSdrSquelchPctLabel();
|
||||||
|
sdrSquelchEl.addEventListener("input", () => {
|
||||||
|
const pct = clampSdrSquelchPercent(Number(sdrSquelchEl.value));
|
||||||
|
sdrSquelchEl.value = String(pct);
|
||||||
|
updateSdrSquelchPctLabel();
|
||||||
|
saveSetting("sdrSquelchPct", pct);
|
||||||
|
if (!syncFromServerSdrSquelch) {
|
||||||
|
submitSdrSquelchPercent(pct);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (wfmAudioModeEl) {
|
if (wfmAudioModeEl) {
|
||||||
wfmAudioModeEl.value = loadSetting("wfmAudioMode", "stereo");
|
wfmAudioModeEl.value = loadSetting("wfmAudioMode", "stereo");
|
||||||
wfmAudioModeEl.addEventListener("change", () => {
|
wfmAudioModeEl.addEventListener("change", () => {
|
||||||
@@ -5824,6 +5914,17 @@ function volWheel(slider, pctEl, getGain, storageKey) {
|
|||||||
}
|
}
|
||||||
volWheel(rxVolSlider, rxVolPct, () => rxGainNode, "rxVol");
|
volWheel(rxVolSlider, rxVolPct, () => rxGainNode, "rxVol");
|
||||||
volWheel(txVolSlider, txVolPct, () => txGainNode, "txVol");
|
volWheel(txVolSlider, txVolPct, () => txGainNode, "txVol");
|
||||||
|
if (sdrSquelchEl) {
|
||||||
|
sdrSquelchEl.addEventListener("wheel", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const step = e.deltaY < 0 ? 2 : -2;
|
||||||
|
const next = clampSdrSquelchPercent(Number(sdrSquelchEl.value) + step);
|
||||||
|
sdrSquelchEl.value = String(next);
|
||||||
|
updateSdrSquelchPctLabel();
|
||||||
|
saveSetting("sdrSquelchPct", next);
|
||||||
|
submitSdrSquelchPercent(next);
|
||||||
|
}, { passive: false });
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById("copyright-year").textContent = new Date().getFullYear();
|
document.getElementById("copyright-year").textContent = new Date().getFullYear();
|
||||||
|
|
||||||
@@ -6942,6 +7043,233 @@ function updateSpectrumDbAxis(dbMin, dbMax, gridStep, heightPx, dpr) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isVisibleForSnapshot(el) {
|
||||||
|
if (!el) return false;
|
||||||
|
const style = getComputedStyle(el);
|
||||||
|
if (style.display === "none" || style.visibility === "hidden") return false;
|
||||||
|
const opacity = Number(style.opacity);
|
||||||
|
if (Number.isFinite(opacity) && opacity <= 0) return false;
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
return rect.width > 0 && rect.height > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawRoundedRectPath(ctx, x, y, w, h, r) {
|
||||||
|
const radius = Math.max(0, Math.min(r, Math.min(w, h) / 2));
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x + radius, y);
|
||||||
|
ctx.lineTo(x + w - radius, y);
|
||||||
|
ctx.quadraticCurveTo(x + w, y, x + w, y + radius);
|
||||||
|
ctx.lineTo(x + w, y + h - radius);
|
||||||
|
ctx.quadraticCurveTo(x + w, y + h, x + w - radius, y + h);
|
||||||
|
ctx.lineTo(x + radius, y + h);
|
||||||
|
ctx.quadraticCurveTo(x, y + h, x, y + h - radius);
|
||||||
|
ctx.lineTo(x, y + radius);
|
||||||
|
ctx.quadraticCurveTo(x, y, x + radius, y);
|
||||||
|
ctx.closePath();
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawElementChrome(ctx, el, rootRect) {
|
||||||
|
if (!isVisibleForSnapshot(el)) return null;
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
const style = getComputedStyle(el);
|
||||||
|
const x = rect.left - rootRect.left;
|
||||||
|
const y = rect.top - rootRect.top;
|
||||||
|
const w = rect.width;
|
||||||
|
const h = rect.height;
|
||||||
|
const radius = parseFloat(style.borderTopLeftRadius) || 0;
|
||||||
|
const bg = cssColorToRgba(style.backgroundColor || "rgba(0,0,0,0)");
|
||||||
|
const borderWidth = Math.max(0, parseFloat(style.borderTopWidth) || 0);
|
||||||
|
const border = cssColorToRgba(style.borderTopColor || "rgba(0,0,0,0)");
|
||||||
|
|
||||||
|
if (bg[3] > 0.01) {
|
||||||
|
drawRoundedRectPath(ctx, x, y, w, h, radius);
|
||||||
|
ctx.fillStyle = `rgba(${Math.round(bg[0])}, ${Math.round(bg[1])}, ${Math.round(bg[2])}, ${bg[3]})`;
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
if (borderWidth > 0 && border[3] > 0.01) {
|
||||||
|
drawRoundedRectPath(ctx, x + borderWidth * 0.5, y + borderWidth * 0.5, w - borderWidth, h - borderWidth, Math.max(0, radius - borderWidth * 0.5));
|
||||||
|
ctx.lineWidth = borderWidth;
|
||||||
|
ctx.strokeStyle = `rgba(${Math.round(border[0])}, ${Math.round(border[1])}, ${Math.round(border[2])}, ${border[3]})`;
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
return { x, y, w, h, style };
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawWrappedText(ctx, text, x, y, maxWidth, lineHeight, maxLines) {
|
||||||
|
const words = String(text || "").split(/\s+/).filter(Boolean);
|
||||||
|
if (!words.length) return;
|
||||||
|
let line = "";
|
||||||
|
let lineIdx = 0;
|
||||||
|
for (let i = 0; i < words.length; i += 1) {
|
||||||
|
const candidate = line ? `${line} ${words[i]}` : words[i];
|
||||||
|
if (ctx.measureText(candidate).width <= maxWidth || !line) {
|
||||||
|
line = candidate;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ctx.fillText(line, x, y + lineIdx * lineHeight);
|
||||||
|
lineIdx += 1;
|
||||||
|
if (lineIdx >= maxLines) return;
|
||||||
|
line = words[i];
|
||||||
|
}
|
||||||
|
if (line && lineIdx < maxLines) {
|
||||||
|
ctx.fillText(line, x, y + lineIdx * lineHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawElementTextBlock(ctx, el, rootRect, fallbackText = null) {
|
||||||
|
const chrome = drawElementChrome(ctx, el, rootRect);
|
||||||
|
if (!chrome) return;
|
||||||
|
const text = (fallbackText == null ? el.innerText : fallbackText) || "";
|
||||||
|
const clean = text.replace(/\s+\n/g, "\n").replace(/\n\s+/g, "\n").trim();
|
||||||
|
if (!clean) return;
|
||||||
|
const style = chrome.style;
|
||||||
|
const fontSize = parseFloat(style.fontSize) || 12;
|
||||||
|
const lineHeight = (parseFloat(style.lineHeight) || fontSize * 1.25);
|
||||||
|
const padX = 6;
|
||||||
|
const padY = 4;
|
||||||
|
const maxWidth = Math.max(20, chrome.w - padX * 2);
|
||||||
|
const maxLines = Math.max(1, Math.floor((chrome.h - padY * 2) / lineHeight));
|
||||||
|
ctx.fillStyle = style.color || "#ffffff";
|
||||||
|
ctx.font = `${style.fontStyle || "normal"} ${style.fontWeight || "400"} ${style.fontSize || "12px"} ${style.fontFamily || "sans-serif"}`;
|
||||||
|
ctx.textBaseline = "top";
|
||||||
|
const lines = clean.split(/\n+/);
|
||||||
|
let lineCursor = 0;
|
||||||
|
for (const line of lines) {
|
||||||
|
if (lineCursor >= maxLines) break;
|
||||||
|
drawWrappedText(
|
||||||
|
ctx,
|
||||||
|
line,
|
||||||
|
chrome.x + padX,
|
||||||
|
chrome.y + padY + lineCursor * lineHeight,
|
||||||
|
maxWidth,
|
||||||
|
lineHeight,
|
||||||
|
maxLines - lineCursor,
|
||||||
|
);
|
||||||
|
lineCursor += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawAxisLabels(ctx, axisEl, rootRect) {
|
||||||
|
if (!isVisibleForSnapshot(axisEl)) return;
|
||||||
|
for (const node of axisEl.children) {
|
||||||
|
if (!(node instanceof HTMLElement)) continue;
|
||||||
|
if (!(node.matches("span") || node.matches("button"))) continue;
|
||||||
|
if (!isVisibleForSnapshot(node)) continue;
|
||||||
|
const chrome = drawElementChrome(ctx, node, rootRect);
|
||||||
|
const text = (node.textContent || "").trim();
|
||||||
|
if (!chrome || !text) continue;
|
||||||
|
const style = chrome.style;
|
||||||
|
ctx.fillStyle = style.color || "#ffffff";
|
||||||
|
ctx.font = `${style.fontStyle || "normal"} ${style.fontWeight || "400"} ${style.fontSize || "12px"} ${style.fontFamily || "sans-serif"}`;
|
||||||
|
ctx.textBaseline = "middle";
|
||||||
|
ctx.fillText(text, chrome.x + 4, chrome.y + chrome.h / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSpectrumSnapshotCanvas() {
|
||||||
|
const rootEl = signalVisualBlockEl || document.querySelector(".signal-visual-block");
|
||||||
|
const spectrumPanelEl = document.getElementById("spectrum-panel");
|
||||||
|
if (!rootEl || !isVisibleForSnapshot(rootEl) || !isVisibleForSnapshot(spectrumPanelEl)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const rootRect = rootEl.getBoundingClientRect();
|
||||||
|
const dpr = window.devicePixelRatio || 1;
|
||||||
|
const out = document.createElement("canvas");
|
||||||
|
out.width = Math.max(1, Math.round(rootRect.width * dpr));
|
||||||
|
out.height = Math.max(1, Math.round(rootRect.height * dpr));
|
||||||
|
const ctx = out.getContext("2d");
|
||||||
|
if (!ctx) return null;
|
||||||
|
ctx.scale(dpr, dpr);
|
||||||
|
|
||||||
|
const bg = getComputedStyle(document.documentElement).getPropertyValue("--bg").trim() || getComputedStyle(document.body).backgroundColor || "#000";
|
||||||
|
ctx.fillStyle = bg;
|
||||||
|
ctx.fillRect(0, 0, rootRect.width, rootRect.height);
|
||||||
|
|
||||||
|
const canvases = [overviewCanvas, spectrumCanvas, signalOverlayCanvas];
|
||||||
|
for (const canvas of canvases) {
|
||||||
|
if (!canvas || !isVisibleForSnapshot(canvas)) continue;
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
ctx.drawImage(
|
||||||
|
canvas,
|
||||||
|
rect.left - rootRect.left,
|
||||||
|
rect.top - rootRect.top,
|
||||||
|
rect.width,
|
||||||
|
rect.height,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decoder overlays over the signal view.
|
||||||
|
const decoderOverlayIds = [
|
||||||
|
"ais-bar-overlay",
|
||||||
|
"vdes-bar-overlay",
|
||||||
|
"ft8-bar-overlay",
|
||||||
|
"aprs-bar-overlay",
|
||||||
|
"rds-ps-overlay",
|
||||||
|
];
|
||||||
|
for (const id of decoderOverlayIds) {
|
||||||
|
const overlayEl = document.getElementById(id);
|
||||||
|
if (!overlayEl || !isVisibleForSnapshot(overlayEl)) continue;
|
||||||
|
drawElementTextBlock(ctx, overlayEl, rootRect);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spectrum axis labels and bookmark chips (includes freq bar).
|
||||||
|
drawAxisLabels(ctx, spectrumFreqAxis, rootRect);
|
||||||
|
drawAxisLabels(ctx, spectrumDbAxis, rootRect);
|
||||||
|
drawAxisLabels(ctx, document.getElementById("spectrum-bookmark-axis"), rootRect);
|
||||||
|
drawAxisLabels(ctx, document.getElementById("spectrum-bookmark-side-left"), rootRect);
|
||||||
|
drawAxisLabels(ctx, document.getElementById("spectrum-bookmark-side-right"), rootRect);
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveCanvasAsPng(canvas, fileName) {
|
||||||
|
if (!canvas) return;
|
||||||
|
if (typeof canvas.toBlob === "function") {
|
||||||
|
canvas.toBlob((blob) => {
|
||||||
|
if (!blob) return;
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = fileName;
|
||||||
|
a.click();
|
||||||
|
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||||
|
}, "image/png");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = canvas.toDataURL("image/png");
|
||||||
|
a.download = fileName;
|
||||||
|
a.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
function captureSpectrumScreenshot() {
|
||||||
|
const snapshotCanvas = buildSpectrumSnapshotCanvas();
|
||||||
|
if (!snapshotCanvas) {
|
||||||
|
showHint("Spectrum view not ready", 1300);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||||
|
saveCanvasAsPng(snapshotCanvas, `trx-spectrum-${stamp}.png`);
|
||||||
|
showHint("Spectrum screenshot saved", 1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldIgnoreGlobalShortcut(target) {
|
||||||
|
if (!(target instanceof HTMLElement)) return false;
|
||||||
|
const tag = target.tagName;
|
||||||
|
if (target.isContentEditable) return true;
|
||||||
|
if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
|
||||||
|
return !!target.closest("[contenteditable='true']");
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("keydown", (event) => {
|
||||||
|
if (event.defaultPrevented || event.repeat) return;
|
||||||
|
if (event.ctrlKey || event.metaKey || event.altKey) return;
|
||||||
|
if (shouldIgnoreGlobalShortcut(event.target)) return;
|
||||||
|
if ((event.key || "").toLowerCase() !== "s") return;
|
||||||
|
event.preventDefault();
|
||||||
|
captureSpectrumScreenshot();
|
||||||
|
});
|
||||||
|
|
||||||
// ── Zoom helpers ──────────────────────────────────────────────────────────────
|
// ── Zoom helpers ──────────────────────────────────────────────────────────────
|
||||||
function spectrumZoomAt(cssX, cssW, data, factor) {
|
function spectrumZoomAt(cssX, cssW, data, factor) {
|
||||||
const range = spectrumVisibleRange(data);
|
const range = spectrumVisibleRange(data);
|
||||||
|
|||||||
@@ -274,6 +274,7 @@
|
|||||||
<button id="tx-audio-btn" type="button">Transmit Audio</button>
|
<button id="tx-audio-btn" type="button">Transmit Audio</button>
|
||||||
<label class="vol-label">RX<input type="range" id="rx-vol" min="0" max="100" value="80" class="vol-slider" /><small class="vol-pct" id="rx-vol-pct">80%</small></label>
|
<label class="vol-label">RX<input type="range" id="rx-vol" min="0" max="100" value="80" class="vol-slider" /><small class="vol-pct" id="rx-vol-pct">80%</small></label>
|
||||||
<label class="vol-label">TX<input type="range" id="tx-vol" min="0" max="100" value="80" class="vol-slider" /><small class="vol-pct" id="tx-vol-pct">80%</small></label>
|
<label class="vol-label">TX<input type="range" id="tx-vol" min="0" max="100" value="80" class="vol-slider" /><small class="vol-pct" id="tx-vol-pct">80%</small></label>
|
||||||
|
<label class="vol-label" id="sdr-squelch-wrap" style="display:none;">SQL<input type="range" id="sdr-squelch" min="0" max="100" value="0" class="vol-slider" /><small class="vol-pct" id="sdr-squelch-pct">Open</small></label>
|
||||||
<div id="audio-level">
|
<div id="audio-level">
|
||||||
<div id="audio-level-fill"></div>
|
<div id="audio-level-fill"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -566,6 +566,27 @@ pub async fn set_sdr_gain(
|
|||||||
send_command(&rig_tx, RigCommand::SetSdrGain(query.db)).await
|
send_command(&rig_tx, RigCommand::SetSdrGain(query.db)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
pub struct SdrSquelchQuery {
|
||||||
|
pub enabled: bool,
|
||||||
|
pub threshold_db: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/set_sdr_squelch")]
|
||||||
|
pub async fn set_sdr_squelch(
|
||||||
|
query: web::Query<SdrSquelchQuery>,
|
||||||
|
rig_tx: web::Data<mpsc::Sender<RigRequest>>,
|
||||||
|
) -> Result<HttpResponse, Error> {
|
||||||
|
send_command(
|
||||||
|
&rig_tx,
|
||||||
|
RigCommand::SetSdrSquelch {
|
||||||
|
enabled: query.enabled,
|
||||||
|
threshold_db: query.threshold_db,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
pub struct WfmDeemphasisQuery {
|
pub struct WfmDeemphasisQuery {
|
||||||
pub us: u32,
|
pub us: u32,
|
||||||
@@ -980,6 +1001,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
|||||||
.service(set_bandwidth)
|
.service(set_bandwidth)
|
||||||
.service(set_fir_taps)
|
.service(set_fir_taps)
|
||||||
.service(set_sdr_gain)
|
.service(set_sdr_gain)
|
||||||
|
.service(set_sdr_squelch)
|
||||||
.service(set_wfm_deemphasis)
|
.service(set_wfm_deemphasis)
|
||||||
.service(set_wfm_stereo)
|
.service(set_wfm_stereo)
|
||||||
.service(set_wfm_denoise)
|
.service(set_wfm_denoise)
|
||||||
|
|||||||
Reference in New Issue
Block a user