[feat](trx-frontend-http): streamline main control layout
Refine main control interactions and presentation in the HTTP frontend.\n\n- remove frequency and mode Set buttons\n- apply mode changes immediately on picker change\n- place Mode/Tune/Transmit-Power controls in one horizontal row\n- align control labels vertically across that row\n- move and enlarge MHz/kHz/Hz selector beside frequency input\n- keep Enter-to-set frequency behavior\n- switch signal measurement to elapsed-time averaging\n- enlarge header logo 2x\n\nCo-authored-by: OpenAI Codex <codex@openai.com> Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
@@ -19,7 +19,6 @@ const vfoPicker = document.getElementById("vfo-picker");
|
||||
const signalBar = document.getElementById("signal-bar");
|
||||
const signalValue = document.getElementById("signal-value");
|
||||
const pttBtn = document.getElementById("ptt-btn");
|
||||
const modeBtn = document.getElementById("mode-apply");
|
||||
const txLimitInput = document.getElementById("tx-limit");
|
||||
const txLimitBtn = document.getElementById("tx-limit-btn");
|
||||
const txLimitRow = document.getElementById("tx-limit-row");
|
||||
@@ -41,7 +40,12 @@ let lastRendered = null;
|
||||
let rigName = "Rig";
|
||||
let hintTimer = null;
|
||||
let sigMeasuring = false;
|
||||
let sigSamples = [];
|
||||
let sigLastSUnits = null;
|
||||
let sigMeasureTimer = null;
|
||||
let sigMeasureLastTickMs = 0;
|
||||
let sigMeasureAccumMs = 0;
|
||||
let sigMeasureWeighted = 0;
|
||||
let sigMeasurePeak = null;
|
||||
let lastFreqHz = null;
|
||||
let jogStep = loadSetting("jogStep", 1000);
|
||||
let minFreqStepHz = 1;
|
||||
@@ -69,7 +73,6 @@ function showHint(msg, duration) {
|
||||
let supportedModes = [];
|
||||
let supportedBands = [];
|
||||
let freqDirty = false;
|
||||
let modeDirty = false;
|
||||
let initialized = false;
|
||||
let lastEventAt = Date.now();
|
||||
let es;
|
||||
@@ -228,7 +231,7 @@ function formatSignal(sUnits) {
|
||||
}
|
||||
|
||||
function setDisabled(disabled) {
|
||||
[freqEl, modeEl, modeBtn, pttBtn, powerBtn, txLimitInput, txLimitBtn, lockBtn].forEach((el) => {
|
||||
[freqEl, modeEl, pttBtn, powerBtn, txLimitInput, txLimitBtn, lockBtn].forEach((el) => {
|
||||
if (el) el.disabled = disabled;
|
||||
});
|
||||
}
|
||||
@@ -317,7 +320,7 @@ function render(update) {
|
||||
window.updateFt8RfDisplay();
|
||||
}
|
||||
}
|
||||
if (!modeDirty && update.status && update.status.mode) {
|
||||
if (update.status && update.status.mode) {
|
||||
const mode = normalizeMode(update.status.mode);
|
||||
modeEl.value = mode ? mode.toUpperCase() : "";
|
||||
}
|
||||
@@ -424,14 +427,12 @@ function render(update) {
|
||||
}
|
||||
if (update.status && update.status.rx && typeof update.status.rx.sig === "number") {
|
||||
const sUnits = dbmToSUnits(update.status.rx.sig);
|
||||
sigLastSUnits = sUnits;
|
||||
const pct = sUnits <= 9 ? Math.max(0, Math.min(100, (sUnits / 9) * 100)) : 100;
|
||||
signalBar.style.width = `${pct}%`;
|
||||
signalValue.textContent = formatSignal(sUnits);
|
||||
if (sigMeasuring) {
|
||||
sigSamples.push(sUnits);
|
||||
sigMeasureBtn.textContent = `Stop (${sigSamples.length})`;
|
||||
}
|
||||
} else {
|
||||
sigLastSUnits = null;
|
||||
signalBar.style.width = "0%";
|
||||
signalValue.textContent = "--";
|
||||
}
|
||||
@@ -756,14 +757,13 @@ jogStepEl.addEventListener("click", (e) => {
|
||||
}
|
||||
}
|
||||
|
||||
modeBtn.addEventListener("click", async () => {
|
||||
async function applyModeFromPicker() {
|
||||
const mode = modeEl.value || "";
|
||||
if (!mode) {
|
||||
showHint("Mode missing", 1500);
|
||||
return;
|
||||
}
|
||||
modeDirty = false;
|
||||
modeBtn.disabled = true;
|
||||
modeEl.disabled = true;
|
||||
showHint("Setting mode…");
|
||||
try {
|
||||
await postPath(`/set_mode?mode=${encodeURIComponent(mode)}`);
|
||||
@@ -772,13 +772,11 @@ modeBtn.addEventListener("click", async () => {
|
||||
showHint("Set mode failed", 2000);
|
||||
console.error(err);
|
||||
} finally {
|
||||
modeBtn.disabled = false;
|
||||
modeEl.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
modeEl.addEventListener("input", () => {
|
||||
modeDirty = true;
|
||||
});
|
||||
modeEl.addEventListener("change", applyModeFromPicker);
|
||||
|
||||
txLimitInput.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") {
|
||||
@@ -997,27 +995,67 @@ const sigMeasureBtn = document.getElementById("sig-measure-btn");
|
||||
const sigClearBtn = document.getElementById("sig-clear-btn");
|
||||
const sigResult = document.getElementById("sig-result");
|
||||
|
||||
function resetSignalMeasurementState() {
|
||||
sigMeasureLastTickMs = 0;
|
||||
sigMeasureAccumMs = 0;
|
||||
sigMeasureWeighted = 0;
|
||||
sigMeasurePeak = null;
|
||||
}
|
||||
|
||||
function updateSignalMeasurement(nowMs) {
|
||||
if (!sigMeasuring) return;
|
||||
if (sigMeasureLastTickMs === 0) {
|
||||
sigMeasureLastTickMs = nowMs;
|
||||
return;
|
||||
}
|
||||
const dt = Math.max(0, nowMs - sigMeasureLastTickMs);
|
||||
sigMeasureLastTickMs = nowMs;
|
||||
if (!Number.isFinite(sigLastSUnits)) return;
|
||||
|
||||
sigMeasureAccumMs += dt;
|
||||
sigMeasureWeighted += sigLastSUnits * dt;
|
||||
if (sigMeasurePeak === null || sigLastSUnits > sigMeasurePeak) {
|
||||
sigMeasurePeak = sigLastSUnits;
|
||||
}
|
||||
}
|
||||
|
||||
function stopSignalMeasurement() {
|
||||
if (sigMeasureTimer) {
|
||||
clearInterval(sigMeasureTimer);
|
||||
sigMeasureTimer = null;
|
||||
}
|
||||
sigMeasuring = false;
|
||||
sigMeasureBtn.textContent = "Measure";
|
||||
sigMeasureBtn.style.borderColor = "";
|
||||
sigMeasureBtn.style.color = "";
|
||||
}
|
||||
|
||||
sigMeasureBtn.addEventListener("click", () => {
|
||||
if (!sigMeasuring) {
|
||||
sigSamples = [];
|
||||
resetSignalMeasurementState();
|
||||
sigMeasuring = true;
|
||||
sigMeasureBtn.textContent = "Stop (0)";
|
||||
sigMeasureBtn.textContent = "Stop (0.0s)";
|
||||
sigMeasureBtn.style.borderColor = "#00d17f";
|
||||
sigMeasureBtn.style.color = "#00d17f";
|
||||
sigMeasureTimer = setInterval(() => {
|
||||
const now = Date.now();
|
||||
updateSignalMeasurement(now);
|
||||
sigMeasureBtn.textContent = `Stop (${(sigMeasureAccumMs / 1000).toFixed(1)}s)`;
|
||||
}, 200);
|
||||
} else {
|
||||
sigMeasuring = false;
|
||||
sigMeasureBtn.textContent = "Measure";
|
||||
sigMeasureBtn.style.borderColor = "";
|
||||
sigMeasureBtn.style.color = "";
|
||||
if (sigSamples.length > 0) {
|
||||
const avg = sigSamples.reduce((a, b) => a + b, 0) / sigSamples.length;
|
||||
const peak = Math.max(...sigSamples);
|
||||
sigResult.textContent = `Avg ${formatSignal(avg)} / Peak ${formatSignal(peak)} (${sigSamples.length} samples)`;
|
||||
updateSignalMeasurement(Date.now());
|
||||
stopSignalMeasurement();
|
||||
if (sigMeasureAccumMs > 0) {
|
||||
const avg = sigMeasureWeighted / sigMeasureAccumMs;
|
||||
const peak = sigMeasurePeak;
|
||||
sigResult.textContent = `Avg ${formatSignal(avg)} / Peak ${formatSignal(peak)} (${(sigMeasureAccumMs / 1000).toFixed(1)}s)`;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
sigClearBtn.addEventListener("click", () => {
|
||||
stopSignalMeasurement();
|
||||
resetSignalMeasurementState();
|
||||
sigResult.textContent = "";
|
||||
});
|
||||
|
||||
|
||||
@@ -41,25 +41,27 @@
|
||||
<button type="button" data-step="1">Hz</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="jog-container">
|
||||
<button id="jog-down" type="button" class="jog-btn">−</button>
|
||||
<div class="jog-wheel" id="jog-wheel">
|
||||
<div class="jog-indicator" id="jog-indicator"></div>
|
||||
</div>
|
||||
<button id="jog-up" type="button" class="jog-btn">+</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="controls-row full-row">
|
||||
<div>
|
||||
<div class="controls-col">
|
||||
<div class="label">Mode</div>
|
||||
<div class="inline">
|
||||
<select class="status-input" id="mode">
|
||||
<option value="">--</option>
|
||||
</select>
|
||||
<button id="mode-apply" type="button">Set</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="controls-col controls-col-center">
|
||||
<div class="label">Tune</div>
|
||||
<div class="jog-container">
|
||||
<button id="jog-down" type="button" class="jog-btn">−</button>
|
||||
<div class="jog-wheel" id="jog-wheel">
|
||||
<div class="jog-indicator" id="jog-indicator"></div>
|
||||
</div>
|
||||
<button id="jog-up" type="button" class="jog-btn">+</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="controls-col">
|
||||
<div class="label">Transmit / Power</div>
|
||||
<div class="btn-grid">
|
||||
<button id="ptt-btn" type="button">Toggle PTT</button>
|
||||
|
||||
@@ -22,10 +22,18 @@ input.status-input, select.status-input { width: 100%; padding: 0.45rem 0.5rem;
|
||||
#freq { font-family: 'DSEG14 Classic', monospace; font-size: 2rem; padding: 0.5rem 0.6rem; letter-spacing: 0.05em; text-align: center; }
|
||||
.controls-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
gap: 1rem;
|
||||
align-items: start;
|
||||
}
|
||||
.controls-col { min-width: 0; }
|
||||
.controls-col-center { justify-self: center; width: auto; }
|
||||
.controls-row .label {
|
||||
margin-bottom: 6px;
|
||||
min-height: 1.2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.btn-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
@@ -37,7 +45,6 @@ input.status-input, select.status-input { width: 100%; padding: 0.45rem 0.5rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.6rem;
|
||||
}
|
||||
.jog-wheel {
|
||||
width: 52px;
|
||||
@@ -137,7 +144,7 @@ button:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||
small { color: var(--text-muted); }
|
||||
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.25rem; }
|
||||
.title { font-size: 1.4rem; font-weight: 700; display: inline-flex; align-items: center; gap: 0.35rem; }
|
||||
.header-logo { height: 5em; width: auto; flex-shrink: 0; filter: drop-shadow(0 4px 12px rgba(0,0,0,0.35)); }
|
||||
.header-logo { height: 10em; width: auto; flex-shrink: 0; filter: drop-shadow(0 4px 12px rgba(0,0,0,0.35)); }
|
||||
.subtitle { color: var(--text-muted); font-size: 0.95rem; }
|
||||
.subtitle a { color: var(--accent-green); text-decoration: none; }
|
||||
.subtitle a:hover { text-decoration: underline; }
|
||||
|
||||
Reference in New Issue
Block a user