[feat](trx-frontend-http): replace header REC button with Recorder tab page
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -254,7 +254,8 @@ function applyAuthRestrictions() {
|
|||||||
"settings-clear-ft2-history",
|
"settings-clear-ft2-history",
|
||||||
"settings-clear-wspr-history",
|
"settings-clear-wspr-history",
|
||||||
"settings-clear-sat-history",
|
"settings-clear-sat-history",
|
||||||
"header-rec-btn"
|
"recorder-start-btn",
|
||||||
|
"recorder-stop-btn"
|
||||||
];
|
];
|
||||||
pluginToggleBtns.forEach(id => {
|
pluginToggleBtns.forEach(id => {
|
||||||
const btn = document.getElementById(id);
|
const btn = document.getElementById(id);
|
||||||
@@ -4300,12 +4301,13 @@ if (spectrumBwSweetBtn) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Tab navigation ---
|
// --- Tab navigation ---
|
||||||
const TAB_ORDER = ["main", "bookmarks", "digital-modes", "map", "statistics", "settings", "about"];
|
const TAB_ORDER = ["main", "bookmarks", "digital-modes", "map", "statistics", "recorder", "settings", "about"];
|
||||||
const TAB_PATHS = {
|
const TAB_PATHS = {
|
||||||
main: "/",
|
main: "/",
|
||||||
bookmarks: "/bookmarks",
|
bookmarks: "/bookmarks",
|
||||||
"digital-modes": "/digital-modes",
|
"digital-modes": "/digital-modes",
|
||||||
map: "/map",
|
map: "/map",
|
||||||
|
recorder: "/recorder",
|
||||||
settings: "/settings",
|
settings: "/settings",
|
||||||
about: "/about",
|
about: "/about",
|
||||||
};
|
};
|
||||||
@@ -4353,6 +4355,9 @@ function navigateToTab(name, options = {}) {
|
|||||||
if (name === "statistics") {
|
if (name === "statistics") {
|
||||||
scheduleStatsRender();
|
scheduleStatsRender();
|
||||||
}
|
}
|
||||||
|
if (name === "recorder") {
|
||||||
|
refreshRecorderStatus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.querySelector(".tab-bar").addEventListener("click", (e) => {
|
document.querySelector(".tab-bar").addEventListener("click", (e) => {
|
||||||
@@ -8857,31 +8862,100 @@ if (headerAudioToggle) {
|
|||||||
headerAudioToggle.addEventListener("click", startRxAudio);
|
headerAudioToggle.addEventListener("click", startRxAudio);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Recorder button ────────────────────────────────────────────────────────
|
// ── Recorder page ──────────────────────────────────────────────────────────
|
||||||
const headerRecBtn = document.getElementById("header-rec-btn");
|
|
||||||
let recorderActive = false;
|
let recorderActive = false;
|
||||||
function syncRecorderBtn() {
|
const recorderStartBtn = document.getElementById("recorder-start-btn");
|
||||||
if (!headerRecBtn) return;
|
const recorderStopBtn = document.getElementById("recorder-stop-btn");
|
||||||
headerRecBtn.classList.toggle("rec-active", recorderActive);
|
const recorderStatusInd = document.getElementById("recorder-status-indicator");
|
||||||
|
|
||||||
|
function syncRecorderUi() {
|
||||||
|
if (recorderStartBtn) recorderStartBtn.disabled = recorderActive;
|
||||||
|
if (recorderStopBtn) recorderStopBtn.disabled = !recorderActive;
|
||||||
|
if (recorderStatusInd) {
|
||||||
|
recorderStatusInd.textContent = recorderActive ? "Recording" : "";
|
||||||
|
recorderStatusInd.classList.toggle("rec-active", recorderActive);
|
||||||
|
}
|
||||||
|
// Sync the tab icon indicator.
|
||||||
|
const tabBtn = document.querySelector('.tab[data-tab="recorder"]');
|
||||||
|
if (tabBtn) tabBtn.classList.toggle("rec-active", recorderActive);
|
||||||
}
|
}
|
||||||
if (headerRecBtn) {
|
|
||||||
headerRecBtn.addEventListener("click", async () => {
|
if (recorderStartBtn) {
|
||||||
|
recorderStartBtn.addEventListener("click", async () => {
|
||||||
try {
|
try {
|
||||||
if (recorderActive) {
|
await postPath("/api/recorder/start");
|
||||||
await postPath("/api/recorder/stop");
|
|
||||||
} else {
|
|
||||||
await postPath("/api/recorder/start");
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Recorder toggle failed", e);
|
console.error("Recorder start failed", e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (recorderStopBtn) {
|
||||||
|
recorderStopBtn.addEventListener("click", async () => {
|
||||||
|
try {
|
||||||
|
await postPath("/api/recorder/stop");
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Recorder stop failed", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
window._syncRecorderState = function (enabled) {
|
window._syncRecorderState = function (enabled) {
|
||||||
recorderActive = enabled;
|
recorderActive = enabled;
|
||||||
syncRecorderBtn();
|
syncRecorderUi();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function refreshRecorderStatus() {
|
||||||
|
try {
|
||||||
|
const [statusResp, filesResp] = await Promise.all([
|
||||||
|
fetch("/api/recorder/status"),
|
||||||
|
fetch("/api/recorder/files"),
|
||||||
|
]);
|
||||||
|
if (statusResp.ok) {
|
||||||
|
const active = await statusResp.json();
|
||||||
|
renderRecorderActive(active);
|
||||||
|
}
|
||||||
|
if (filesResp.ok) {
|
||||||
|
const files = await filesResp.json();
|
||||||
|
renderRecorderFiles(files);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Recorder status fetch failed", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRecorderActive(list) {
|
||||||
|
const el = document.getElementById("recorder-active-list");
|
||||||
|
if (!el) return;
|
||||||
|
if (!list.length) {
|
||||||
|
el.innerHTML = '<p class="recorder-empty">No active recordings.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let html = '<table class="recorder-table"><thead><tr><th>Rig</th><th>VChan</th><th>File</th><th>Started</th></tr></thead><tbody>';
|
||||||
|
for (const r of list) {
|
||||||
|
const started = new Date(r.started_at * 1000).toLocaleTimeString();
|
||||||
|
const fname = r.path.split("/").pop();
|
||||||
|
html += `<tr><td>${escapeMapHtml(r.rig_id)}</td><td>${r.vchan_id ? escapeMapHtml(r.vchan_id) : "-"}</td><td>${escapeMapHtml(fname)}</td><td>${started}</td></tr>`;
|
||||||
|
}
|
||||||
|
html += "</tbody></table>";
|
||||||
|
el.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRecorderFiles(list) {
|
||||||
|
const el = document.getElementById("recorder-files-list");
|
||||||
|
if (!el) return;
|
||||||
|
if (!list.length) {
|
||||||
|
el.innerHTML = '<p class="recorder-empty">No recorded files.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let html = '<table class="recorder-table"><thead><tr><th>File</th><th>Size</th></tr></thead><tbody>';
|
||||||
|
for (const f of list) {
|
||||||
|
const size = f.size < 1048576 ? (f.size / 1024).toFixed(1) + " KB" : (f.size / 1048576).toFixed(1) + " MB";
|
||||||
|
html += `<tr><td>${escapeMapHtml(f.name)}</td><td>${size}</td></tr>`;
|
||||||
|
}
|
||||||
|
html += "</tbody></table>";
|
||||||
|
el.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
const rxVolPct = document.getElementById("rx-vol-pct");
|
const rxVolPct = document.getElementById("rx-vol-pct");
|
||||||
const txVolPct = document.getElementById("tx-vol-pct");
|
const txVolPct = document.getElementById("tx-vol-pct");
|
||||||
|
|
||||||
@@ -10513,14 +10587,14 @@ function createBookmarkChip(bm, colorMap, options = {}) {
|
|||||||
`<svg class='bm-icon-svg' viewBox='0 0 8 12' width='8' height='12' aria-hidden='true'>` +
|
`<svg class='bm-icon-svg' viewBox='0 0 8 12' width='8' height='12' aria-hidden='true'>` +
|
||||||
"<path d='M0,0 h8 v10 l-4,2 l-4,-2 Z'/>" +
|
"<path d='M0,0 h8 v10 l-4,2 l-4,-2 Z'/>" +
|
||||||
`</svg>` +
|
`</svg>` +
|
||||||
`<span class="spectrum-bookmark-freq">${esc(freqStr)}</span>` +
|
`<span class="spectrum-bookmark-freq">${escapeMapHtml(freqStr)}</span>` +
|
||||||
`</span>` +
|
`</span>` +
|
||||||
`<span class="spectrum-bookmark-name">${esc(bm.name)}</span>`
|
`<span class="spectrum-bookmark-name">${escapeMapHtml(bm.name)}</span>`
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
"<svg class='bm-icon-svg' viewBox='0 0 8 12' width='8' height='12' aria-hidden='true'>" +
|
"<svg class='bm-icon-svg' viewBox='0 0 8 12' width='8' height='12' aria-hidden='true'>" +
|
||||||
"<path d='M0,0 h8 v10 l-4,2 l-4,-2 Z'/>" +
|
"<path d='M0,0 h8 v10 l-4,2 l-4,-2 Z'/>" +
|
||||||
"</svg>\u00a0<span class='spectrum-bookmark-name'>" + esc(bm.name) + "</span>"
|
"</svg>\u00a0<span class='spectrum-bookmark-name'>" + escapeMapHtml(bm.name) + "</span>"
|
||||||
);
|
);
|
||||||
span.innerHTML =
|
span.innerHTML =
|
||||||
labelHtml;
|
labelHtml;
|
||||||
|
|||||||
@@ -51,6 +51,9 @@
|
|||||||
<svg class="tab-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 14h12"/><rect x="3" y="8" width="2" height="6" rx="0.4" fill="currentColor" stroke="none" opacity="0.6"/><rect x="7" y="5" width="2" height="9" rx="0.4" fill="currentColor" stroke="none" opacity="0.75"/><rect x="11" y="2" width="2" height="12" rx="0.4" fill="currentColor" stroke="none" opacity="0.9"/></svg>
|
<svg class="tab-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 14h12"/><rect x="3" y="8" width="2" height="6" rx="0.4" fill="currentColor" stroke="none" opacity="0.6"/><rect x="7" y="5" width="2" height="9" rx="0.4" fill="currentColor" stroke="none" opacity="0.75"/><rect x="11" y="2" width="2" height="12" rx="0.4" fill="currentColor" stroke="none" opacity="0.9"/></svg>
|
||||||
<span class="tab-label">Statistics</span>
|
<span class="tab-label">Statistics</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="tab" data-tab="recorder">
|
||||||
|
<svg class="tab-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="8" cy="8" r="6"/><circle cx="8" cy="8" r="2.5" fill="currentColor" stroke="none"/></svg>
|
||||||
|
<span class="tab-label">Recorder</span>
|
||||||
<button class="tab" data-tab="settings">
|
<button class="tab" data-tab="settings">
|
||||||
<svg class="tab-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9.8 3.1a2.6 2.6 0 0 0-2.2 3.9L3.4 11.2a1.2 1.2 0 1 0 1.7 1.7l4.2-4.2a2.6 2.6 0 0 0 3.9-2.2l-1.8.6-1.2-1.2z"/><path d="M10.2 5.8 12 4"/></svg>
|
<svg class="tab-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9.8 3.1a2.6 2.6 0 0 0-2.2 3.9L3.4 11.2a1.2 1.2 0 1 0 1.7 1.7l4.2-4.2a2.6 2.6 0 0 0 3.9-2.2l-1.8.6-1.2-1.2z"/><path d="M10.2 5.8 12 4"/></svg>
|
||||||
<span class="tab-label">Settings</span>
|
<span class="tab-label">Settings</span>
|
||||||
@@ -65,7 +68,6 @@
|
|||||||
<button id="header-audio-toggle" class="header-bar-btn header-audio-btn" aria-label="Toggle audio playback" title="Toggle audio playback">
|
<button id="header-audio-toggle" class="header-bar-btn header-audio-btn" aria-label="Toggle audio playback" title="Toggle audio playback">
|
||||||
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M5 3v10l8-5z"/></svg>
|
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M5 3v10l8-5z"/></svg>
|
||||||
</button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
@@ -1059,6 +1061,26 @@
|
|||||||
<div id="map-weak-signal-summary-list" class="map-qso-summary-list"></div>
|
<div id="map-weak-signal-summary-list" class="map-qso-summary-list"></div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="tab-recorder" class="tab-panel" style="display:none;">
|
||||||
|
<h2 class="section-heading">Recorder</h2>
|
||||||
|
<div class="recorder-controls-bar">
|
||||||
|
<button id="recorder-start-btn" class="recorder-action-btn" type="button">Start recording</button>
|
||||||
|
<button id="recorder-stop-btn" class="recorder-action-btn recorder-stop" type="button" disabled>Stop recording</button>
|
||||||
|
<span id="recorder-status-indicator" class="recorder-status-indicator"></span>
|
||||||
|
</div>
|
||||||
|
<section class="recorder-section">
|
||||||
|
<h3 class="recorder-section-heading">Active recordings</h3>
|
||||||
|
<div id="recorder-active-list" class="recorder-list">
|
||||||
|
<p class="recorder-empty">No active recordings.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="recorder-section">
|
||||||
|
<h3 class="recorder-section-heading">Recorded files</h3>
|
||||||
|
<div id="recorder-files-list" class="recorder-list">
|
||||||
|
<p class="recorder-empty">No recorded files.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
<div id="tab-settings" class="tab-panel" style="display:none;">
|
<div id="tab-settings" class="tab-panel" style="display:none;">
|
||||||
<div class="sub-tab-bar">
|
<div class="sub-tab-bar">
|
||||||
<button class="sub-tab active" data-subtab="settings-scheduler">Scheduler</button>
|
<button class="sub-tab active" data-subtab="settings-scheduler">Scheduler</button>
|
||||||
|
|||||||
@@ -1142,31 +1142,68 @@ small { color: var(--text-muted); }
|
|||||||
color: #00d17f;
|
color: #00d17f;
|
||||||
border-color: #00d17f;
|
border-color: #00d17f;
|
||||||
}
|
}
|
||||||
.header-bar-btn.header-rec-btn {
|
/* ── Recorder page ──────────────────────────────────────────────────────── */
|
||||||
height: 2rem;
|
.recorder-controls-bar {
|
||||||
min-height: 0;
|
display: flex;
|
||||||
padding: 0 0.5rem;
|
gap: 0.75rem;
|
||||||
font-size: 0.7rem;
|
align-items: center;
|
||||||
font-weight: 700;
|
margin-bottom: 1.25rem;
|
||||||
letter-spacing: 0.04em;
|
}
|
||||||
|
.recorder-action-btn {
|
||||||
|
padding: 0.45rem 1rem;
|
||||||
border: 1px solid var(--border-light);
|
border: 1px solid var(--border-light);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background: var(--input-bg);
|
background: var(--input-bg);
|
||||||
color: var(--text-muted);
|
color: var(--text-primary);
|
||||||
|
font-size: 0.85rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
flex-shrink: 0;
|
transition: border-color 0.15s, color 0.15s, background 0.15s;
|
||||||
transition: color 0.15s, border-color 0.15s, background 0.15s;
|
|
||||||
}
|
}
|
||||||
.header-rec-btn.rec-active {
|
.recorder-action-btn:hover:not(:disabled) { border-color: var(--accent-green); color: var(--accent-green); }
|
||||||
|
.recorder-action-btn:disabled { opacity: 0.4; cursor: default; }
|
||||||
|
.recorder-action-btn.recorder-stop:not(:disabled) { border-color: #ff3b30; color: #ff3b30; }
|
||||||
|
.recorder-action-btn.recorder-stop:hover:not(:disabled) { background: rgba(255,59,48,0.1); }
|
||||||
|
.recorder-status-indicator {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
.recorder-status-indicator.rec-active {
|
||||||
color: #ff3b30;
|
color: #ff3b30;
|
||||||
border-color: #ff3b30;
|
font-weight: 600;
|
||||||
background: rgba(255, 59, 48, 0.12);
|
|
||||||
animation: rec-pulse 1.5s ease-in-out infinite;
|
animation: rec-pulse 1.5s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
@keyframes rec-pulse {
|
@keyframes rec-pulse {
|
||||||
0%, 100% { opacity: 1; }
|
0%, 100% { opacity: 1; }
|
||||||
50% { opacity: 0.6; }
|
50% { opacity: 0.6; }
|
||||||
}
|
}
|
||||||
|
.tab.rec-active .tab-icon { color: #ff3b30; }
|
||||||
|
.recorder-section { margin-bottom: 1.5rem; }
|
||||||
|
.recorder-section-heading {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.recorder-list { font-size: 0.82rem; }
|
||||||
|
.recorder-empty { color: var(--text-muted); font-style: italic; margin: 0; }
|
||||||
|
.recorder-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
.recorder-table th,
|
||||||
|
.recorder-table td {
|
||||||
|
padding: 0.35rem 0.6rem;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid var(--border-light);
|
||||||
|
}
|
||||||
|
.recorder-table th {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
}
|
||||||
.header-rig-switch {
|
.header-rig-switch {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user