CI / lint (pull_request) Successful in 2m24s
CI / test (pull_request) Successful in 8m34s
CI / test (push) Successful in 7m47s
CI / frontend (pull_request) Successful in 4m28s
CI / reuse (pull_request) Successful in 5s
CI / lint (push) Successful in 2m21s
CI / frontend (push) Successful in 3m37s
CI / reuse (push) Successful in 6s
A client connected to several rigs decodes all of them at once, and the decode stream carries every rig's traffic to every browser. The decoder panels listed all of it: a station a background rig copied on another band appeared in the APRS list next to the selected rig's, the vessel counts and the "latest seen" lines counted both, the status lines said "Receiving" because some other rig was, and the CW pane interleaved two rigs into one stream of text that read as neither. The page is about the rig the operator selected — the one whose spectrum is on screen and whose audio is playing — so each panel now shows what that rig heard: rows, counts, latest-seen, status, the live picture a WEFAX or SSTV frame is painting, and the CW pane. Nothing is dropped on the way in. The map is the whole station's view, has its own rig filter, and would empty out if the plugins stopped feeding it, so the histories still hold every rig and the map still plots them. That also means a switch loses nothing: the runtime gained a rerender hook, which the rig switch calls, and switching back brings the other rig's traffic up again. The CW pane is the exception — a running stream of text cannot be unpicked after the fact — so it starts empty on the rig switched to. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7 Signed-off-by: Stan Grams <sjg@haxx.space>
422 lines
17 KiB
JavaScript
422 lines
17 KiB
JavaScript
import {
|
|
isActiveRigDecode
|
|
} from "./chunk-S57W63QN.js";
|
|
import {
|
|
hostCore
|
|
} from "./chunk-KL66PICH.js";
|
|
|
|
// src/plugins/cw.ts
|
|
var cwWindow = window;
|
|
var cwStatusEl = document.getElementById("cw-status");
|
|
var cwOutputEl = document.getElementById("cw-output");
|
|
var cwAutoInput = document.getElementById("cw-auto");
|
|
var cwWpmInput = document.getElementById("cw-wpm");
|
|
var cwToneInput = document.getElementById("cw-tone");
|
|
var cwSignalIndicator = document.getElementById("cw-signal-indicator");
|
|
var cwToneCanvas = document.getElementById("cw-tone-waterfall");
|
|
var cwToneGl = cwToneCanvas && cwWindow.createTrxWebGlRenderer ? cwWindow.createTrxWebGlRenderer(cwToneCanvas, { alpha: true }) : null;
|
|
var cwTonePickerEl = document.querySelector(".cw-tone-picker");
|
|
var cwToneRangeEl = document.getElementById("cw-tone-range");
|
|
var cwBarOverlay = document.getElementById("cw-bar-overlay");
|
|
var CW_MAX_LINES = 200;
|
|
var CW_TONE_MIN_HZ = 100;
|
|
var CW_TONE_MAX_HZ = 1e4;
|
|
var CW_WPM_MIN = 5;
|
|
var CW_WPM_MAX = 40;
|
|
var CW_BAR_WINDOW_MS = 15 * 60 * 1e3;
|
|
var CW_BAR_LINE_GAP_MS = 5e3;
|
|
var cwLastAppendTime = 0;
|
|
var cwTonePickerRaf = null;
|
|
var cwBarHistory = [];
|
|
var cwBarCurrentLines = /* @__PURE__ */ new Map();
|
|
var cwBarDismissedAtMs = 0;
|
|
var cwAutoLocalOverride = null;
|
|
function escapeCwHtml(input) {
|
|
return hostCore.escapeMapHtml(input);
|
|
}
|
|
function applyCwAutoUi(enabled) {
|
|
if (cwAutoInput) cwAutoInput.checked = enabled;
|
|
if (cwWpmInput) {
|
|
cwWpmInput.disabled = enabled;
|
|
cwWpmInput.readOnly = enabled;
|
|
}
|
|
if (cwToneInput) {
|
|
cwToneInput.disabled = enabled;
|
|
cwToneInput.readOnly = enabled;
|
|
}
|
|
if (cwTonePickerEl) {
|
|
cwTonePickerEl.classList.toggle("is-auto", enabled);
|
|
}
|
|
}
|
|
cwWindow.applyCwAutoUi = applyCwAutoUi;
|
|
cwWindow.applyCwAutoUiFromServer = function(enabled) {
|
|
if (cwAutoLocalOverride !== null) return;
|
|
applyCwAutoUi(enabled);
|
|
};
|
|
function cwBarFlushCurrentLine(key) {
|
|
const line = cwBarCurrentLines.get(key);
|
|
cwBarCurrentLines.delete(key);
|
|
if (line?.text.trim()) {
|
|
cwBarHistory.unshift(line);
|
|
if (cwBarHistory.length > 50) cwBarHistory.length = 50;
|
|
}
|
|
}
|
|
function updateCwBar() {
|
|
if (!cwBarOverlay) return;
|
|
const mode = (document.getElementById("mode")?.value || "").toUpperCase();
|
|
const isCw = mode === "CW" || mode === "CWR";
|
|
const cutoffMs = Date.now() - CW_BAR_WINDOW_MS;
|
|
const recent = cwBarHistory.filter((l) => l.tsMs >= cutoffMs && isActiveRigDecode(l.rigId));
|
|
const inProgress = [...cwBarCurrentLines.values()].filter((l) => l.text && isActiveRigDecode(l.rigId)).sort((a, b) => b.tsMs - a.tsMs);
|
|
const liveLines = [...inProgress, ...recent];
|
|
const newestTsMs = liveLines.reduce((latest, line) => Math.max(latest, line.tsMs || 0), 0);
|
|
if (!isCw || liveLines.length === 0 || newestTsMs <= cwBarDismissedAtMs) {
|
|
cwBarOverlay.style.display = "none";
|
|
cwBarOverlay.innerHTML = "";
|
|
return;
|
|
}
|
|
let html = `<div class="aprs-bar-header"><span class="aprs-bar-title"><span class="aprs-bar-title-word">CW</span><span class="aprs-bar-title-word">Live</span></span><span class="aprs-bar-actions"><span class="aprs-bar-window">Last 15 minutes</span><span class="aprs-bar-clear-wrap"><span class="aprs-bar-clear" role="button" tabindex="0" onclick="window.clearCwBar()" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();window.clearCwBar();}" aria-label="Clear CW overlay">Clear</span></span><button class="aprs-bar-close" type="button" onclick="window.closeCwBar()" aria-label="Close CW overlay">×</button></span></div>`;
|
|
for (const line of liveLines.slice(0, 8)) {
|
|
const ts = line.ts ? `<span class="aprs-bar-time">${line.ts}</span>` : "";
|
|
const meta = [
|
|
line.wpm ? `${String(line.wpm)} WPM` : null,
|
|
line.tone_hz ? `${String(line.tone_hz)} Hz` : null
|
|
].filter(Boolean).join(" · ");
|
|
html += `<div class="aprs-bar-frame"><div class="aprs-bar-frame-main">${ts}${escapeCwHtml(line.text)}` + (meta ? ` <span class="aprs-bar-time">${escapeCwHtml(meta)}</span>` : "") + `</div></div>`;
|
|
}
|
|
cwBarOverlay.innerHTML = html;
|
|
cwBarOverlay.style.display = "flex";
|
|
}
|
|
cwWindow.updateCwBar = updateCwBar;
|
|
cwWindow.clearCwBar = function() {
|
|
resetCwHistoryView();
|
|
};
|
|
cwWindow.closeCwBar = function() {
|
|
cwBarDismissedAtMs = Date.now();
|
|
if (cwBarOverlay) {
|
|
cwBarOverlay.style.display = "none";
|
|
cwBarOverlay.innerHTML = "";
|
|
}
|
|
};
|
|
function clampCwWpm(wpm) {
|
|
const numeric = Number(wpm);
|
|
if (!Number.isFinite(numeric)) return 15;
|
|
return Math.round(Math.max(CW_WPM_MIN, Math.min(CW_WPM_MAX, numeric)));
|
|
}
|
|
function clampCwTone(tone) {
|
|
const numeric = Number(tone);
|
|
if (!Number.isFinite(numeric)) return 700;
|
|
return Math.round(Math.max(CW_TONE_MIN_HZ, Math.min(CW_TONE_MAX_HZ, numeric)));
|
|
}
|
|
function currentCwToneRange() {
|
|
const tunedHz = Number.isFinite(cwWindow.lastFreqHz) ? Number(cwWindow.lastFreqHz) : NaN;
|
|
const bandwidthHz = Number.isFinite(cwWindow.currentBandwidthHz) ? Number(cwWindow.currentBandwidthHz) : NaN;
|
|
if (!Number.isFinite(tunedHz) || !Number.isFinite(bandwidthHz) || bandwidthHz <= 0) {
|
|
return null;
|
|
}
|
|
const mode = (document.getElementById("mode")?.value || "").toUpperCase();
|
|
const lowerSideband = mode === "CWR";
|
|
const upperSideband = mode === "CW";
|
|
if (!lowerSideband && !upperSideband) return null;
|
|
const toneMinHz = CW_TONE_MIN_HZ;
|
|
const toneMaxHz = CW_TONE_MAX_HZ;
|
|
return {
|
|
tunedHz,
|
|
bandwidthHz,
|
|
toneMinHz,
|
|
toneMaxHz,
|
|
toneSpanHz: Math.max(1, toneMaxHz - toneMinHz),
|
|
lowerSideband,
|
|
mode
|
|
};
|
|
}
|
|
function cwToneToRfHz(range, toneHz) {
|
|
if (!range) return NaN;
|
|
return range.lowerSideband ? range.tunedHz - toneHz : range.tunedHz + toneHz;
|
|
}
|
|
function toneClampForRange(tone, range) {
|
|
const clamped = clampCwTone(tone);
|
|
if (!range) return clamped;
|
|
return Math.max(range.toneMinHz, Math.min(range.toneMaxHz, clamped));
|
|
}
|
|
function ensureCwToneCanvasResolution() {
|
|
if (!cwToneCanvas || !cwToneGl || !cwToneGl.ready) return false;
|
|
const rect = cwToneCanvas.getBoundingClientRect();
|
|
const cssWidth = Math.round(rect.width);
|
|
const cssHeight = Math.round(rect.height);
|
|
if (cssWidth < 8 || cssHeight < 8) {
|
|
return false;
|
|
}
|
|
const dpr = window.devicePixelRatio || 1;
|
|
return cwToneGl.ensureSize(cssWidth, cssHeight, dpr);
|
|
}
|
|
function drawCwTonePicker() {
|
|
if (!cwToneCanvas || !cwToneGl || !cwToneGl.ready) return;
|
|
ensureCwToneCanvasResolution();
|
|
if (cwToneCanvas.width < 8 || cwToneCanvas.height < 8) return;
|
|
const width = cwToneCanvas.width;
|
|
const height = cwToneCanvas.height;
|
|
cwToneGl.clear([0, 0, 0, 0]);
|
|
const range = currentCwToneRange();
|
|
if (!cwWindow.lastSpectrumData || !Array.isArray(cwWindow.lastSpectrumData.bins) || !cwWindow.lastSpectrumData.bins.length || !range) {
|
|
if (cwToneRangeEl) {
|
|
const mode = (document.getElementById("mode")?.value || "").toUpperCase();
|
|
if (mode !== "CW" && mode !== "CWR") {
|
|
cwToneRangeEl.textContent = "CW/CWR mode required";
|
|
} else if (!cwWindow.lastSpectrumData || !Array.isArray(cwWindow.lastSpectrumData.bins) || !cwWindow.lastSpectrumData.bins.length) {
|
|
cwToneRangeEl.textContent = "Waiting for spectrum";
|
|
}
|
|
}
|
|
cwToneGl.fillRect(0, 0, width, height, [130 / 255, 150 / 255, 165 / 255, 0.22]);
|
|
return;
|
|
}
|
|
if (cwToneRangeEl) {
|
|
const side = range.lowerSideband ? "Lower side" : "Upper side";
|
|
cwToneRangeEl.textContent = `Audio ${String(range.toneMinHz)}-${String(range.toneMaxHz)} Hz · ${side}`;
|
|
}
|
|
const bins = cwWindow.lastSpectrumData.bins;
|
|
const sampleRate = cwWindow.lastSpectrumData.sample_rate;
|
|
const centerHz = cwWindow.lastSpectrumData.center_hz;
|
|
const maxIdx = Math.max(1, bins.length - 1);
|
|
const fullLoHz = centerHz - sampleRate / 2;
|
|
const tones = new Array(width).fill(-140);
|
|
for (let x = 0; x < width; x += 1) {
|
|
const frac = width <= 1 ? 0 : x / (width - 1);
|
|
const toneHz = range.toneMinHz + frac * range.toneSpanHz;
|
|
const rfHz = cwToneToRfHz(range, toneHz);
|
|
const idx = Math.max(0, Math.min(maxIdx, Math.round((rfHz - fullLoHz) / sampleRate * maxIdx)));
|
|
const power = Number.isFinite(Number(bins[idx])) ? Number(bins[idx]) : -140;
|
|
tones[x] = power;
|
|
}
|
|
const smoothed = new Array(width).fill(-140);
|
|
const smoothRadius = Math.max(1, Math.round(width / 180));
|
|
for (let x = 0; x < width; x += 1) {
|
|
let sum = 0;
|
|
let count = 0;
|
|
for (let i = x - smoothRadius; i <= x + smoothRadius; i += 1) {
|
|
if (i < 0 || i >= width) continue;
|
|
sum += tones[i] ?? -140;
|
|
count += 1;
|
|
}
|
|
smoothed[x] = count > 0 ? sum / count : tones[x] ?? -140;
|
|
}
|
|
const sorted = smoothed.slice().sort((a, b) => a - b);
|
|
const q20 = sorted[Math.floor((sorted.length - 1) * 0.2)] ?? -120;
|
|
const q95 = sorted[Math.floor((sorted.length - 1) * 0.95)] ?? -70;
|
|
const floorDb = Math.min(q20 - 2, q95 - 10);
|
|
const ceilDb = Math.max(floorDb + 18, q95 + 2);
|
|
const dbSpan = Math.max(1, ceilDb - floorDb);
|
|
const yForDb = (db) => {
|
|
const n = Math.max(0, Math.min(1, (db - floorDb) / dbSpan));
|
|
return Math.round((1 - n) * (height - 1));
|
|
};
|
|
const rootStyle = getComputedStyle(document.documentElement);
|
|
const accent = (rootStyle.getPropertyValue("--accent-green") || "").trim() || "#00d17f";
|
|
const parseColor = typeof cwWindow.trxParseCssColor === "function" ? cwWindow.trxParseCssColor : null;
|
|
const accentRgba = parseColor ? parseColor(accent) : [0, 0.82, 0.5, 1];
|
|
const axisColor = [230 / 255, 235 / 255, 245 / 255, 0.15];
|
|
cwToneGl.fillRect(0, 0, width, height, [7 / 255, 12 / 255, 18 / 255, 0.94]);
|
|
const hGridCount = 4;
|
|
const gridSegments = [];
|
|
for (let i = 1; i <= hGridCount; i += 1) {
|
|
const y = Math.round(i / (hGridCount + 1) * (height - 1));
|
|
gridSegments.push(0, y, width, y);
|
|
}
|
|
cwToneGl.drawSegments(gridSegments, axisColor, 1);
|
|
const toneStep = range.toneSpanHz <= 500 ? 50 : range.toneSpanHz <= 1e3 ? 100 : 200;
|
|
const firstTick = Math.ceil(range.toneMinHz / toneStep) * toneStep;
|
|
const tickSegments = [];
|
|
for (let tone = firstTick; tone <= range.toneMaxHz; tone += toneStep) {
|
|
const frac = (tone - range.toneMinHz) / range.toneSpanHz;
|
|
const x = Math.max(0, Math.min(width - 1, Math.round(frac * (width - 1))));
|
|
tickSegments.push(x, 0, x, height);
|
|
}
|
|
cwToneGl.drawSegments(tickSegments, axisColor, 1);
|
|
const linePoints = [];
|
|
for (let x = 0; x < width; x += 1) {
|
|
linePoints.push(x, yForDb(smoothed[x] ?? -140));
|
|
}
|
|
cwToneGl.drawFilledArea(linePoints, height, [accentRgba[0], accentRgba[1], accentRgba[2], 0.24]);
|
|
cwToneGl.drawPolyline(linePoints, accentRgba, Math.max(1.2, (window.devicePixelRatio || 1) * 1.2));
|
|
const currentTone = toneClampForRange(cwToneInput ? cwToneInput.value : 700, range);
|
|
const markerFrac = (currentTone - range.toneMinHz) / range.toneSpanHz;
|
|
const markerX = Math.max(0, Math.min(width - 1, Math.round(markerFrac * (width - 1))));
|
|
const markerY = yForDb(smoothed[Math.max(0, Math.min(width - 1, markerX))] ?? -140);
|
|
cwToneGl.drawSegments([markerX, 0, markerX, height], [1, 1, 1, 0.9], 1.5);
|
|
cwToneGl.drawPoints([markerX, markerY], Math.max(2, Math.round(height * 0.055)), [1, 1, 1, 0.9]);
|
|
if (cwAutoInput?.checked) {
|
|
cwToneGl.fillRect(0, 0, width, height, [0, 0, 0, 0.22]);
|
|
}
|
|
}
|
|
async function setCwTone(tone, { syncInput = true } = {}) {
|
|
const range = currentCwToneRange();
|
|
const clamped = toneClampForRange(tone, range);
|
|
if (cwToneInput && syncInput) {
|
|
cwToneInput.value = String(clamped);
|
|
}
|
|
try {
|
|
await hostCore.postPath(`/set_cw_tone?tone_hz=${encodeURIComponent(clamped)}`);
|
|
} catch (e) {
|
|
console.error("CW tone set failed", e);
|
|
}
|
|
drawCwTonePicker();
|
|
}
|
|
if (cwAutoInput) {
|
|
cwAutoInput.addEventListener("change", () => {
|
|
void (async () => {
|
|
const enabled = cwAutoInput.checked;
|
|
cwAutoLocalOverride = enabled;
|
|
applyCwAutoUi(enabled);
|
|
try {
|
|
await hostCore.postPath(`/set_cw_auto?enabled=${enabled ? "true" : "false"}`);
|
|
drawCwTonePicker();
|
|
} catch (error) {
|
|
console.error("CW auto toggle failed", error);
|
|
} finally {
|
|
cwAutoLocalOverride = null;
|
|
}
|
|
})();
|
|
});
|
|
}
|
|
if (cwWpmInput) {
|
|
cwWpmInput.addEventListener("change", () => {
|
|
void (async () => {
|
|
if (cwAutoInput?.checked) return;
|
|
const wpm = clampCwWpm(cwWpmInput.value);
|
|
cwWpmInput.value = String(wpm);
|
|
try {
|
|
await hostCore.postPath(`/set_cw_wpm?wpm=${encodeURIComponent(wpm)}`);
|
|
} catch (error) {
|
|
console.error("CW WPM set failed", error);
|
|
}
|
|
})();
|
|
});
|
|
}
|
|
if (cwToneInput) {
|
|
cwToneInput.addEventListener("change", () => {
|
|
if (!cwAutoInput?.checked) void setCwTone(cwToneInput.value);
|
|
});
|
|
}
|
|
if (cwToneCanvas) {
|
|
cwToneCanvas.addEventListener("click", (event) => {
|
|
if (cwAutoInput?.checked) return;
|
|
const rect = cwToneCanvas.getBoundingClientRect();
|
|
if (rect.width <= 0) return;
|
|
const range = currentCwToneRange();
|
|
if (!range) return;
|
|
const frac = Math.max(0, Math.min(1, (event.clientX - rect.left) / rect.width));
|
|
const tone = range.toneMinHz + frac * range.toneSpanHz;
|
|
void setCwTone(tone);
|
|
});
|
|
}
|
|
function resetCwHistoryView() {
|
|
if (cwOutputEl) cwOutputEl.innerHTML = "";
|
|
cwLastAppendTime = 0;
|
|
cwBarHistory = [];
|
|
cwBarCurrentLines.clear();
|
|
updateCwBar();
|
|
drawCwTonePicker();
|
|
}
|
|
document.getElementById("settings-clear-cw-history")?.addEventListener("click", () => {
|
|
void (async () => {
|
|
if (!await cwWindow.trxUi.confirm({ title: "Clear CW history?", message: "All stored CW decodes will be permanently removed.", confirmLabel: "Clear history" })) return;
|
|
try {
|
|
await hostCore.postPath("/clear_cw_decode");
|
|
resetCwHistoryView();
|
|
} catch (error) {
|
|
console.error("CW history clear failed", error);
|
|
}
|
|
})();
|
|
});
|
|
function onServerCw(evt) {
|
|
const forSelectedRig = isActiveRigDecode(evt.rig_id ?? null);
|
|
if (cwStatusEl && forSelectedRig) cwStatusEl.textContent = "Receiving";
|
|
if (evt.text && cwOutputEl && forSelectedRig) {
|
|
const now = Date.now();
|
|
if (!cwOutputEl.lastElementChild || now - cwLastAppendTime > 1e4 || evt.text === "\n") {
|
|
const line = document.createElement("div");
|
|
line.className = "cw-line";
|
|
cwOutputEl.appendChild(line);
|
|
}
|
|
cwLastAppendTime = now;
|
|
const lastLine = cwOutputEl.lastElementChild;
|
|
if (lastLine) {
|
|
lastLine.textContent += evt.text;
|
|
}
|
|
while (cwOutputEl.children.length > CW_MAX_LINES) {
|
|
const firstChild = cwOutputEl.firstChild;
|
|
if (!firstChild) break;
|
|
cwOutputEl.removeChild(firstChild);
|
|
}
|
|
cwOutputEl.scrollTop = cwOutputEl.scrollHeight;
|
|
}
|
|
if (evt.text) {
|
|
const now = Date.now();
|
|
const rigId = evt.rig_id ?? null;
|
|
const key = rigId ?? "";
|
|
if (evt.text === "\n") {
|
|
cwBarFlushCurrentLine(key);
|
|
} else {
|
|
let line = cwBarCurrentLines.get(key);
|
|
if (!line || now - line.lastMs > CW_BAR_LINE_GAP_MS) {
|
|
cwBarFlushCurrentLine(key);
|
|
const ts = new Date(now).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
|
line = { rigId, tsMs: now, ts, text: "", wpm: null, tone_hz: null, lastMs: now };
|
|
cwBarCurrentLines.set(key, line);
|
|
}
|
|
line.text += evt.text;
|
|
line.lastMs = now;
|
|
if (Number.isFinite(Number(evt.wpm))) line.wpm = clampCwWpm(evt.wpm);
|
|
if (Number.isFinite(Number(evt.tone_hz))) line.tone_hz = Math.round(Number(evt.tone_hz));
|
|
}
|
|
updateCwBar();
|
|
}
|
|
if (cwSignalIndicator && forSelectedRig) {
|
|
cwSignalIndicator.className = evt.signal_on ? "cw-signal-on" : "cw-signal-off";
|
|
}
|
|
if (forSelectedRig && (!cwAutoInput || cwAutoInput.checked)) {
|
|
if (cwWpmInput && Number.isFinite(Number(evt.wpm))) {
|
|
cwWpmInput.value = String(clampCwWpm(evt.wpm));
|
|
}
|
|
if (cwToneInput && Number.isFinite(Number(evt.tone_hz))) {
|
|
cwToneInput.value = String(toneClampForRange(evt.tone_hz, currentCwToneRange()));
|
|
}
|
|
}
|
|
if (cwTonePickerRaf != null) return;
|
|
cwTonePickerRaf = requestAnimationFrame(() => {
|
|
cwTonePickerRaf = null;
|
|
drawCwTonePicker();
|
|
});
|
|
}
|
|
function restoreCwHistory(events) {
|
|
if (!Array.isArray(events) || events.length === 0) return;
|
|
if (cwStatusEl) cwStatusEl.textContent = "Receiving";
|
|
for (const evt of events) {
|
|
onServerCw(evt);
|
|
}
|
|
}
|
|
cwWindow.trxPluginRuntime.registerDecoder({
|
|
id: "cw",
|
|
onMessage: onServerCw,
|
|
restore: restoreCwHistory,
|
|
reset: resetCwHistoryView,
|
|
// The copied text of a rig that is no longer on screen cannot be unpicked
|
|
// from the pane, so the switch starts the new rig's stream from empty.
|
|
rerender: () => {
|
|
if (cwOutputEl) cwOutputEl.innerHTML = "";
|
|
cwLastAppendTime = 0;
|
|
updateCwBar();
|
|
}
|
|
});
|
|
cwWindow.refreshCwTonePicker = function refreshCwTonePicker() {
|
|
ensureCwToneCanvasResolution();
|
|
drawCwTonePicker();
|
|
};
|
|
window.addEventListener("resize", () => {
|
|
if (ensureCwToneCanvasResolution()) drawCwTonePicker();
|
|
});
|
|
applyCwAutoUi(!!cwAutoInput?.checked);
|
|
updateCwBar();
|
|
ensureCwToneCanvasResolution();
|
|
drawCwTonePicker();
|