Complete TypeScript frontend migration #22
@@ -802,11 +802,12 @@ function setTheme(theme) {
|
|||||||
invalidateBookmarkColors();
|
invalidateBookmarkColors();
|
||||||
}
|
}
|
||||||
function invalidateBookmarkColors() {
|
function invalidateBookmarkColors() {
|
||||||
if (typeof bmOverlayRevision === "undefined") return;
|
const bookmarks = window.trx.modules.bookmarks;
|
||||||
bmOverlayRevision++;
|
if (!bookmarks) return;
|
||||||
|
bookmarks.invalidateColors();
|
||||||
void getComputedStyle(document.documentElement).getPropertyValue("--bg");
|
void getComputedStyle(document.documentElement).getPropertyValue("--bg");
|
||||||
const colorMap = bmCategoryColorMap();
|
const colorMap = bmCategoryColorMap();
|
||||||
const ref = typeof bmOverlayList !== "undefined" ? bmOverlayList : [];
|
const ref = bookmarks.overlayList;
|
||||||
document.querySelectorAll(".spectrum-bookmark-chip").forEach((chip) => {
|
document.querySelectorAll(".spectrum-bookmark-chip").forEach((chip) => {
|
||||||
const bm = ref.find((b) => b.id === chip.dataset.bmId);
|
const bm = ref.find((b) => b.id === chip.dataset.bmId);
|
||||||
if (!bm) return;
|
if (!bm) return;
|
||||||
@@ -1450,7 +1451,7 @@ function drawSignalOverlay() {
|
|||||||
const bwEdge = BW_OVERLAY_COLORS.edge;
|
const bwEdge = BW_OVERLAY_COLORS.edge;
|
||||||
const bwStroke = BW_OVERLAY_COLORS.stroke;
|
const bwStroke = BW_OVERLAY_COLORS.stroke;
|
||||||
const bwHard = BW_OVERLAY_COLORS.hard;
|
const bwHard = BW_OVERLAY_COLORS.hard;
|
||||||
const bmRef = typeof bmOverlayList !== "undefined" ? bmOverlayList : null;
|
const bmRef = window.trx.modules.bookmarks?.overlayList ?? null;
|
||||||
if (Array.isArray(bmRef) && bmRef.length > 0) {
|
if (Array.isArray(bmRef) && bmRef.length > 0) {
|
||||||
const colorMap = bmCategoryColorMap();
|
const colorMap = bmCategoryColorMap();
|
||||||
const grouped = /* @__PURE__ */ new Map();
|
const grouped = /* @__PURE__ */ new Map();
|
||||||
@@ -4625,7 +4626,7 @@ function buildBookmarkTooltipText(bm) {
|
|||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
function nearestBookmarkForHz(hz, widthPx, range) {
|
function nearestBookmarkForHz(hz, widthPx, range) {
|
||||||
const ref = typeof bmOverlayList !== "undefined" ? bmOverlayList : null;
|
const ref = window.trx.modules.bookmarks?.overlayList ?? null;
|
||||||
if (!Array.isArray(ref) || !Number.isFinite(hz) || !widthPx || !range || !Number.isFinite(range.visSpanHz) || range.visSpanHz <= 0) {
|
if (!Array.isArray(ref) || !Number.isFinite(hz) || !widthPx || !range || !Number.isFinite(range.visSpanHz) || range.visSpanHz <= 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -7189,7 +7190,7 @@ function bmThemePalette() {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
function bmCategoryColorMap() {
|
function bmCategoryColorMap() {
|
||||||
const ref = typeof bmOverlayList !== "undefined" ? bmOverlayList : [];
|
const ref = window.trx.modules.bookmarks?.overlayList ?? [];
|
||||||
const cats = [...new Set(ref.map((b) => b.category).filter(Boolean))].sort();
|
const cats = [...new Set(ref.map((b) => b.category).filter(Boolean))].sort();
|
||||||
const palette = bmThemePalette();
|
const palette = bmThemePalette();
|
||||||
const map = { "": palette[0] };
|
const map = { "": palette[0] };
|
||||||
@@ -7216,13 +7217,13 @@ function createBookmarkChip(bm, colorMap, options = {}) {
|
|||||||
span.style.setProperty("--bm-cat-bg", col);
|
span.style.setProperty("--bm-cat-bg", col);
|
||||||
span.style.setProperty("--bm-cat-fg", bmContrastFg(col));
|
span.style.setProperty("--bm-cat-fg", bmContrastFg(col));
|
||||||
span.addEventListener("click", () => {
|
span.addEventListener("click", () => {
|
||||||
if (typeof bmApply === "function") bmApply(bm);
|
void window.trx.modules.bookmarks?.apply(bm);
|
||||||
});
|
});
|
||||||
return span;
|
return span;
|
||||||
}
|
}
|
||||||
function updateSideBookmarkStack(container, bookmarks, colorMap) {
|
function updateSideBookmarkStack(container, bookmarks, colorMap) {
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
const rev = typeof bmOverlayRevision !== "undefined" ? bmOverlayRevision : 0;
|
const rev = window.trx.modules.bookmarks?.overlayRevision ?? 0;
|
||||||
const nextKey = Array.isArray(bookmarks) ? `${rev}:${bookmarks.map((bm) => bm.id).join(",")}` : "";
|
const nextKey = Array.isArray(bookmarks) ? `${rev}:${bookmarks.map((bm) => bm.id).join(",")}` : "";
|
||||||
if (!Array.isArray(bookmarks) || bookmarks.length === 0) {
|
if (!Array.isArray(bookmarks) || bookmarks.length === 0) {
|
||||||
if (container.dataset.bmKey) {
|
if (container.dataset.bmKey) {
|
||||||
@@ -7246,7 +7247,7 @@ function updateBookmarkAxis(range) {
|
|||||||
const leftSideEl = document.getElementById("spectrum-bookmark-side-left");
|
const leftSideEl = document.getElementById("spectrum-bookmark-side-left");
|
||||||
const rightSideEl = document.getElementById("spectrum-bookmark-side-right");
|
const rightSideEl = document.getElementById("spectrum-bookmark-side-right");
|
||||||
if (!axisEl) return;
|
if (!axisEl) return;
|
||||||
const _bmRef = typeof bmOverlayList !== "undefined" ? bmOverlayList : null;
|
const _bmRef = window.trx.modules.bookmarks?.overlayList ?? null;
|
||||||
const allBookmarks = Array.isArray(_bmRef) ? _bmRef : [];
|
const allBookmarks = Array.isArray(_bmRef) ? _bmRef : [];
|
||||||
const visBookmarks = allBookmarks.filter((bm) => bm.freq_hz >= range.visLoHz && bm.freq_hz <= range.visHiHz);
|
const visBookmarks = allBookmarks.filter((bm) => bm.freq_hz >= range.visLoHz && bm.freq_hz <= range.visHiHz);
|
||||||
const leftBookmarks = allBookmarks.filter((bm) => bm.freq_hz < range.visLoHz).sort((a, b) => b.freq_hz - a.freq_hz).slice(0, 3);
|
const leftBookmarks = allBookmarks.filter((bm) => bm.freq_hz < range.visLoHz).sort((a, b) => b.freq_hz - a.freq_hz).slice(0, 3);
|
||||||
@@ -7263,7 +7264,7 @@ function updateBookmarkAxis(range) {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const rev = typeof bmOverlayRevision !== "undefined" ? bmOverlayRevision : 0;
|
const rev = window.trx.modules.bookmarks?.overlayRevision ?? 0;
|
||||||
const newKey = `${rev}:${visBookmarks.map((b) => b.id).join(",")}`;
|
const newKey = `${rev}:${visBookmarks.map((b) => b.id).join(",")}`;
|
||||||
if (axisEl.dataset.bmKey !== newKey) {
|
if (axisEl.dataset.bmKey !== newKey) {
|
||||||
axisEl.dataset.bmKey = newKey;
|
axisEl.dataset.bmKey = newKey;
|
||||||
|
|||||||
@@ -1,62 +1,71 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
let bmScope = "general";
|
(() => {
|
||||||
function bmScopeParam(prefix, scope) {
|
// src/plugins/bookmarks.ts
|
||||||
|
var bridge = window;
|
||||||
|
function bmEl(id) {
|
||||||
|
const element = document.getElementById(id);
|
||||||
|
if (!element) throw new Error(`Missing bookmark element #${id}`);
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
function errorMessage(error) {
|
||||||
|
return error instanceof Error ? error.message : String(error);
|
||||||
|
}
|
||||||
|
var bmScope = "general";
|
||||||
|
function bmScopeParam(prefix, scope) {
|
||||||
const sep = prefix ? "&" : "?";
|
const sep = prefix ? "&" : "?";
|
||||||
return sep + "scope=" + encodeURIComponent(scope != null ? scope : bmScope);
|
return sep + "scope=" + encodeURIComponent(scope != null ? scope : bmScope);
|
||||||
}
|
}
|
||||||
var bmList = [];
|
var bmList = [];
|
||||||
var bmRevision = 0;
|
var bmOverlayList = [];
|
||||||
var bmOverlayList = [];
|
var bmOverlayRevision = 0;
|
||||||
var bmOverlayRevision = 0;
|
var bmFilteredList = [];
|
||||||
let bmFilteredList = [];
|
var bmEditScope = null;
|
||||||
let bmEditId = null;
|
var bmCurrentPage = 1;
|
||||||
let bmEditScope = null;
|
var BM_PAGE_SIZE = 25;
|
||||||
let bmCurrentPage = 1;
|
var bmSelected = /* @__PURE__ */ new Set();
|
||||||
const BM_PAGE_SIZE = 25;
|
function bmFmtFreq(hz) {
|
||||||
const bmSelected = /* @__PURE__ */ new Set();
|
|
||||||
function bmFmtFreq(hz) {
|
|
||||||
if (!Number.isFinite(hz) || hz <= 0) return "--";
|
if (!Number.isFinite(hz) || hz <= 0) return "--";
|
||||||
if (hz >= 1e9) return (hz / 1e9).toFixed(6).replace(/\.?0+$/, "") + " GHz";
|
if (hz >= 1e9) return (hz / 1e9).toFixed(6).replace(/\.?0+$/, "") + " GHz";
|
||||||
if (hz >= 1e6) return (hz / 1e6).toFixed(6).replace(/\.?0+$/, "") + " MHz";
|
if (hz >= 1e6) return (hz / 1e6).toFixed(6).replace(/\.?0+$/, "") + " MHz";
|
||||||
if (hz >= 1e3) return (hz / 1e3).toFixed(3).replace(/\.?0+$/, "") + " kHz";
|
if (hz >= 1e3) return (hz / 1e3).toFixed(3).replace(/\.?0+$/, "") + " kHz";
|
||||||
return hz + " Hz";
|
return `${hz} Hz`;
|
||||||
}
|
}
|
||||||
function bmEsc(str) {
|
function bmEsc(str) {
|
||||||
const d = document.createElement("div");
|
const d = document.createElement("div");
|
||||||
d.appendChild(document.createTextNode(String(str)));
|
d.appendChild(document.createTextNode(String(str)));
|
||||||
return d.innerHTML;
|
return d.innerHTML;
|
||||||
}
|
}
|
||||||
function bmCanControl() {
|
function bmCanControl() {
|
||||||
return typeof authEnabled !== "undefined" && !authEnabled || typeof authRole !== "undefined" && authRole === "control";
|
return typeof bridge.authEnabled !== "undefined" && !bridge.authEnabled || typeof bridge.authRole !== "undefined" && bridge.authRole === "control";
|
||||||
}
|
}
|
||||||
function bmSyncAccess() {
|
function bmSyncAccess() {
|
||||||
const canCtrl = bmCanControl();
|
const canCtrl = bmCanControl();
|
||||||
const addBtn = document.getElementById("bm-add-btn");
|
const addBtn = bmEl("bm-add-btn");
|
||||||
const selectAllBtn = document.getElementById("bm-select-all-btn");
|
const selectAllBtn = bmEl("bm-select-all-btn");
|
||||||
if (addBtn) addBtn.style.display = canCtrl ? "" : "none";
|
if (addBtn) addBtn.style.display = canCtrl ? "" : "none";
|
||||||
if (selectAllBtn) selectAllBtn.style.display = canCtrl ? "" : "none";
|
if (selectAllBtn) selectAllBtn.style.display = canCtrl ? "" : "none";
|
||||||
}
|
}
|
||||||
function bmListScope() {
|
function bmListScope() {
|
||||||
const rig = typeof lastActiveRigId !== "undefined" ? lastActiveRigId : null;
|
const rig = typeof bridge.lastActiveRigId !== "undefined" ? bridge.lastActiveRigId : null;
|
||||||
return rig || "general";
|
return rig || "general";
|
||||||
}
|
}
|
||||||
async function bmFetchOverlay() {
|
async function bmFetchOverlay() {
|
||||||
const overlayScope = bmListScope();
|
const overlayScope = bmListScope();
|
||||||
try {
|
try {
|
||||||
const resp = await fetch("/bookmarks" + bmScopeParam(false, overlayScope));
|
const resp = await fetch("/bookmarks" + bmScopeParam(false, overlayScope));
|
||||||
if (!resp.ok) throw new Error("HTTP " + resp.status);
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||||
bmOverlayList = await resp.json();
|
bmOverlayList = await resp.json();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to fetch overlay bookmarks:", e);
|
console.error("Failed to fetch overlay bookmarks:", e);
|
||||||
bmOverlayList = [];
|
bmOverlayList = [];
|
||||||
}
|
}
|
||||||
bmOverlayRevision++;
|
bmOverlayRevision++;
|
||||||
if (typeof window.syncBookmarkMapLocators === "function") {
|
if (typeof bridge.syncBookmarkMapLocators === "function") {
|
||||||
window.syncBookmarkMapLocators(bmOverlayList);
|
bridge.syncBookmarkMapLocators(bmOverlayList);
|
||||||
}
|
}
|
||||||
if (typeof scheduleSpectrumDraw === "function") scheduleSpectrumDraw();
|
if (typeof bridge.scheduleSpectrumDraw === "function") bridge.scheduleSpectrumDraw();
|
||||||
}
|
}
|
||||||
async function bmFetch(categoryFilter) {
|
async function bmFetch(categoryFilter) {
|
||||||
let url = "/bookmarks";
|
let url = "/bookmarks";
|
||||||
let hasQuery = false;
|
let hasQuery = false;
|
||||||
if (categoryFilter && categoryFilter !== "") {
|
if (categoryFilter && categoryFilter !== "") {
|
||||||
@@ -67,34 +76,33 @@ async function bmFetch(categoryFilter) {
|
|||||||
const overlayPromise = bmFetchOverlay();
|
const overlayPromise = bmFetchOverlay();
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(url);
|
const resp = await fetch(url);
|
||||||
if (!resp.ok) throw new Error("HTTP " + resp.status);
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||||
bmList = await resp.json();
|
bmList = await resp.json();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to fetch bookmarks:", e);
|
console.error("Failed to fetch bookmarks:", e);
|
||||||
bmList = [];
|
bmList = [];
|
||||||
}
|
}
|
||||||
bmRevision++;
|
|
||||||
bmSelected.clear();
|
bmSelected.clear();
|
||||||
bmUpdateSelectionUi();
|
bmUpdateSelectionUi();
|
||||||
bmSyncAccess();
|
bmSyncAccess();
|
||||||
bmApplyFilters();
|
bmApplyFilters();
|
||||||
bmRefreshCategoryFilter(categoryFilter);
|
void bmRefreshCategoryFilter(categoryFilter);
|
||||||
await overlayPromise;
|
await overlayPromise;
|
||||||
}
|
}
|
||||||
function bmApplyFilters() {
|
function bmApplyFilters() {
|
||||||
const text = (document.getElementById("bm-text-filter")?.value || "").trim().toLowerCase();
|
const text = (bmEl("bm-text-filter")?.value || "").trim().toLowerCase();
|
||||||
const modeFilter = (document.getElementById("bm-mode-filter")?.value || "").trim().toUpperCase();
|
const modeFilter = (bmEl("bm-mode-filter")?.value || "").trim().toUpperCase();
|
||||||
let filtered = modeFilter ? bmList.filter((bm) => String(bm.mode || "").toUpperCase() === modeFilter) : bmList;
|
let filtered = modeFilter ? bmList.filter((bm) => (bm.mode || "").toUpperCase() === modeFilter) : bmList;
|
||||||
filtered = text ? filtered.filter(
|
filtered = text ? filtered.filter(
|
||||||
(bm) => (bm.name || "").toLowerCase().includes(text) || (bm.locator || "").toLowerCase().includes(text) || (bm.category || "").toLowerCase().includes(text) || (bm.comment || "").toLowerCase().includes(text)
|
(bm) => (bm.name || "").toLowerCase().includes(text) || (bm.locator || "").toLowerCase().includes(text) || (bm.category || "").toLowerCase().includes(text) || (bm.comment || "").toLowerCase().includes(text)
|
||||||
) : filtered;
|
) : filtered;
|
||||||
bmFilteredList = filtered;
|
bmFilteredList = filtered;
|
||||||
bmCurrentPage = 1;
|
bmCurrentPage = 1;
|
||||||
bmRender(filtered);
|
bmRender(filtered);
|
||||||
}
|
}
|
||||||
async function bmRefreshCategoryFilter(keepValue) {
|
async function bmRefreshCategoryFilter(keepValue) {
|
||||||
const sel = document.getElementById("bm-category-filter");
|
const sel = bmEl("bm-category-filter");
|
||||||
const modeSel = document.getElementById("bm-mode-filter");
|
const modeSel = bmEl("bm-mode-filter");
|
||||||
if (!sel && !modeSel) return;
|
if (!sel && !modeSel) return;
|
||||||
try {
|
try {
|
||||||
const resp = await fetch("/bookmarks" + bmScopeParam(false));
|
const resp = await fetch("/bookmarks" + bmScopeParam(false));
|
||||||
@@ -113,7 +121,7 @@ async function bmRefreshCategoryFilter(keepValue) {
|
|||||||
}
|
}
|
||||||
if (modeSel) {
|
if (modeSel) {
|
||||||
const keepMode = modeSel.value;
|
const keepMode = modeSel.value;
|
||||||
const modes = [...new Set(all.map((b) => String(b.mode || "").trim().toUpperCase()).filter(Boolean))].sort();
|
const modes = [...new Set(all.map((b) => (b.mode || "").trim().toUpperCase()).filter(Boolean))].sort();
|
||||||
while (modeSel.options.length > 1) modeSel.remove(1);
|
while (modeSel.options.length > 1) modeSel.remove(1);
|
||||||
modes.forEach((mode) => {
|
modes.forEach((mode) => {
|
||||||
const opt = document.createElement("option");
|
const opt = document.createElement("option");
|
||||||
@@ -123,17 +131,17 @@ async function bmRefreshCategoryFilter(keepValue) {
|
|||||||
});
|
});
|
||||||
if (keepMode && modes.includes(keepMode)) modeSel.value = keepMode;
|
if (keepMode && modes.includes(keepMode)) modeSel.value = keepMode;
|
||||||
}
|
}
|
||||||
} catch (_) {
|
} catch {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function bmRender(list) {
|
function bmRender(list) {
|
||||||
const tbody = document.getElementById("bm-tbody");
|
const tbody = bmEl("bm-tbody");
|
||||||
const emptyEl = document.getElementById("bm-empty");
|
const emptyEl = bmEl("bm-empty");
|
||||||
const paginatorEl = document.getElementById("bm-paginator");
|
const paginatorEl = bmEl("bm-paginator");
|
||||||
const pageSummaryEl = document.getElementById("bm-page-summary");
|
const pageSummaryEl = bmEl("bm-page-summary");
|
||||||
const pageIndicatorEl = document.getElementById("bm-page-indicator");
|
const pageIndicatorEl = bmEl("bm-page-indicator");
|
||||||
const prevBtn = document.getElementById("bm-page-prev");
|
const prevBtn = bmEl("bm-page-prev");
|
||||||
const nextBtn = document.getElementById("bm-page-next");
|
const nextBtn = bmEl("bm-page-next");
|
||||||
if (!tbody) return;
|
if (!tbody) return;
|
||||||
tbody.innerHTML = "";
|
tbody.innerHTML = "";
|
||||||
if (list.length === 0) {
|
if (list.length === 0) {
|
||||||
@@ -169,93 +177,91 @@ function bmRender(list) {
|
|||||||
if (pageIndicatorEl) pageIndicatorEl.textContent = `Page ${page} of ${totalPages}`;
|
if (pageIndicatorEl) pageIndicatorEl.textContent = `Page ${page} of ${totalPages}`;
|
||||||
if (prevBtn) prevBtn.disabled = page <= 1;
|
if (prevBtn) prevBtn.disabled = page <= 1;
|
||||||
if (nextBtn) nextBtn.disabled = page >= totalPages;
|
if (nextBtn) nextBtn.disabled = page >= totalPages;
|
||||||
}
|
}
|
||||||
function bmChangePage(delta) {
|
function bmChangePage(delta) {
|
||||||
const totalPages = Math.max(1, Math.ceil(bmFilteredList.length / BM_PAGE_SIZE));
|
const totalPages = Math.max(1, Math.ceil(bmFilteredList.length / BM_PAGE_SIZE));
|
||||||
const nextPage = Math.min(Math.max(bmCurrentPage + delta, 1), totalPages);
|
const nextPage = Math.min(Math.max(bmCurrentPage + delta, 1), totalPages);
|
||||||
if (nextPage === bmCurrentPage) return;
|
if (nextPage === bmCurrentPage) return;
|
||||||
bmCurrentPage = nextPage;
|
bmCurrentPage = nextPage;
|
||||||
bmRender(bmFilteredList);
|
bmRender(bmFilteredList);
|
||||||
}
|
}
|
||||||
function bmReadDecoders() {
|
function bmReadDecoders() {
|
||||||
return (window.decoderRegistry || []).filter((d) => d.bookmark_selectable).filter((d) => document.getElementById("bm-dec-" + d.id)?.checked).map((d) => d.id);
|
return (bridge.decoderRegistry || []).filter((d) => d.bookmark_selectable).filter((d) => bmEl("bm-dec-" + d.id)?.checked).map((d) => d.id);
|
||||||
}
|
}
|
||||||
function bmWriteDecoders(decoders) {
|
function bmWriteDecoders(decoders) {
|
||||||
const set = new Set(decoders || []);
|
const set = new Set(decoders || []);
|
||||||
(window.decoderRegistry || []).filter((d) => d.bookmark_selectable).forEach((d) => {
|
(bridge.decoderRegistry || []).filter((d) => d.bookmark_selectable).forEach((d) => {
|
||||||
const el = document.getElementById("bm-dec-" + d.id);
|
const el = bmEl("bm-dec-" + d.id);
|
||||||
if (el) el.checked = set.has(d.id);
|
if (el) el.checked = set.has(d.id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function bmBuildDecoderCheckboxes() {
|
function bmBuildDecoderCheckboxes() {
|
||||||
const container = document.getElementById("bm-decoder-checkboxes");
|
const container = bmEl("bm-decoder-checkboxes");
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
container.innerHTML = "";
|
container.innerHTML = "";
|
||||||
(window.decoderRegistry || []).filter((d) => d.bookmark_selectable).forEach((d) => {
|
(bridge.decoderRegistry || []).filter((d) => d.bookmark_selectable).forEach((d) => {
|
||||||
const label = document.createElement("label");
|
const label = document.createElement("label");
|
||||||
label.className = "bm-decoder-check";
|
label.className = "bm-decoder-check";
|
||||||
label.innerHTML = '<input type="checkbox" id="bm-dec-' + d.id + '" value="' + d.id + '" /> ' + d.label;
|
label.innerHTML = '<input type="checkbox" id="bm-dec-' + d.id + '" value="' + d.id + '" /> ' + d.label;
|
||||||
container.appendChild(label);
|
container.appendChild(label);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function bmOpenForm(bm) {
|
function bmOpenForm(bm) {
|
||||||
const wrap = document.getElementById("bm-form-wrap");
|
const wrap = bmEl("bm-form-wrap");
|
||||||
if (!wrap) return;
|
if (!wrap) return;
|
||||||
bmEditId = bm ? bm.id : null;
|
|
||||||
bmEditScope = bm ? bm.scope || bmScope : null;
|
bmEditScope = bm ? bm.scope || bmScope : null;
|
||||||
bmBuildDecoderCheckboxes();
|
bmBuildDecoderCheckboxes();
|
||||||
document.getElementById("bm-id").value = bm ? bm.id : "";
|
bmEl("bm-id").value = bm ? bm.id : "";
|
||||||
document.getElementById("bm-name").value = bm ? bm.name : "";
|
bmEl("bm-name").value = bm ? bm.name : "";
|
||||||
document.getElementById("bm-freq").value = bm ? bm.freq_hz : "";
|
bmEl("bm-freq").value = bm ? String(bm.freq_hz) : "";
|
||||||
document.getElementById("bm-mode").value = bm ? bm.mode : "";
|
bmEl("bm-mode").value = bm ? bm.mode : "";
|
||||||
document.getElementById("bm-bw").value = bm && bm.bandwidth_hz ? bm.bandwidth_hz : "";
|
bmEl("bm-bw").value = bm?.bandwidth_hz ? String(bm.bandwidth_hz) : "";
|
||||||
document.getElementById("bm-locator").value = bm ? bm.locator || "" : "";
|
bmEl("bm-locator").value = bm ? bm.locator || "" : "";
|
||||||
document.getElementById("bm-category-input").value = bm ? bm.category || "" : "";
|
bmEl("bm-category-input").value = bm ? bm.category || "" : "";
|
||||||
document.getElementById("bm-comment").value = bm ? bm.comment || "" : "";
|
bmEl("bm-comment").value = bm ? bm.comment || "" : "";
|
||||||
bmWriteDecoders(bm ? bm.decoders : []);
|
bmWriteDecoders(bm?.decoders ?? []);
|
||||||
document.getElementById("bm-form-title").textContent = bm ? "Edit Bookmark" : "Add Bookmark";
|
bmEl("bm-form-title").textContent = bm ? "Edit Bookmark" : "Add Bookmark";
|
||||||
wrap.style.display = "flex";
|
wrap.style.display = "flex";
|
||||||
document.getElementById("bm-name").focus();
|
bmEl("bm-name").focus();
|
||||||
}
|
}
|
||||||
function bmCloseForm() {
|
function bmCloseForm() {
|
||||||
const wrap = document.getElementById("bm-form-wrap");
|
const wrap = bmEl("bm-form-wrap");
|
||||||
if (wrap) wrap.style.display = "none";
|
if (wrap) wrap.style.display = "none";
|
||||||
bmEditId = null;
|
|
||||||
}
|
|
||||||
function bmPrefillFromStatus() {
|
|
||||||
if (typeof lastFreqHz === "number" && Number.isFinite(lastFreqHz)) {
|
|
||||||
document.getElementById("bm-freq").value = Math.round(lastFreqHz);
|
|
||||||
}
|
}
|
||||||
if (typeof lastModeName === "string" && lastModeName) {
|
function bmPrefillFromStatus() {
|
||||||
document.getElementById("bm-mode").value = lastModeName;
|
if (typeof bridge.lastFreqHz === "number" && Number.isFinite(bridge.lastFreqHz)) {
|
||||||
|
bmEl("bm-freq").value = String(Math.round(bridge.lastFreqHz));
|
||||||
}
|
}
|
||||||
if (typeof currentBandwidthHz === "number" && currentBandwidthHz > 0) {
|
if (typeof bridge.lastModeName === "string" && bridge.lastModeName) {
|
||||||
document.getElementById("bm-bw").value = Math.round(currentBandwidthHz);
|
bmEl("bm-mode").value = bridge.lastModeName;
|
||||||
}
|
}
|
||||||
const activeDecoders = (window.decoderRegistry || []).filter((d) => d.bookmark_selectable && d.activation === "toggle").filter((d) => {
|
if (typeof bridge.currentBandwidthHz === "number" && bridge.currentBandwidthHz > 0) {
|
||||||
const btn = document.getElementById(d.id + "-decode-toggle-btn");
|
bmEl("bm-bw").value = String(Math.round(bridge.currentBandwidthHz));
|
||||||
|
}
|
||||||
|
const activeDecoders = (bridge.decoderRegistry || []).filter((d) => d.bookmark_selectable && d.activation === "toggle").filter((d) => {
|
||||||
|
const btn = bmEl(d.id + "-decode-toggle-btn");
|
||||||
return btn && btn.dataset.enabled === "true";
|
return btn && btn.dataset.enabled === "true";
|
||||||
}).map((d) => d.id);
|
}).map((d) => d.id);
|
||||||
bmWriteDecoders(activeDecoders);
|
bmWriteDecoders(activeDecoders);
|
||||||
}
|
}
|
||||||
async function bmSave(e) {
|
async function bmSave(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const id = document.getElementById("bm-id").value;
|
const id = bmEl("bm-id").value;
|
||||||
const name = document.getElementById("bm-name").value.trim();
|
const name = bmEl("bm-name").value.trim();
|
||||||
const freqStr = document.getElementById("bm-freq").value;
|
const freqStr = bmEl("bm-freq").value;
|
||||||
const freq_hz = parseInt(freqStr, 10);
|
const freq_hz = parseInt(freqStr, 10);
|
||||||
const mode = document.getElementById("bm-mode").value.trim();
|
const mode = bmEl("bm-mode").value.trim();
|
||||||
const bwStr = document.getElementById("bm-bw").value;
|
const bwStr = bmEl("bm-bw").value;
|
||||||
const bandwidth_hz = bwStr ? parseInt(bwStr, 10) : null;
|
const bandwidth_hz = bwStr ? parseInt(bwStr, 10) : null;
|
||||||
const locator = document.getElementById("bm-locator").value.trim().toUpperCase();
|
const locator = bmEl("bm-locator").value.trim().toUpperCase();
|
||||||
const category = document.getElementById("bm-category-input").value.trim();
|
const category = bmEl("bm-category-input").value.trim();
|
||||||
const comment = document.getElementById("bm-comment").value.trim();
|
const comment = bmEl("bm-comment").value.trim();
|
||||||
const decoders = bmReadDecoders();
|
const decoders = bmReadDecoders();
|
||||||
const formError = document.getElementById("bm-form-error");
|
const formError = bmEl("bm-form-error");
|
||||||
if (formError) formError.textContent = "";
|
if (formError) formError.textContent = "";
|
||||||
if (!name || !Number.isFinite(freq_hz) || !mode) {
|
if (!name || !Number.isFinite(freq_hz) || !mode) {
|
||||||
if (formError) formError.textContent = "Enter a name, a valid frequency, and a mode.";
|
if (formError) formError.textContent = "Enter a name, a valid frequency, and a mode.";
|
||||||
const invalid = !name ? document.getElementById("bm-name") : !Number.isFinite(freq_hz) ? document.getElementById("bm-freq") : document.getElementById("bm-mode");
|
const invalid = !name ? bmEl("bm-name") : !Number.isFinite(freq_hz) ? bmEl("bm-freq") : bmEl("bm-mode");
|
||||||
invalid?.focus();
|
invalid?.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -289,84 +295,84 @@ async function bmSave(e) {
|
|||||||
if (resp.status === 409) {
|
if (resp.status === 409) {
|
||||||
throw new Error("A bookmark for that frequency already exists.");
|
throw new Error("A bookmark for that frequency already exists.");
|
||||||
}
|
}
|
||||||
throw new Error(text || "HTTP " + resp.status);
|
throw new Error(text || `HTTP ${resp.status}`);
|
||||||
}
|
}
|
||||||
bmCloseForm();
|
bmCloseForm();
|
||||||
await bmFetch(document.getElementById("bm-category-filter").value);
|
await bmFetch(bmEl("bm-category-filter").value);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to save bookmark:", err);
|
console.error("Failed to save bookmark:", err);
|
||||||
if (formError) formError.textContent = "Failed to save bookmark: " + err.message;
|
if (formError) formError.textContent = "Failed to save bookmark: " + errorMessage(err);
|
||||||
window.trxUi?.notify("Bookmark could not be saved", { kind: "error" });
|
bridge.trxUi.notify?.("Bookmark could not be saved", { kind: "error" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function bmDelete(id) {
|
async function bmDelete(id) {
|
||||||
if (!await window.trxUi.confirm({ title: "Delete bookmark?", message: "This bookmark will be permanently removed.", confirmLabel: "Delete" })) return;
|
if (!await bridge.trxUi.confirm({ title: "Delete bookmark?", message: "This bookmark will be permanently removed.", confirmLabel: "Delete" })) return;
|
||||||
const bm = bmList.find((b) => b.id === id);
|
const bm = bmList.find((b) => b.id === id);
|
||||||
const scope = bm ? bm.scope : void 0;
|
const scope = bm ? bm.scope : void 0;
|
||||||
try {
|
try {
|
||||||
const resp = await fetch("/bookmarks/" + encodeURIComponent(id) + bmScopeParam(false, scope), {
|
const resp = await fetch("/bookmarks/" + encodeURIComponent(id) + bmScopeParam(false, scope), {
|
||||||
method: "DELETE"
|
method: "DELETE"
|
||||||
});
|
});
|
||||||
if (!resp.ok) throw new Error("HTTP " + resp.status);
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||||
await bmFetch(document.getElementById("bm-category-filter").value);
|
await bmFetch(bmEl("bm-category-filter").value);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to delete bookmark:", err);
|
console.error("Failed to delete bookmark:", err);
|
||||||
window.trxUi?.notify("Failed to delete bookmark: " + err.message, { kind: "error" });
|
bridge.trxUi.notify?.("Failed to delete bookmark: " + errorMessage(err), { kind: "error" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function bmApply(bm) {
|
function bmApply(bm) {
|
||||||
try {
|
try {
|
||||||
if (typeof modeEl !== "undefined" && modeEl) {
|
if (typeof bridge.modeEl !== "undefined" && bridge.modeEl) {
|
||||||
modeEl.value = String(bm.mode || "").toUpperCase();
|
bridge.modeEl.value = (bm.mode || "").toUpperCase();
|
||||||
}
|
}
|
||||||
if (bm.bandwidth_hz) {
|
if (bm.bandwidth_hz) {
|
||||||
if (typeof currentBandwidthHz !== "undefined") {
|
if (typeof bridge.currentBandwidthHz !== "undefined") {
|
||||||
currentBandwidthHz = bm.bandwidth_hz;
|
bridge.currentBandwidthHz = bm.bandwidth_hz;
|
||||||
}
|
}
|
||||||
window.currentBandwidthHz = bm.bandwidth_hz;
|
bridge.currentBandwidthHz = bm.bandwidth_hz;
|
||||||
if (typeof syncBandwidthInput === "function") {
|
if (typeof bridge.syncBandwidthInput === "function") {
|
||||||
syncBandwidthInput(bm.bandwidth_hz);
|
bridge.syncBandwidthInput(bm.bandwidth_hz);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (typeof applyLocalTunedFrequency === "function") {
|
if (typeof bridge.applyLocalTunedFrequency === "function") {
|
||||||
if (typeof _freqOptimisticSeq !== "undefined") {
|
if (typeof bridge._freqOptimisticSeq !== "undefined") {
|
||||||
++_freqOptimisticSeq;
|
++bridge._freqOptimisticSeq;
|
||||||
_freqOptimisticHz = bm.freq_hz;
|
bridge._freqOptimisticHz = bm.freq_hz;
|
||||||
}
|
}
|
||||||
applyLocalTunedFrequency(bm.freq_hz, true);
|
bridge.applyLocalTunedFrequency(bm.freq_hz, true);
|
||||||
}
|
}
|
||||||
if (typeof scheduleSpectrumDraw === "function" && typeof lastSpectrumData !== "undefined" && lastSpectrumData) {
|
if (typeof bridge.scheduleSpectrumDraw === "function" && typeof bridge.lastSpectrumData !== "undefined" && bridge.lastSpectrumData) {
|
||||||
scheduleSpectrumDraw();
|
bridge.scheduleSpectrumDraw();
|
||||||
}
|
}
|
||||||
const tunePromise = (async () => {
|
const tunePromise = (async () => {
|
||||||
if (typeof vchanTakeSchedulerControl === "function") {
|
if (typeof bridge.vchanTakeSchedulerControl === "function") {
|
||||||
await vchanTakeSchedulerControl();
|
await bridge.vchanTakeSchedulerControl();
|
||||||
}
|
}
|
||||||
const onVirtual = typeof vchanInterceptMode === "function" && await vchanInterceptMode(bm.mode);
|
const onVirtual = typeof bridge.vchanInterceptMode === "function" && await bridge.vchanInterceptMode(bm.mode);
|
||||||
if (!onVirtual) {
|
if (!onVirtual) {
|
||||||
await postPath("/set_mode?mode=" + encodeURIComponent(bm.mode));
|
await bridge.postPath("/set_mode?mode=" + encodeURIComponent(bm.mode));
|
||||||
}
|
}
|
||||||
if (bm.bandwidth_hz) {
|
if (bm.bandwidth_hz) {
|
||||||
const bwHandledByVchan = typeof vchanInterceptBandwidth === "function" && await vchanInterceptBandwidth(bm.bandwidth_hz);
|
const bwHandledByVchan = typeof bridge.vchanInterceptBandwidth === "function" && await bridge.vchanInterceptBandwidth(bm.bandwidth_hz);
|
||||||
if (!bwHandledByVchan) {
|
if (!bwHandledByVchan) {
|
||||||
await postPath("/set_bandwidth?hz=" + bm.bandwidth_hz);
|
await bridge.postPath(`/set_bandwidth?hz=${bm.bandwidth_hz}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (typeof setRigFrequency === "function") {
|
if (typeof bridge.setRigFrequency === "function") {
|
||||||
await setRigFrequency(bm.freq_hz);
|
await bridge.setRigFrequency(bm.freq_hz);
|
||||||
} else {
|
} else {
|
||||||
await postPath("/set_freq?hz=" + bm.freq_hz);
|
await bridge.postPath(`/set_freq?hz=${bm.freq_hz}`);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
const hasDecoders = Array.isArray(bm.decoders) && bm.decoders.length > 0;
|
const hasDecoders = Array.isArray(bm.decoders) && bm.decoders.length > 0;
|
||||||
const modeUp = (bm.mode || "").toUpperCase();
|
const modeUp = (bm.mode || "").toUpperCase();
|
||||||
const allToggleDecoders = (window.decoderRegistry || []).filter(
|
const allToggleDecoders = (bridge.decoderRegistry || []).filter(
|
||||||
(d) => d.activation === "toggle"
|
(d) => d.activation === "toggle"
|
||||||
);
|
);
|
||||||
const decoderPromise = allToggleDecoders.length ? (async () => {
|
const decoderPromise = allToggleDecoders.length ? (async () => {
|
||||||
let statusUrl = "/status";
|
let statusUrl = "/status";
|
||||||
if (typeof lastActiveRigId !== "undefined" && lastActiveRigId) {
|
if (typeof bridge.lastActiveRigId !== "undefined" && bridge.lastActiveRigId) {
|
||||||
statusUrl += "?remote=" + encodeURIComponent(lastActiveRigId);
|
statusUrl += "?remote=" + encodeURIComponent(bridge.lastActiveRigId);
|
||||||
}
|
}
|
||||||
const statusResp = await fetch(statusUrl);
|
const statusResp = await fetch(statusUrl);
|
||||||
if (!statusResp.ok) return;
|
if (!statusResp.ok) return;
|
||||||
@@ -380,47 +386,62 @@ async function bmApply(bm) {
|
|||||||
if (!compatible) {
|
if (!compatible) {
|
||||||
wanted = false;
|
wanted = false;
|
||||||
} else if (hasDecoders) {
|
} else if (hasDecoders) {
|
||||||
wanted = bm.decoders.includes(d.id);
|
wanted = bm.decoders?.includes(d.id) ?? false;
|
||||||
} else {
|
} else {
|
||||||
wanted = currentlyOn;
|
wanted = currentlyOn;
|
||||||
}
|
}
|
||||||
if (wanted !== currentlyOn) {
|
if (wanted !== currentlyOn) {
|
||||||
toggles.push(postPath("/toggle_" + d.id.replace(/-/g, "_") + "_decode"));
|
toggles.push(bridge.postPath("/toggle_" + d.id.replace(/-/g, "_") + "_decode"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (toggles.length) await Promise.all(toggles);
|
if (toggles.length) await Promise.all(toggles);
|
||||||
})() : Promise.resolve();
|
})() : Promise.resolve();
|
||||||
Promise.all([tunePromise, decoderPromise]).catch(
|
void Promise.all([tunePromise, decoderPromise]).catch((error) => {
|
||||||
(err) => console.error("Bookmark apply background error:", err)
|
console.error("Bookmark apply background error:", error);
|
||||||
);
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to apply bookmark:", err);
|
console.error("Failed to apply bookmark:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function bmUpdateSelectionUi() {
|
bridge.trx ??= {};
|
||||||
|
bridge.trx.modules ??= {};
|
||||||
|
bridge.trx.modules.bookmarks = {
|
||||||
|
get overlayList() {
|
||||||
|
return bmOverlayList;
|
||||||
|
},
|
||||||
|
get overlayRevision() {
|
||||||
|
return bmOverlayRevision;
|
||||||
|
},
|
||||||
|
refreshOverlay: bmFetchOverlay,
|
||||||
|
invalidateColors() {
|
||||||
|
bmOverlayRevision += 1;
|
||||||
|
},
|
||||||
|
apply: bmApply
|
||||||
|
};
|
||||||
|
function bmUpdateSelectionUi() {
|
||||||
const count = bmSelected.size;
|
const count = bmSelected.size;
|
||||||
const canCtrl = bmCanControl();
|
const canCtrl = bmCanControl();
|
||||||
const visible = count > 0 && canCtrl;
|
const visible = count > 0 && canCtrl;
|
||||||
const btn = document.getElementById("bm-del-selected-btn");
|
const btn = bmEl("bm-del-selected-btn");
|
||||||
const countEl = document.getElementById("bm-del-selected-count");
|
const countEl = bmEl("bm-del-selected-count");
|
||||||
if (btn) btn.style.display = visible ? "" : "none";
|
if (btn) btn.style.display = visible ? "" : "none";
|
||||||
if (countEl) countEl.textContent = count;
|
if (countEl) countEl.textContent = String(count);
|
||||||
const moveWrap = document.getElementById("bm-move-selected-wrap");
|
const moveWrap = bmEl("bm-move-selected-wrap");
|
||||||
const moveCountEl = document.getElementById("bm-move-selected-count");
|
const moveCountEl = bmEl("bm-move-selected-count");
|
||||||
if (moveWrap) moveWrap.style.display = visible ? "" : "none";
|
if (moveWrap) moveWrap.style.display = visible ? "" : "none";
|
||||||
if (moveCountEl) moveCountEl.textContent = count;
|
if (moveCountEl) moveCountEl.textContent = String(count);
|
||||||
if (visible) bmPopulateMoveTarget();
|
if (visible) bmPopulateMoveTarget();
|
||||||
const selectAllBtn = document.getElementById("bm-select-all-btn");
|
const selectAllBtn = bmEl("bm-select-all-btn");
|
||||||
if (selectAllBtn && bmCanControl()) {
|
if (selectAllBtn && bmCanControl()) {
|
||||||
const allSelected = bmFilteredList.length > 0 && bmFilteredList.every((bm) => bmSelected.has(bm.id));
|
const allSelected = bmFilteredList.length > 0 && bmFilteredList.every((bm) => bmSelected.has(bm.id));
|
||||||
selectAllBtn.textContent = allSelected ? "Deselect All" : "Select All";
|
selectAllBtn.textContent = allSelected ? "Deselect All" : "Select All";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function bmPopulateMoveTarget() {
|
function bmPopulateMoveTarget() {
|
||||||
const sel = document.getElementById("bm-move-target");
|
const sel = bmEl("bm-move-target");
|
||||||
if (!sel) return;
|
if (!sel) return;
|
||||||
const rigIds = typeof lastRigIds !== "undefined" && Array.isArray(lastRigIds) ? lastRigIds : [];
|
const rigIds = typeof bridge.lastRigIds !== "undefined" && Array.isArray(bridge.lastRigIds) ? bridge.lastRigIds : [];
|
||||||
const displayNames = typeof lastRigDisplayNames !== "undefined" ? lastRigDisplayNames : {};
|
const displayNames = typeof bridge.lastRigDisplayNames !== "undefined" ? bridge.lastRigDisplayNames : {};
|
||||||
const prev = sel.value;
|
const prev = sel.value;
|
||||||
sel.innerHTML = "";
|
sel.innerHTML = "";
|
||||||
if (bmScope !== "general") {
|
if (bmScope !== "general") {
|
||||||
@@ -439,14 +460,14 @@ function bmPopulateMoveTarget() {
|
|||||||
if (prev && sel.querySelector(`option[value="${CSS.escape(prev)}"]`)) {
|
if (prev && sel.querySelector(`option[value="${CSS.escape(prev)}"]`)) {
|
||||||
sel.value = prev;
|
sel.value = prev;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function bmMoveSelected() {
|
async function bmMoveSelected() {
|
||||||
const ids = Array.from(bmSelected);
|
const ids = Array.from(bmSelected);
|
||||||
if (ids.length === 0) return;
|
if (ids.length === 0) return;
|
||||||
const target = document.getElementById("bm-move-target")?.value;
|
const target = bmEl("bm-move-target")?.value;
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
const targetLabel = document.getElementById("bm-move-target")?.selectedOptions[0]?.textContent || target;
|
const targetLabel = bmEl("bm-move-target")?.selectedOptions[0]?.textContent || target;
|
||||||
if (!await window.trxUi.confirm({
|
if (!await bridge.trxUi.confirm({
|
||||||
title: "Move selected bookmarks?",
|
title: "Move selected bookmarks?",
|
||||||
message: `${ids.length} bookmark${ids.length > 1 ? "s" : ""} will move to “${targetLabel}”.`,
|
message: `${ids.length} bookmark${ids.length > 1 ? "s" : ""} will move to “${targetLabel}”.`,
|
||||||
confirmLabel: "Move",
|
confirmLabel: "Move",
|
||||||
@@ -466,19 +487,19 @@ async function bmMoveSelected() {
|
|||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ ids: scopeIds, to: target })
|
body: JSON.stringify({ ids: scopeIds, to: target })
|
||||||
}).then((r) => {
|
}).then((r) => {
|
||||||
if (!r.ok) throw new Error("HTTP " + r.status);
|
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
||||||
})
|
})
|
||||||
));
|
));
|
||||||
bmSelected.clear();
|
bmSelected.clear();
|
||||||
bmUpdateSelectionUi();
|
bmUpdateSelectionUi();
|
||||||
await bmFetch(document.getElementById("bm-category-filter").value);
|
await bmFetch(bmEl("bm-category-filter").value);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to move bookmarks:", err);
|
console.error("Failed to move bookmarks:", err);
|
||||||
window.trxUi?.notify("Failed to move bookmarks: " + err.message, { kind: "error" });
|
bridge.trxUi.notify?.("Failed to move bookmarks: " + errorMessage(err), { kind: "error" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function bmSyncSelectAllCheckbox() {
|
function bmSyncSelectAllCheckbox() {
|
||||||
const selectAll = document.getElementById("bm-select-all");
|
const selectAll = bmEl("bm-select-all");
|
||||||
if (!selectAll) return;
|
if (!selectAll) return;
|
||||||
const checkboxes = document.querySelectorAll(".bm-row-sel");
|
const checkboxes = document.querySelectorAll(".bm-row-sel");
|
||||||
if (checkboxes.length === 0) {
|
if (checkboxes.length === 0) {
|
||||||
@@ -489,11 +510,11 @@ function bmSyncSelectAllCheckbox() {
|
|||||||
const checkedCount = Array.from(checkboxes).filter((cb) => cb.checked).length;
|
const checkedCount = Array.from(checkboxes).filter((cb) => cb.checked).length;
|
||||||
selectAll.checked = checkedCount === checkboxes.length;
|
selectAll.checked = checkedCount === checkboxes.length;
|
||||||
selectAll.indeterminate = checkedCount > 0 && checkedCount < checkboxes.length;
|
selectAll.indeterminate = checkedCount > 0 && checkedCount < checkboxes.length;
|
||||||
}
|
}
|
||||||
async function bmDeleteSelected() {
|
async function bmDeleteSelected() {
|
||||||
const ids = Array.from(bmSelected);
|
const ids = Array.from(bmSelected);
|
||||||
if (ids.length === 0) return;
|
if (ids.length === 0) return;
|
||||||
if (!await window.trxUi.confirm({
|
if (!await bridge.trxUi.confirm({
|
||||||
title: "Delete selected bookmarks?",
|
title: "Delete selected bookmarks?",
|
||||||
message: `${ids.length} bookmark${ids.length > 1 ? "s" : ""} will be permanently removed.`,
|
message: `${ids.length} bookmark${ids.length > 1 ? "s" : ""} will be permanently removed.`,
|
||||||
confirmLabel: "Delete"
|
confirmLabel: "Delete"
|
||||||
@@ -511,22 +532,22 @@ async function bmDeleteSelected() {
|
|||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ ids: scopeIds })
|
body: JSON.stringify({ ids: scopeIds })
|
||||||
}).then((r) => {
|
}).then((r) => {
|
||||||
if (!r.ok) throw new Error("HTTP " + r.status);
|
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
||||||
})
|
})
|
||||||
));
|
));
|
||||||
bmSelected.clear();
|
bmSelected.clear();
|
||||||
bmUpdateSelectionUi();
|
bmUpdateSelectionUi();
|
||||||
await bmFetch(document.getElementById("bm-category-filter").value);
|
await bmFetch(bmEl("bm-category-filter").value);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to delete bookmarks:", err);
|
console.error("Failed to delete bookmarks:", err);
|
||||||
window.trxUi?.notify("Failed to delete bookmarks: " + err.message, { kind: "error" });
|
bridge.trxUi.notify?.("Failed to delete bookmarks: " + errorMessage(err), { kind: "error" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function bmPopulateScopePicker() {
|
function bmPopulateScopePicker() {
|
||||||
const picker = document.getElementById("bm-scope-picker");
|
const picker = bmEl("bm-scope-picker");
|
||||||
if (!picker) return;
|
if (!picker) return;
|
||||||
const rigIds = typeof lastRigIds !== "undefined" && Array.isArray(lastRigIds) ? lastRigIds : [];
|
const rigIds = typeof bridge.lastRigIds !== "undefined" && Array.isArray(bridge.lastRigIds) ? bridge.lastRigIds : [];
|
||||||
const displayNames = typeof lastRigDisplayNames !== "undefined" ? lastRigDisplayNames : {};
|
const displayNames = typeof bridge.lastRigDisplayNames !== "undefined" ? bridge.lastRigDisplayNames : {};
|
||||||
const prev = picker.value;
|
const prev = picker.value;
|
||||||
while (picker.options.length > 1) picker.remove(1);
|
while (picker.options.length > 1) picker.remove(1);
|
||||||
rigIds.forEach((id) => {
|
rigIds.forEach((id) => {
|
||||||
@@ -541,68 +562,72 @@ function bmPopulateScopePicker() {
|
|||||||
picker.value = "general";
|
picker.value = "general";
|
||||||
}
|
}
|
||||||
bmScope = picker.value;
|
bmScope = picker.value;
|
||||||
}
|
}
|
||||||
(function initBookmarks() {
|
(function initBookmarks() {
|
||||||
bmSyncAccess();
|
bmSyncAccess();
|
||||||
bmBuildDecoderCheckboxes();
|
bmBuildDecoderCheckboxes();
|
||||||
if (typeof window.onDecoderRegistryReady === "function") {
|
if (typeof bridge.onDecoderRegistryReady === "function") {
|
||||||
window.onDecoderRegistryReady(bmBuildDecoderCheckboxes);
|
bridge.onDecoderRegistryReady(bmBuildDecoderCheckboxes);
|
||||||
}
|
}
|
||||||
bmPopulateScopePicker();
|
bmPopulateScopePicker();
|
||||||
const scopePicker = document.getElementById("bm-scope-picker");
|
const scopePicker = bmEl("bm-scope-picker");
|
||||||
if (scopePicker) {
|
if (scopePicker) {
|
||||||
scopePicker.addEventListener("change", (e) => {
|
scopePicker.addEventListener("change", (e) => {
|
||||||
bmScope = e.target.value;
|
bmScope = e.currentTarget.value;
|
||||||
bmFetch(document.getElementById("bm-category-filter")?.value || "");
|
void bmFetch(bmEl("bm-category-filter").value || "");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
document.querySelector(".tab-bar").addEventListener("click", (e) => {
|
document.querySelector(".tab-bar")?.addEventListener("click", (e) => {
|
||||||
const btn = e.target.closest('.tab[data-tab="bookmarks"]');
|
const btn = e.target instanceof Element ? e.target.closest('.tab[data-tab="bookmarks"]') : null;
|
||||||
if (!btn) return;
|
if (!btn) return;
|
||||||
bmFetch(document.getElementById("bm-category-filter").value);
|
void bmFetch(bmEl("bm-category-filter").value);
|
||||||
});
|
});
|
||||||
document.getElementById("bm-add-btn").addEventListener("click", () => {
|
bmEl("bm-add-btn").addEventListener("click", () => {
|
||||||
bmOpenForm(null);
|
bmOpenForm(null);
|
||||||
bmPrefillFromStatus();
|
bmPrefillFromStatus();
|
||||||
});
|
});
|
||||||
document.getElementById("bm-category-filter").addEventListener("change", (e) => {
|
bmEl("bm-category-filter").addEventListener("change", (e) => {
|
||||||
bmFetch(e.target.value);
|
void bmFetch(e.currentTarget.value);
|
||||||
});
|
});
|
||||||
document.getElementById("bm-mode-filter").addEventListener("change", () => {
|
bmEl("bm-mode-filter").addEventListener("change", () => {
|
||||||
bmApplyFilters();
|
bmApplyFilters();
|
||||||
});
|
});
|
||||||
document.getElementById("bm-text-filter").addEventListener("input", () => {
|
bmEl("bm-text-filter").addEventListener("input", () => {
|
||||||
bmApplyFilters();
|
bmApplyFilters();
|
||||||
});
|
});
|
||||||
document.getElementById("bm-page-prev").addEventListener("click", () => {
|
bmEl("bm-page-prev").addEventListener("click", () => {
|
||||||
bmChangePage(-1);
|
bmChangePage(-1);
|
||||||
});
|
});
|
||||||
document.getElementById("bm-page-next").addEventListener("click", () => {
|
bmEl("bm-page-next").addEventListener("click", () => {
|
||||||
bmChangePage(1);
|
bmChangePage(1);
|
||||||
});
|
});
|
||||||
document.getElementById("bm-form").addEventListener("submit", bmSave);
|
bmEl("bm-form").addEventListener("submit", (event) => {
|
||||||
document.getElementById("bm-form-cancel").addEventListener("click", bmCloseForm);
|
void bmSave(event);
|
||||||
const formWrap = document.getElementById("bm-form-wrap");
|
});
|
||||||
|
bmEl("bm-form-cancel").addEventListener("click", bmCloseForm);
|
||||||
|
const formWrap = bmEl("bm-form-wrap");
|
||||||
if (formWrap) {
|
if (formWrap) {
|
||||||
formWrap.addEventListener("click", (e) => {
|
formWrap.addEventListener("click", (e) => {
|
||||||
if (e.target === formWrap) bmCloseForm();
|
if (e.target === formWrap) bmCloseForm();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
document.addEventListener("keydown", (e) => {
|
document.addEventListener("keydown", (e) => {
|
||||||
if (e.key === "Escape" && document.getElementById("bm-form-wrap")?.style.display === "flex") {
|
if (e.key === "Escape" && bmEl("bm-form-wrap")?.style.display === "flex") {
|
||||||
bmCloseForm();
|
bmCloseForm();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
document.getElementById("bm-select-all").addEventListener("change", (e) => {
|
bmEl("bm-select-all").addEventListener("change", (e) => {
|
||||||
const checked = e.target.checked;
|
const checked = e.currentTarget.checked;
|
||||||
document.querySelectorAll(".bm-row-sel").forEach((cb) => {
|
document.querySelectorAll(".bm-row-sel").forEach((cb) => {
|
||||||
cb.checked = checked;
|
cb.checked = checked;
|
||||||
if (checked) bmSelected.add(cb.dataset.bmId);
|
const id = cb.dataset.bmId;
|
||||||
else bmSelected.delete(cb.dataset.bmId);
|
if (!id) return;
|
||||||
|
if (checked) bmSelected.add(id);
|
||||||
|
else bmSelected.delete(id);
|
||||||
});
|
});
|
||||||
bmUpdateSelectionUi();
|
bmUpdateSelectionUi();
|
||||||
});
|
});
|
||||||
document.getElementById("bm-select-all-btn").addEventListener("click", () => {
|
bmEl("bm-select-all-btn").addEventListener("click", () => {
|
||||||
const allSelected = bmFilteredList.length > 0 && bmFilteredList.every((bm) => bmSelected.has(bm.id));
|
const allSelected = bmFilteredList.length > 0 && bmFilteredList.every((bm) => bmSelected.has(bm.id));
|
||||||
if (allSelected) {
|
if (allSelected) {
|
||||||
bmSelected.clear();
|
bmSelected.clear();
|
||||||
@@ -610,22 +635,26 @@ function bmPopulateScopePicker() {
|
|||||||
bmFilteredList.forEach((bm) => bmSelected.add(bm.id));
|
bmFilteredList.forEach((bm) => bmSelected.add(bm.id));
|
||||||
}
|
}
|
||||||
document.querySelectorAll(".bm-row-sel").forEach((cb) => {
|
document.querySelectorAll(".bm-row-sel").forEach((cb) => {
|
||||||
cb.checked = bmSelected.has(cb.dataset.bmId);
|
cb.checked = !!cb.dataset.bmId && bmSelected.has(cb.dataset.bmId);
|
||||||
});
|
});
|
||||||
bmSyncSelectAllCheckbox();
|
bmSyncSelectAllCheckbox();
|
||||||
bmUpdateSelectionUi();
|
bmUpdateSelectionUi();
|
||||||
});
|
});
|
||||||
document.getElementById("bm-del-selected-btn").addEventListener("click", () => {
|
bmEl("bm-del-selected-btn").addEventListener("click", () => {
|
||||||
bmDeleteSelected();
|
void bmDeleteSelected();
|
||||||
});
|
});
|
||||||
document.getElementById("bm-move-selected-btn").addEventListener("click", () => {
|
bmEl("bm-move-selected-btn").addEventListener("click", () => {
|
||||||
bmMoveSelected();
|
void bmMoveSelected();
|
||||||
});
|
});
|
||||||
document.getElementById("bm-tbody").addEventListener("click", async (e) => {
|
bmEl("bm-tbody").addEventListener("click", (e) => {
|
||||||
|
void (async () => {
|
||||||
|
if (!(e.target instanceof Element)) return;
|
||||||
const checkbox = e.target.closest(".bm-row-sel");
|
const checkbox = e.target.closest(".bm-row-sel");
|
||||||
if (checkbox) {
|
if (checkbox) {
|
||||||
if (checkbox.checked) bmSelected.add(checkbox.dataset.bmId);
|
const id = checkbox.dataset.bmId;
|
||||||
else bmSelected.delete(checkbox.dataset.bmId);
|
if (!id) return;
|
||||||
|
if (checkbox.checked) bmSelected.add(id);
|
||||||
|
else bmSelected.delete(id);
|
||||||
bmSyncSelectAllCheckbox();
|
bmSyncSelectAllCheckbox();
|
||||||
bmUpdateSelectionUi();
|
bmUpdateSelectionUi();
|
||||||
return;
|
return;
|
||||||
@@ -635,13 +664,16 @@ function bmPopulateScopePicker() {
|
|||||||
const delBtn = e.target.closest(".bm-del-btn");
|
const delBtn = e.target.closest(".bm-del-btn");
|
||||||
if (tuneBtn) {
|
if (tuneBtn) {
|
||||||
const bm = bmList.find((b) => b.id === tuneBtn.dataset.bmId);
|
const bm = bmList.find((b) => b.id === tuneBtn.dataset.bmId);
|
||||||
if (bm) await bmApply(bm);
|
if (bm) bmApply(bm);
|
||||||
} else if (editBtn) {
|
} else if (editBtn) {
|
||||||
const bm = bmList.find((b) => b.id === editBtn.dataset.bmId);
|
const bm = bmList.find((b) => b.id === editBtn.dataset.bmId);
|
||||||
if (bm) bmOpenForm(bm);
|
if (bm) bmOpenForm(bm);
|
||||||
} else if (delBtn) {
|
} else if (delBtn) {
|
||||||
await bmDelete(delBtn.dataset.bmId);
|
const id = delBtn.dataset.bmId;
|
||||||
|
if (id) await bmDelete(id);
|
||||||
}
|
}
|
||||||
|
})();
|
||||||
});
|
});
|
||||||
bmFetch("");
|
void bmFetch("");
|
||||||
|
})();
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const pluginGroups = {
|
|||||||
};
|
};
|
||||||
const loaded = /* @__PURE__ */ new Set();
|
const loaded = /* @__PURE__ */ new Set();
|
||||||
const loading = /* @__PURE__ */ new Map();
|
const loading = /* @__PURE__ */ new Map();
|
||||||
const modulePlugins = /* @__PURE__ */ new Set(["/ft8.js", "/ft4.js", "/ft2.js", "/wspr.js", "/cw.js", "/vdes.js", "/wefax.js", "/background-decode.js", "/ais.js", "/aprs.js", "/hf-aprs.js", "/sat.js", "/sat-scheduler.js", "/vchan.js"]);
|
const modulePlugins = /* @__PURE__ */ new Set(["/ft8.js", "/ft4.js", "/ft2.js", "/wspr.js", "/cw.js", "/vdes.js", "/wefax.js", "/background-decode.js", "/ais.js", "/aprs.js", "/hf-aprs.js", "/sat.js", "/sat-scheduler.js", "/vchan.js", "/bookmarks.js"]);
|
||||||
function loadLegacyScript(path) {
|
function loadLegacyScript(path) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const script = document.createElement("script");
|
const script = document.createElement("script");
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ await build({
|
|||||||
"plugin-runtime": path.join(sourceDir, "plugin-runtime.ts"),
|
"plugin-runtime": path.join(sourceDir, "plugin-runtime.ts"),
|
||||||
screenshot: path.join(sourceDir, "screenshot.ts"),
|
screenshot: path.join(sourceDir, "screenshot.ts"),
|
||||||
"webgl-renderer": path.join(sourceDir, "webgl-renderer.ts"),
|
"webgl-renderer": path.join(sourceDir, "webgl-renderer.ts"),
|
||||||
bookmarks: path.join(sourceDir, "plugins", "bookmarks.js"),
|
|
||||||
scheduler: path.join(sourceDir, "plugins", "scheduler.js"),
|
scheduler: path.join(sourceDir, "plugins", "scheduler.js"),
|
||||||
},
|
},
|
||||||
outdir: outputDir,
|
outdir: outputDir,
|
||||||
@@ -52,6 +51,7 @@ await build({
|
|||||||
sat: path.join(sourceDir, "plugins", "sat.ts"),
|
sat: path.join(sourceDir, "plugins", "sat.ts"),
|
||||||
"sat-scheduler": path.join(sourceDir, "plugins", "sat-scheduler.ts"),
|
"sat-scheduler": path.join(sourceDir, "plugins", "sat-scheduler.ts"),
|
||||||
vchan: path.join(sourceDir, "plugins", "vchan.ts"),
|
vchan: path.join(sourceDir, "plugins", "vchan.ts"),
|
||||||
|
bookmarks: path.join(sourceDir, "plugins", "bookmarks.ts"),
|
||||||
},
|
},
|
||||||
outdir: outputDir,
|
outdir: outputDir,
|
||||||
bundle: true,
|
bundle: true,
|
||||||
|
|||||||
@@ -912,12 +912,13 @@ function setTheme(theme) {
|
|||||||
|
|
||||||
// Recolour bookmark chips after any palette/theme change (setTheme or setStyle).
|
// Recolour bookmark chips after any palette/theme change (setTheme or setStyle).
|
||||||
function invalidateBookmarkColors() {
|
function invalidateBookmarkColors() {
|
||||||
if (typeof bmOverlayRevision === "undefined") return;
|
const bookmarks = window.trx.modules.bookmarks;
|
||||||
bmOverlayRevision++;
|
if (!bookmarks) return;
|
||||||
|
bookmarks.invalidateColors();
|
||||||
// Force the browser to recalculate styles so getComputedStyle reads new values.
|
// Force the browser to recalculate styles so getComputedStyle reads new values.
|
||||||
void getComputedStyle(document.documentElement).getPropertyValue("--bg");
|
void getComputedStyle(document.documentElement).getPropertyValue("--bg");
|
||||||
const colorMap = bmCategoryColorMap();
|
const colorMap = bmCategoryColorMap();
|
||||||
const ref = typeof bmOverlayList !== "undefined" ? bmOverlayList : [];
|
const ref = bookmarks.overlayList;
|
||||||
document.querySelectorAll(".spectrum-bookmark-chip").forEach((chip) => {
|
document.querySelectorAll(".spectrum-bookmark-chip").forEach((chip) => {
|
||||||
const bm = ref.find((b) => b.id === chip.dataset.bmId);
|
const bm = ref.find((b) => b.id === chip.dataset.bmId);
|
||||||
if (!bm) return;
|
if (!bm) return;
|
||||||
@@ -1482,7 +1483,7 @@ function drawSignalOverlay() {
|
|||||||
const bwEdge = BW_OVERLAY_COLORS.edge;
|
const bwEdge = BW_OVERLAY_COLORS.edge;
|
||||||
const bwStroke = BW_OVERLAY_COLORS.stroke;
|
const bwStroke = BW_OVERLAY_COLORS.stroke;
|
||||||
const bwHard = BW_OVERLAY_COLORS.hard;
|
const bwHard = BW_OVERLAY_COLORS.hard;
|
||||||
const bmRef = typeof bmOverlayList !== "undefined" ? bmOverlayList : null;
|
const bmRef = window.trx.modules.bookmarks?.overlayList ?? null;
|
||||||
if (Array.isArray(bmRef) && bmRef.length > 0) {
|
if (Array.isArray(bmRef) && bmRef.length > 0) {
|
||||||
const colorMap = bmCategoryColorMap();
|
const colorMap = bmCategoryColorMap();
|
||||||
const grouped = new Map();
|
const grouped = new Map();
|
||||||
@@ -4973,7 +4974,7 @@ function buildBookmarkTooltipText(bm) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function nearestBookmarkForHz(hz, widthPx, range) {
|
function nearestBookmarkForHz(hz, widthPx, range) {
|
||||||
const ref = typeof bmOverlayList !== "undefined" ? bmOverlayList : null;
|
const ref = window.trx.modules.bookmarks?.overlayList ?? null;
|
||||||
if (!Array.isArray(ref) || !Number.isFinite(hz) || !widthPx || !range || !Number.isFinite(range.visSpanHz) || range.visSpanHz <= 0) {
|
if (!Array.isArray(ref) || !Number.isFinite(hz) || !widthPx || !range || !Number.isFinite(range.visSpanHz) || range.visSpanHz <= 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -7753,7 +7754,7 @@ function bmThemePalette() {
|
|||||||
|
|
||||||
// Returns a map of category → hex colour, including "" for uncategorised.
|
// Returns a map of category → hex colour, including "" for uncategorised.
|
||||||
function bmCategoryColorMap() {
|
function bmCategoryColorMap() {
|
||||||
const ref = typeof bmOverlayList !== "undefined" ? bmOverlayList : [];
|
const ref = window.trx.modules.bookmarks?.overlayList ?? [];
|
||||||
const cats = [...new Set(ref.map((b) => b.category).filter(Boolean))].sort();
|
const cats = [...new Set(ref.map((b) => b.category).filter(Boolean))].sort();
|
||||||
const palette = bmThemePalette();
|
const palette = bmThemePalette();
|
||||||
const map = { "": palette[0] };
|
const map = { "": palette[0] };
|
||||||
@@ -7797,14 +7798,14 @@ function createBookmarkChip(bm, colorMap, options = {}) {
|
|||||||
span.style.setProperty("--bm-cat-bg", col);
|
span.style.setProperty("--bm-cat-bg", col);
|
||||||
span.style.setProperty("--bm-cat-fg", bmContrastFg(col));
|
span.style.setProperty("--bm-cat-fg", bmContrastFg(col));
|
||||||
span.addEventListener("click", () => {
|
span.addEventListener("click", () => {
|
||||||
if (typeof bmApply === "function") bmApply(bm);
|
void window.trx.modules.bookmarks?.apply(bm);
|
||||||
});
|
});
|
||||||
return span;
|
return span;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSideBookmarkStack(container, bookmarks, colorMap) {
|
function updateSideBookmarkStack(container, bookmarks, colorMap) {
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
const rev = typeof bmOverlayRevision !== "undefined" ? bmOverlayRevision : 0;
|
const rev = window.trx.modules.bookmarks?.overlayRevision ?? 0;
|
||||||
const nextKey = Array.isArray(bookmarks) ? `${rev}:${bookmarks.map((bm) => bm.id).join(",")}` : "";
|
const nextKey = Array.isArray(bookmarks) ? `${rev}:${bookmarks.map((bm) => bm.id).join(",")}` : "";
|
||||||
if (!Array.isArray(bookmarks) || bookmarks.length === 0) {
|
if (!Array.isArray(bookmarks) || bookmarks.length === 0) {
|
||||||
if (container.dataset.bmKey) {
|
if (container.dataset.bmKey) {
|
||||||
@@ -7832,7 +7833,7 @@ function updateBookmarkAxis(range) {
|
|||||||
const rightSideEl = document.getElementById("spectrum-bookmark-side-right");
|
const rightSideEl = document.getElementById("spectrum-bookmark-side-right");
|
||||||
if (!axisEl) return;
|
if (!axisEl) return;
|
||||||
|
|
||||||
const _bmRef = typeof bmOverlayList !== "undefined" ? bmOverlayList : null;
|
const _bmRef = window.trx.modules.bookmarks?.overlayList ?? null;
|
||||||
const allBookmarks = Array.isArray(_bmRef) ? _bmRef : [];
|
const allBookmarks = Array.isArray(_bmRef) ? _bmRef : [];
|
||||||
const visBookmarks = allBookmarks.filter((bm) => bm.freq_hz >= range.visLoHz && bm.freq_hz <= range.visHiHz);
|
const visBookmarks = allBookmarks.filter((bm) => bm.freq_hz >= range.visLoHz && bm.freq_hz <= range.visHiHz);
|
||||||
const leftBookmarks = allBookmarks
|
const leftBookmarks = allBookmarks
|
||||||
@@ -7858,7 +7859,7 @@ function updateBookmarkAxis(range) {
|
|||||||
|
|
||||||
// Only rebuild DOM when the set of visible bookmarks changes.
|
// Only rebuild DOM when the set of visible bookmarks changes.
|
||||||
// Positions are always updated to handle pan/zoom smoothly.
|
// Positions are always updated to handle pan/zoom smoothly.
|
||||||
const rev = typeof bmOverlayRevision !== "undefined" ? bmOverlayRevision : 0;
|
const rev = window.trx.modules.bookmarks?.overlayRevision ?? 0;
|
||||||
const newKey = `${rev}:${visBookmarks.map((b) => b.id).join(",")}`;
|
const newKey = `${rev}:${visBookmarks.map((b) => b.id).join(",")}`;
|
||||||
if (axisEl.dataset.bmKey !== newKey) {
|
if (axisEl.dataset.bmKey !== newKey) {
|
||||||
axisEl.dataset.bmKey = newKey;
|
axisEl.dataset.bmKey = newKey;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const pluginGroups: Readonly<Record<PluginGroup, readonly string[]>> = {
|
|||||||
|
|
||||||
const loaded = new Set<string>();
|
const loaded = new Set<string>();
|
||||||
const loading = new Map<string, Promise<void>>();
|
const loading = new Map<string, Promise<void>>();
|
||||||
const modulePlugins = new Set(["/ft8.js", "/ft4.js", "/ft2.js", "/wspr.js", "/cw.js", "/vdes.js", "/wefax.js", "/background-decode.js", "/ais.js", "/aprs.js", "/hf-aprs.js", "/sat.js", "/sat-scheduler.js", "/vchan.js"]);
|
const modulePlugins = new Set(["/ft8.js", "/ft4.js", "/ft2.js", "/wspr.js", "/cw.js", "/vdes.js", "/wefax.js", "/background-decode.js", "/ais.js", "/aprs.js", "/hf-aprs.js", "/sat.js", "/sat-scheduler.js", "/vchan.js", "/bookmarks.js"]);
|
||||||
|
|
||||||
function loadLegacyScript(path: string): Promise<void> {
|
function loadLegacyScript(path: string): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|||||||
+294
-205
@@ -2,38 +2,114 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
export {};
|
||||||
|
|
||||||
|
/* DOM IDs in the server-owned page are required by this feature; bmEl throws
|
||||||
|
* during initialization if that contract is broken. */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
|
||||||
|
|
||||||
|
interface Bookmark {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
freq_hz: number;
|
||||||
|
mode: string;
|
||||||
|
bandwidth_hz?: number | null;
|
||||||
|
locator?: string | null;
|
||||||
|
category?: string | null;
|
||||||
|
comment?: string | null;
|
||||||
|
decoders?: string[];
|
||||||
|
scope?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DecoderDescriptor {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
activation?: string;
|
||||||
|
active_modes?: string[];
|
||||||
|
bookmark_selectable?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BookmarkService {
|
||||||
|
readonly overlayList: readonly Bookmark[];
|
||||||
|
readonly overlayRevision: number;
|
||||||
|
refreshOverlay(): Promise<void>;
|
||||||
|
invalidateColors(): void;
|
||||||
|
apply(bookmark: Bookmark): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BookmarkBridge extends Window {
|
||||||
|
authEnabled?: boolean;
|
||||||
|
authRole?: string | null;
|
||||||
|
lastActiveRigId?: string | null;
|
||||||
|
lastRigIds?: string[];
|
||||||
|
lastRigDisplayNames?: Record<string, string>;
|
||||||
|
lastFreqHz?: number;
|
||||||
|
lastModeName?: string;
|
||||||
|
lastSpectrumData?: unknown;
|
||||||
|
currentBandwidthHz?: number;
|
||||||
|
modeEl?: HTMLSelectElement | null;
|
||||||
|
decoderRegistry?: DecoderDescriptor[];
|
||||||
|
trx?: { modules?: { bookmarks?: BookmarkService } };
|
||||||
|
trxUi: {
|
||||||
|
confirm(options: { title: string; message: string; confirmLabel: string; danger?: boolean }): Promise<boolean>;
|
||||||
|
notify?(message: string, options: { kind: "error" }): void;
|
||||||
|
};
|
||||||
|
syncBookmarkMapLocators?(bookmarks: readonly Bookmark[]): void;
|
||||||
|
scheduleSpectrumDraw?(): void;
|
||||||
|
syncBandwidthInput?(bandwidthHz: number): void;
|
||||||
|
applyLocalTunedFrequency?(frequencyHz: number, force?: boolean): void;
|
||||||
|
vchanTakeSchedulerControl?(): Promise<void>;
|
||||||
|
vchanInterceptMode?(mode: string): Promise<boolean>;
|
||||||
|
vchanInterceptBandwidth?(bandwidthHz: number): Promise<boolean>;
|
||||||
|
setRigFrequency?(frequencyHz: number): Promise<unknown>;
|
||||||
|
postPath(path: string): Promise<unknown>;
|
||||||
|
onDecoderRegistryReady?(callback: () => void): void;
|
||||||
|
_freqOptimisticSeq?: number;
|
||||||
|
_freqOptimisticHz?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type BookmarkElement = HTMLElement & HTMLInputElement & HTMLSelectElement;
|
||||||
|
const bridge = window as unknown as BookmarkBridge;
|
||||||
|
function bmEl(id: string): BookmarkElement {
|
||||||
|
const element = document.getElementById(id);
|
||||||
|
if (!element) throw new Error(`Missing bookmark element #${id}`);
|
||||||
|
return element as BookmarkElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
function errorMessage(error: unknown): string {
|
||||||
|
return error instanceof Error ? error.message : String(error);
|
||||||
|
}
|
||||||
|
|
||||||
// --- Bookmarks Tab ---
|
// --- Bookmarks Tab ---
|
||||||
|
|
||||||
/** Current bookmark scope: "general" or a rig remote name. */
|
/** Current bookmark scope: "general" or a rig remote name. */
|
||||||
let bmScope = "general";
|
let bmScope = "general";
|
||||||
|
|
||||||
/** Build the ?scope= query string for a given or current bookmark scope. */
|
/** Build the ?scope= query string for a given or current bookmark scope. */
|
||||||
function bmScopeParam(prefix, scope) {
|
function bmScopeParam(prefix: boolean, scope?: string | null): string {
|
||||||
const sep = prefix ? "&" : "?";
|
const sep = prefix ? "&" : "?";
|
||||||
return sep + "scope=" + encodeURIComponent(scope != null ? scope : bmScope);
|
return sep + "scope=" + encodeURIComponent(scope != null ? scope : bmScope);
|
||||||
}
|
}
|
||||||
|
|
||||||
var bmList = [];
|
let bmList: Bookmark[] = [];
|
||||||
var bmRevision = 0;
|
|
||||||
/** Overlay list: always merged general + active rig bookmarks (for spectrum/map). */
|
/** Overlay list: always merged general + active rig bookmarks (for spectrum/map). */
|
||||||
var bmOverlayList = [];
|
let bmOverlayList: Bookmark[] = [];
|
||||||
var bmOverlayRevision = 0;
|
let bmOverlayRevision = 0;
|
||||||
let bmFilteredList = [];
|
let bmFilteredList: Bookmark[] = [];
|
||||||
let bmEditId = null;
|
let bmEditScope: string | null = null;
|
||||||
let bmEditScope = null;
|
|
||||||
let bmCurrentPage = 1;
|
let bmCurrentPage = 1;
|
||||||
const BM_PAGE_SIZE = 25;
|
const BM_PAGE_SIZE = 25;
|
||||||
const bmSelected = new Set();
|
const bmSelected = new Set<string>();
|
||||||
|
|
||||||
function bmFmtFreq(hz) {
|
function bmFmtFreq(hz: number): string {
|
||||||
if (!Number.isFinite(hz) || hz <= 0) return "--";
|
if (!Number.isFinite(hz) || hz <= 0) return "--";
|
||||||
if (hz >= 1e9) return (hz / 1e9).toFixed(6).replace(/\.?0+$/, "") + "\u202fGHz";
|
if (hz >= 1e9) return (hz / 1e9).toFixed(6).replace(/\.?0+$/, "") + "\u202fGHz";
|
||||||
if (hz >= 1e6) return (hz / 1e6).toFixed(6).replace(/\.?0+$/, "") + "\u202fMHz";
|
if (hz >= 1e6) return (hz / 1e6).toFixed(6).replace(/\.?0+$/, "") + "\u202fMHz";
|
||||||
if (hz >= 1e3) return (hz / 1e3).toFixed(3).replace(/\.?0+$/, "") + "\u202fkHz";
|
if (hz >= 1e3) return (hz / 1e3).toFixed(3).replace(/\.?0+$/, "") + "\u202fkHz";
|
||||||
return hz + "\u202fHz";
|
return `${hz}\u202fHz`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function bmEsc(str) {
|
function bmEsc(str: unknown): string {
|
||||||
const d = document.createElement("div");
|
const d = document.createElement("div");
|
||||||
d.appendChild(document.createTextNode(String(str)));
|
d.appendChild(document.createTextNode(String(str)));
|
||||||
return d.innerHTML;
|
return d.innerHTML;
|
||||||
@@ -41,23 +117,23 @@ function bmEsc(str) {
|
|||||||
|
|
||||||
function bmCanControl() {
|
function bmCanControl() {
|
||||||
return (
|
return (
|
||||||
(typeof authEnabled !== "undefined" && !authEnabled) ||
|
(typeof bridge.authEnabled !== "undefined" && !bridge.authEnabled) ||
|
||||||
(typeof authRole !== "undefined" && authRole === "control")
|
(typeof bridge.authRole !== "undefined" && bridge.authRole === "control")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show/hide the Add Bookmark / Select All buttons based on the current auth role.
|
// Show/hide the Add Bookmark / Select All buttons based on the current auth role.
|
||||||
function bmSyncAccess() {
|
function bmSyncAccess() {
|
||||||
const canCtrl = bmCanControl();
|
const canCtrl = bmCanControl();
|
||||||
const addBtn = document.getElementById("bm-add-btn");
|
const addBtn = bmEl("bm-add-btn");
|
||||||
const selectAllBtn = document.getElementById("bm-select-all-btn");
|
const selectAllBtn = bmEl("bm-select-all-btn");
|
||||||
if (addBtn) addBtn.style.display = canCtrl ? "" : "none";
|
if (addBtn) addBtn.style.display = canCtrl ? "" : "none";
|
||||||
if (selectAllBtn) selectAllBtn.style.display = canCtrl ? "" : "none";
|
if (selectAllBtn) selectAllBtn.style.display = canCtrl ? "" : "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The listing scope: always the active rig (to merge general + rig bookmarks). */
|
/** The listing scope: always the active rig (to merge general + rig bookmarks). */
|
||||||
function bmListScope() {
|
function bmListScope() {
|
||||||
const rig = (typeof lastActiveRigId !== "undefined") ? lastActiveRigId : null;
|
const rig = (typeof bridge.lastActiveRigId !== "undefined") ? bridge.lastActiveRigId : null;
|
||||||
return rig || "general";
|
return rig || "general";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,20 +141,20 @@ async function bmFetchOverlay() {
|
|||||||
const overlayScope = bmListScope();
|
const overlayScope = bmListScope();
|
||||||
try {
|
try {
|
||||||
const resp = await fetch("/bookmarks" + bmScopeParam(false, overlayScope));
|
const resp = await fetch("/bookmarks" + bmScopeParam(false, overlayScope));
|
||||||
if (!resp.ok) throw new Error("HTTP " + resp.status);
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||||
bmOverlayList = await resp.json();
|
bmOverlayList = await resp.json() as Bookmark[];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to fetch overlay bookmarks:", e);
|
console.error("Failed to fetch overlay bookmarks:", e);
|
||||||
bmOverlayList = [];
|
bmOverlayList = [];
|
||||||
}
|
}
|
||||||
bmOverlayRevision++;
|
bmOverlayRevision++;
|
||||||
if (typeof window.syncBookmarkMapLocators === "function") {
|
if (typeof bridge.syncBookmarkMapLocators === "function") {
|
||||||
window.syncBookmarkMapLocators(bmOverlayList);
|
bridge.syncBookmarkMapLocators(bmOverlayList);
|
||||||
}
|
}
|
||||||
if (typeof scheduleSpectrumDraw === "function") scheduleSpectrumDraw();
|
if (typeof bridge.scheduleSpectrumDraw === "function") bridge.scheduleSpectrumDraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function bmFetch(categoryFilter) {
|
async function bmFetch(categoryFilter: string): Promise<void> {
|
||||||
let url = "/bookmarks";
|
let url = "/bookmarks";
|
||||||
let hasQuery = false;
|
let hasQuery = false;
|
||||||
if (categoryFilter && categoryFilter !== "") {
|
if (categoryFilter && categoryFilter !== "") {
|
||||||
@@ -89,26 +165,25 @@ async function bmFetch(categoryFilter) {
|
|||||||
const overlayPromise = bmFetchOverlay();
|
const overlayPromise = bmFetchOverlay();
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(url);
|
const resp = await fetch(url);
|
||||||
if (!resp.ok) throw new Error("HTTP " + resp.status);
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||||
bmList = await resp.json();
|
bmList = await resp.json() as Bookmark[];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to fetch bookmarks:", e);
|
console.error("Failed to fetch bookmarks:", e);
|
||||||
bmList = [];
|
bmList = [];
|
||||||
}
|
}
|
||||||
bmRevision++;
|
|
||||||
bmSelected.clear();
|
bmSelected.clear();
|
||||||
bmUpdateSelectionUi();
|
bmUpdateSelectionUi();
|
||||||
bmSyncAccess();
|
bmSyncAccess();
|
||||||
bmApplyFilters();
|
bmApplyFilters();
|
||||||
bmRefreshCategoryFilter(categoryFilter);
|
void bmRefreshCategoryFilter(categoryFilter);
|
||||||
await overlayPromise;
|
await overlayPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
function bmApplyFilters() {
|
function bmApplyFilters() {
|
||||||
const text = (document.getElementById("bm-text-filter")?.value || "").trim().toLowerCase();
|
const text = (bmEl("bm-text-filter")?.value || "").trim().toLowerCase();
|
||||||
const modeFilter = (document.getElementById("bm-mode-filter")?.value || "").trim().toUpperCase();
|
const modeFilter = (bmEl("bm-mode-filter")?.value || "").trim().toUpperCase();
|
||||||
let filtered = modeFilter
|
let filtered = modeFilter
|
||||||
? bmList.filter((bm) => String(bm.mode || "").toUpperCase() === modeFilter)
|
? bmList.filter((bm) => (bm.mode || "").toUpperCase() === modeFilter)
|
||||||
: bmList;
|
: bmList;
|
||||||
filtered = text
|
filtered = text
|
||||||
? filtered.filter((bm) =>
|
? filtered.filter((bm) =>
|
||||||
@@ -123,14 +198,14 @@ function bmApplyFilters() {
|
|||||||
bmRender(filtered);
|
bmRender(filtered);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function bmRefreshCategoryFilter(keepValue) {
|
async function bmRefreshCategoryFilter(keepValue: string): Promise<void> {
|
||||||
const sel = document.getElementById("bm-category-filter");
|
const sel = bmEl("bm-category-filter");
|
||||||
const modeSel = document.getElementById("bm-mode-filter");
|
const modeSel = bmEl("bm-mode-filter");
|
||||||
if (!sel && !modeSel) return;
|
if (!sel && !modeSel) return;
|
||||||
try {
|
try {
|
||||||
const resp = await fetch("/bookmarks" + bmScopeParam(false));
|
const resp = await fetch("/bookmarks" + bmScopeParam(false));
|
||||||
if (!resp.ok) return;
|
if (!resp.ok) return;
|
||||||
const all = await resp.json();
|
const all = await resp.json() as Bookmark[];
|
||||||
if (sel) {
|
if (sel) {
|
||||||
const cats = [...new Set(all.map((b) => b.category || "").filter(Boolean))].sort();
|
const cats = [...new Set(all.map((b) => b.category || "").filter(Boolean))].sort();
|
||||||
while (sel.options.length > 1) sel.remove(1);
|
while (sel.options.length > 1) sel.remove(1);
|
||||||
@@ -144,7 +219,7 @@ async function bmRefreshCategoryFilter(keepValue) {
|
|||||||
}
|
}
|
||||||
if (modeSel) {
|
if (modeSel) {
|
||||||
const keepMode = modeSel.value;
|
const keepMode = modeSel.value;
|
||||||
const modes = [...new Set(all.map((b) => String(b.mode || "").trim().toUpperCase()).filter(Boolean))].sort();
|
const modes = [...new Set(all.map((b) => (b.mode || "").trim().toUpperCase()).filter(Boolean))].sort();
|
||||||
while (modeSel.options.length > 1) modeSel.remove(1);
|
while (modeSel.options.length > 1) modeSel.remove(1);
|
||||||
modes.forEach((mode) => {
|
modes.forEach((mode) => {
|
||||||
const opt = document.createElement("option");
|
const opt = document.createElement("option");
|
||||||
@@ -154,17 +229,17 @@ async function bmRefreshCategoryFilter(keepValue) {
|
|||||||
});
|
});
|
||||||
if (keepMode && modes.includes(keepMode)) modeSel.value = keepMode;
|
if (keepMode && modes.includes(keepMode)) modeSel.value = keepMode;
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
function bmRender(list) {
|
function bmRender(list: Bookmark[]): void {
|
||||||
const tbody = document.getElementById("bm-tbody");
|
const tbody = bmEl("bm-tbody");
|
||||||
const emptyEl = document.getElementById("bm-empty");
|
const emptyEl = bmEl("bm-empty");
|
||||||
const paginatorEl = document.getElementById("bm-paginator");
|
const paginatorEl = bmEl("bm-paginator");
|
||||||
const pageSummaryEl = document.getElementById("bm-page-summary");
|
const pageSummaryEl = bmEl("bm-page-summary");
|
||||||
const pageIndicatorEl = document.getElementById("bm-page-indicator");
|
const pageIndicatorEl = bmEl("bm-page-indicator");
|
||||||
const prevBtn = document.getElementById("bm-page-prev");
|
const prevBtn = bmEl("bm-page-prev");
|
||||||
const nextBtn = document.getElementById("bm-page-next");
|
const nextBtn = bmEl("bm-page-next");
|
||||||
if (!tbody) return;
|
if (!tbody) return;
|
||||||
tbody.innerHTML = "";
|
tbody.innerHTML = "";
|
||||||
|
|
||||||
@@ -222,7 +297,7 @@ function bmRender(list) {
|
|||||||
if (nextBtn) nextBtn.disabled = page >= totalPages;
|
if (nextBtn) nextBtn.disabled = page >= totalPages;
|
||||||
}
|
}
|
||||||
|
|
||||||
function bmChangePage(delta) {
|
function bmChangePage(delta: number): void {
|
||||||
const totalPages = Math.max(1, Math.ceil(bmFilteredList.length / BM_PAGE_SIZE));
|
const totalPages = Math.max(1, Math.ceil(bmFilteredList.length / BM_PAGE_SIZE));
|
||||||
const nextPage = Math.min(Math.max(bmCurrentPage + delta, 1), totalPages);
|
const nextPage = Math.min(Math.max(bmCurrentPage + delta, 1), totalPages);
|
||||||
if (nextPage === bmCurrentPage) return;
|
if (nextPage === bmCurrentPage) return;
|
||||||
@@ -231,30 +306,30 @@ function bmChangePage(delta) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read decoder checkboxes and return an array of selected decoder names.
|
// Read decoder checkboxes and return an array of selected decoder names.
|
||||||
function bmReadDecoders() {
|
function bmReadDecoders(): string[] {
|
||||||
return (window.decoderRegistry || [])
|
return (bridge.decoderRegistry || [])
|
||||||
.filter(d => d.bookmark_selectable)
|
.filter(d => d.bookmark_selectable)
|
||||||
.filter(d => document.getElementById("bm-dec-" + d.id)?.checked)
|
.filter(d => bmEl("bm-dec-" + d.id)?.checked)
|
||||||
.map(d => d.id);
|
.map(d => d.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set decoder checkboxes to match the given array.
|
// Set decoder checkboxes to match the given array.
|
||||||
function bmWriteDecoders(decoders) {
|
function bmWriteDecoders(decoders: readonly string[]): void {
|
||||||
const set = new Set(decoders || []);
|
const set = new Set(decoders || []);
|
||||||
(window.decoderRegistry || [])
|
(bridge.decoderRegistry || [])
|
||||||
.filter(d => d.bookmark_selectable)
|
.filter(d => d.bookmark_selectable)
|
||||||
.forEach(d => {
|
.forEach(d => {
|
||||||
const el = document.getElementById("bm-dec-" + d.id);
|
const el = bmEl("bm-dec-" + d.id);
|
||||||
if (el) el.checked = set.has(d.id);
|
if (el) el.checked = set.has(d.id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build decoder checkboxes dynamically from the registry.
|
// Build decoder checkboxes dynamically from the registry.
|
||||||
function bmBuildDecoderCheckboxes() {
|
function bmBuildDecoderCheckboxes() {
|
||||||
const container = document.getElementById("bm-decoder-checkboxes");
|
const container = bmEl("bm-decoder-checkboxes");
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
container.innerHTML = "";
|
container.innerHTML = "";
|
||||||
(window.decoderRegistry || [])
|
(bridge.decoderRegistry || [])
|
||||||
.filter(d => d.bookmark_selectable)
|
.filter(d => d.bookmark_selectable)
|
||||||
.forEach(d => {
|
.forEach(d => {
|
||||||
const label = document.createElement("label");
|
const label = document.createElement("label");
|
||||||
@@ -264,79 +339,77 @@ function bmBuildDecoderCheckboxes() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function bmOpenForm(bm) {
|
function bmOpenForm(bm: Bookmark | null): void {
|
||||||
const wrap = document.getElementById("bm-form-wrap");
|
const wrap = bmEl("bm-form-wrap");
|
||||||
if (!wrap) return;
|
if (!wrap) return;
|
||||||
bmEditId = bm ? bm.id : null;
|
|
||||||
bmEditScope = bm ? (bm.scope || bmScope) : null;
|
bmEditScope = bm ? (bm.scope || bmScope) : null;
|
||||||
|
|
||||||
// Rebuild decoder checkboxes from registry (handles race where registry
|
// Rebuild decoder checkboxes from registry (handles race where registry
|
||||||
// loaded after initial build).
|
// loaded after initial build).
|
||||||
bmBuildDecoderCheckboxes();
|
bmBuildDecoderCheckboxes();
|
||||||
|
|
||||||
document.getElementById("bm-id").value = bm ? bm.id : "";
|
bmEl("bm-id").value = bm ? bm.id : "";
|
||||||
document.getElementById("bm-name").value = bm ? bm.name : "";
|
bmEl("bm-name").value = bm ? bm.name : "";
|
||||||
document.getElementById("bm-freq").value = bm ? bm.freq_hz : "";
|
bmEl("bm-freq").value = bm ? String(bm.freq_hz) : "";
|
||||||
document.getElementById("bm-mode").value = bm ? bm.mode : "";
|
bmEl("bm-mode").value = bm ? bm.mode : "";
|
||||||
document.getElementById("bm-bw").value = bm && bm.bandwidth_hz ? bm.bandwidth_hz : "";
|
bmEl("bm-bw").value = bm?.bandwidth_hz ? String(bm.bandwidth_hz) : "";
|
||||||
document.getElementById("bm-locator").value = bm ? (bm.locator || "") : "";
|
bmEl("bm-locator").value = bm ? (bm.locator || "") : "";
|
||||||
document.getElementById("bm-category-input").value = bm ? (bm.category || "") : "";
|
bmEl("bm-category-input").value = bm ? (bm.category || "") : "";
|
||||||
document.getElementById("bm-comment").value = bm ? (bm.comment || "") : "";
|
bmEl("bm-comment").value = bm ? (bm.comment || "") : "";
|
||||||
bmWriteDecoders(bm ? bm.decoders : []);
|
bmWriteDecoders(bm?.decoders ?? []);
|
||||||
document.getElementById("bm-form-title").textContent = bm ? "Edit Bookmark" : "Add Bookmark";
|
bmEl("bm-form-title").textContent = bm ? "Edit Bookmark" : "Add Bookmark";
|
||||||
|
|
||||||
wrap.style.display = "flex";
|
wrap.style.display = "flex";
|
||||||
document.getElementById("bm-name").focus();
|
bmEl("bm-name").focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function bmCloseForm() {
|
function bmCloseForm() {
|
||||||
const wrap = document.getElementById("bm-form-wrap");
|
const wrap = bmEl("bm-form-wrap");
|
||||||
if (wrap) wrap.style.display = "none";
|
if (wrap) wrap.style.display = "none";
|
||||||
bmEditId = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function bmPrefillFromStatus() {
|
function bmPrefillFromStatus() {
|
||||||
// Use globals maintained by app.js (updated by SSE stream)
|
// Use globals maintained by app.js (updated by SSE stream)
|
||||||
if (typeof lastFreqHz === "number" && Number.isFinite(lastFreqHz)) {
|
if (typeof bridge.lastFreqHz === "number" && Number.isFinite(bridge.lastFreqHz)) {
|
||||||
document.getElementById("bm-freq").value = Math.round(lastFreqHz);
|
bmEl("bm-freq").value = String(Math.round(bridge.lastFreqHz));
|
||||||
}
|
}
|
||||||
if (typeof lastModeName === "string" && lastModeName) {
|
if (typeof bridge.lastModeName === "string" && bridge.lastModeName) {
|
||||||
document.getElementById("bm-mode").value = lastModeName;
|
bmEl("bm-mode").value = bridge.lastModeName;
|
||||||
}
|
}
|
||||||
if (typeof currentBandwidthHz === "number" && currentBandwidthHz > 0) {
|
if (typeof bridge.currentBandwidthHz === "number" && bridge.currentBandwidthHz > 0) {
|
||||||
document.getElementById("bm-bw").value = Math.round(currentBandwidthHz);
|
bmEl("bm-bw").value = String(Math.round(bridge.currentBandwidthHz));
|
||||||
}
|
}
|
||||||
// Prefill decoder checkboxes from current toggle button state.
|
// Prefill decoder checkboxes from current toggle button state.
|
||||||
const activeDecoders = (window.decoderRegistry || [])
|
const activeDecoders = (bridge.decoderRegistry || [])
|
||||||
.filter(d => d.bookmark_selectable && d.activation === "toggle")
|
.filter(d => d.bookmark_selectable && d.activation === "toggle")
|
||||||
.filter(d => {
|
.filter(d => {
|
||||||
const btn = document.getElementById(d.id + "-decode-toggle-btn");
|
const btn = bmEl(d.id + "-decode-toggle-btn");
|
||||||
return btn && btn.dataset.enabled === "true";
|
return btn && btn.dataset.enabled === "true";
|
||||||
})
|
})
|
||||||
.map(d => d.id);
|
.map(d => d.id);
|
||||||
bmWriteDecoders(activeDecoders);
|
bmWriteDecoders(activeDecoders);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function bmSave(e) {
|
async function bmSave(e: Event): Promise<void> {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const id = document.getElementById("bm-id").value;
|
const id = bmEl("bm-id").value;
|
||||||
const name = document.getElementById("bm-name").value.trim();
|
const name = bmEl("bm-name").value.trim();
|
||||||
const freqStr = document.getElementById("bm-freq").value;
|
const freqStr = bmEl("bm-freq").value;
|
||||||
const freq_hz = parseInt(freqStr, 10);
|
const freq_hz = parseInt(freqStr, 10);
|
||||||
const mode = document.getElementById("bm-mode").value.trim();
|
const mode = bmEl("bm-mode").value.trim();
|
||||||
const bwStr = document.getElementById("bm-bw").value;
|
const bwStr = bmEl("bm-bw").value;
|
||||||
const bandwidth_hz = bwStr ? parseInt(bwStr, 10) : null;
|
const bandwidth_hz = bwStr ? parseInt(bwStr, 10) : null;
|
||||||
const locator = document.getElementById("bm-locator").value.trim().toUpperCase();
|
const locator = bmEl("bm-locator").value.trim().toUpperCase();
|
||||||
const category = document.getElementById("bm-category-input").value.trim();
|
const category = bmEl("bm-category-input").value.trim();
|
||||||
const comment = document.getElementById("bm-comment").value.trim();
|
const comment = bmEl("bm-comment").value.trim();
|
||||||
const decoders = bmReadDecoders();
|
const decoders = bmReadDecoders();
|
||||||
|
|
||||||
const formError = document.getElementById("bm-form-error");
|
const formError = bmEl("bm-form-error");
|
||||||
if (formError) formError.textContent = "";
|
if (formError) formError.textContent = "";
|
||||||
if (!name || !Number.isFinite(freq_hz) || !mode) {
|
if (!name || !Number.isFinite(freq_hz) || !mode) {
|
||||||
if (formError) formError.textContent = "Enter a name, a valid frequency, and a mode.";
|
if (formError) formError.textContent = "Enter a name, a valid frequency, and a mode.";
|
||||||
const invalid = !name ? document.getElementById("bm-name")
|
const invalid = !name ? bmEl("bm-name")
|
||||||
: !Number.isFinite(freq_hz) ? document.getElementById("bm-freq") : document.getElementById("bm-mode");
|
: !Number.isFinite(freq_hz) ? bmEl("bm-freq") : bmEl("bm-mode");
|
||||||
invalid?.focus();
|
invalid?.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -372,90 +445,90 @@ async function bmSave(e) {
|
|||||||
if (resp.status === 409) {
|
if (resp.status === 409) {
|
||||||
throw new Error("A bookmark for that frequency already exists.");
|
throw new Error("A bookmark for that frequency already exists.");
|
||||||
}
|
}
|
||||||
throw new Error(text || "HTTP " + resp.status);
|
throw new Error(text || `HTTP ${resp.status}`);
|
||||||
}
|
}
|
||||||
bmCloseForm();
|
bmCloseForm();
|
||||||
await bmFetch(document.getElementById("bm-category-filter").value);
|
await bmFetch(bmEl("bm-category-filter").value);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to save bookmark:", err);
|
console.error("Failed to save bookmark:", err);
|
||||||
if (formError) formError.textContent = "Failed to save bookmark: " + err.message;
|
if (formError) formError.textContent = "Failed to save bookmark: " + errorMessage(err);
|
||||||
window.trxUi?.notify("Bookmark could not be saved", { kind: "error" });
|
bridge.trxUi.notify?.("Bookmark could not be saved", { kind: "error" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function bmDelete(id) {
|
async function bmDelete(id: string): Promise<void> {
|
||||||
if (!await window.trxUi.confirm({ title: "Delete bookmark?", message: "This bookmark will be permanently removed.", confirmLabel: "Delete" })) return;
|
if (!await bridge.trxUi.confirm({ title: "Delete bookmark?", message: "This bookmark will be permanently removed.", confirmLabel: "Delete" })) return;
|
||||||
const bm = bmList.find((b) => b.id === id);
|
const bm = bmList.find((b) => b.id === id);
|
||||||
const scope = bm ? bm.scope : undefined;
|
const scope = bm ? bm.scope : undefined;
|
||||||
try {
|
try {
|
||||||
const resp = await fetch("/bookmarks/" + encodeURIComponent(id) + bmScopeParam(false, scope), {
|
const resp = await fetch("/bookmarks/" + encodeURIComponent(id) + bmScopeParam(false, scope), {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
});
|
});
|
||||||
if (!resp.ok) throw new Error("HTTP " + resp.status);
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||||
await bmFetch(document.getElementById("bm-category-filter").value);
|
await bmFetch(bmEl("bm-category-filter").value);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to delete bookmark:", err);
|
console.error("Failed to delete bookmark:", err);
|
||||||
window.trxUi?.notify("Failed to delete bookmark: " + err.message, { kind: "error" });
|
bridge.trxUi.notify?.("Failed to delete bookmark: " + errorMessage(err), { kind: "error" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function bmApply(bm) {
|
function bmApply(bm: Bookmark): void {
|
||||||
try {
|
try {
|
||||||
// --- Optimistic UI updates (instant, before any network round-trips) ---
|
// --- Optimistic UI updates (instant, before any network round-trips) ---
|
||||||
if (typeof modeEl !== "undefined" && modeEl) {
|
if (typeof bridge.modeEl !== "undefined" && bridge.modeEl) {
|
||||||
modeEl.value = String(bm.mode || "").toUpperCase();
|
bridge.modeEl.value = (bm.mode || "").toUpperCase();
|
||||||
}
|
}
|
||||||
if (bm.bandwidth_hz) {
|
if (bm.bandwidth_hz) {
|
||||||
if (typeof currentBandwidthHz !== "undefined") {
|
if (typeof bridge.currentBandwidthHz !== "undefined") {
|
||||||
currentBandwidthHz = bm.bandwidth_hz;
|
bridge.currentBandwidthHz = bm.bandwidth_hz;
|
||||||
}
|
}
|
||||||
window.currentBandwidthHz = bm.bandwidth_hz;
|
bridge.currentBandwidthHz = bm.bandwidth_hz;
|
||||||
if (typeof syncBandwidthInput === "function") {
|
if (typeof bridge.syncBandwidthInput === "function") {
|
||||||
syncBandwidthInput(bm.bandwidth_hz);
|
bridge.syncBandwidthInput(bm.bandwidth_hz);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (typeof applyLocalTunedFrequency === "function") {
|
if (typeof bridge.applyLocalTunedFrequency === "function") {
|
||||||
// Set optimistic guard before applying so SSE cannot snap back.
|
// Set optimistic guard before applying so SSE cannot snap back.
|
||||||
if (typeof _freqOptimisticSeq !== "undefined") {
|
if (typeof bridge._freqOptimisticSeq !== "undefined") {
|
||||||
++_freqOptimisticSeq;
|
++bridge._freqOptimisticSeq;
|
||||||
_freqOptimisticHz = bm.freq_hz;
|
bridge._freqOptimisticHz = bm.freq_hz;
|
||||||
}
|
}
|
||||||
// Force display so the BW overlay is repositioned even when freq is unchanged.
|
// Force display so the BW overlay is repositioned even when freq is unchanged.
|
||||||
applyLocalTunedFrequency(bm.freq_hz, true);
|
bridge.applyLocalTunedFrequency(bm.freq_hz, true);
|
||||||
}
|
}
|
||||||
if (typeof scheduleSpectrumDraw === "function" && typeof lastSpectrumData !== "undefined" && lastSpectrumData) {
|
if (typeof bridge.scheduleSpectrumDraw === "function" && typeof bridge.lastSpectrumData !== "undefined" && bridge.lastSpectrumData) {
|
||||||
scheduleSpectrumDraw();
|
bridge.scheduleSpectrumDraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Take scheduler control up front, then apply mode before bandwidth so a
|
// Take scheduler control up front, then apply mode before bandwidth so a
|
||||||
// late SetMode cannot revert a saved WFM bookmark bandwidth to 180 kHz.
|
// late SetMode cannot revert a saved WFM bookmark bandwidth to 180 kHz.
|
||||||
const tunePromise = (async () => {
|
const tunePromise = (async () => {
|
||||||
if (typeof vchanTakeSchedulerControl === "function") {
|
if (typeof bridge.vchanTakeSchedulerControl === "function") {
|
||||||
await vchanTakeSchedulerControl();
|
await bridge.vchanTakeSchedulerControl();
|
||||||
}
|
}
|
||||||
|
|
||||||
const onVirtual = typeof vchanInterceptMode === "function"
|
const onVirtual = typeof bridge.vchanInterceptMode === "function"
|
||||||
&& await vchanInterceptMode(bm.mode);
|
&& await bridge.vchanInterceptMode(bm.mode);
|
||||||
if (!onVirtual) {
|
if (!onVirtual) {
|
||||||
await postPath("/set_mode?mode=" + encodeURIComponent(bm.mode));
|
await bridge.postPath("/set_mode?mode=" + encodeURIComponent(bm.mode));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bm.bandwidth_hz) {
|
if (bm.bandwidth_hz) {
|
||||||
const bwHandledByVchan = typeof vchanInterceptBandwidth === "function"
|
const bwHandledByVchan = typeof bridge.vchanInterceptBandwidth === "function"
|
||||||
&& await vchanInterceptBandwidth(bm.bandwidth_hz);
|
&& await bridge.vchanInterceptBandwidth(bm.bandwidth_hz);
|
||||||
if (!bwHandledByVchan) {
|
if (!bwHandledByVchan) {
|
||||||
await postPath("/set_bandwidth?hz=" + bm.bandwidth_hz);
|
await bridge.postPath(`/set_bandwidth?hz=${bm.bandwidth_hz}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// setRigFrequency is wrapped by vchan.js to redirect to the channel API
|
// bridge.setRigFrequency is wrapped by vchan.js to redirect to the channel API
|
||||||
// when on a virtual channel, so this call works correctly in both cases.
|
// when on a virtual channel, so this call works correctly in both cases.
|
||||||
// It also does its own optimistic update (applyLocalTunedFrequency) but
|
// It also does its own optimistic update (bridge.applyLocalTunedFrequency) but
|
||||||
// that's a no-op since we already set the same value above.
|
// that's a no-op since we already set the same value above.
|
||||||
if (typeof setRigFrequency === "function") {
|
if (typeof bridge.setRigFrequency === "function") {
|
||||||
await setRigFrequency(bm.freq_hz);
|
await bridge.setRigFrequency(bm.freq_hz);
|
||||||
} else {
|
} else {
|
||||||
await postPath("/set_freq?hz=" + bm.freq_hz);
|
await bridge.postPath(`/set_freq?hz=${bm.freq_hz}`);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
// Decoder toggles — fire-and-forget.
|
// Decoder toggles — fire-and-forget.
|
||||||
@@ -466,18 +539,18 @@ async function bmApply(bm) {
|
|||||||
// alone.
|
// alone.
|
||||||
const hasDecoders = Array.isArray(bm.decoders) && bm.decoders.length > 0;
|
const hasDecoders = Array.isArray(bm.decoders) && bm.decoders.length > 0;
|
||||||
const modeUp = (bm.mode || "").toUpperCase();
|
const modeUp = (bm.mode || "").toUpperCase();
|
||||||
const allToggleDecoders = (window.decoderRegistry || []).filter(d =>
|
const allToggleDecoders = (bridge.decoderRegistry || []).filter(d =>
|
||||||
d.activation === "toggle"
|
d.activation === "toggle"
|
||||||
);
|
);
|
||||||
const decoderPromise = allToggleDecoders.length ? (async () => {
|
const decoderPromise = allToggleDecoders.length ? (async () => {
|
||||||
let statusUrl = "/status";
|
let statusUrl = "/status";
|
||||||
if (typeof lastActiveRigId !== "undefined" && lastActiveRigId) {
|
if (typeof bridge.lastActiveRigId !== "undefined" && bridge.lastActiveRigId) {
|
||||||
statusUrl += "?remote=" + encodeURIComponent(lastActiveRigId);
|
statusUrl += "?remote=" + encodeURIComponent(bridge.lastActiveRigId);
|
||||||
}
|
}
|
||||||
const statusResp = await fetch(statusUrl);
|
const statusResp = await fetch(statusUrl);
|
||||||
if (!statusResp.ok) return;
|
if (!statusResp.ok) return;
|
||||||
const st = await statusResp.json();
|
const st = await statusResp.json() as Record<string, unknown>;
|
||||||
const toggles = [];
|
const toggles: Promise<unknown>[] = [];
|
||||||
for (const d of allToggleDecoders) {
|
for (const d of allToggleDecoders) {
|
||||||
const statusKey = d.id.replace(/-/g, "_") + "_decode_enabled";
|
const statusKey = d.id.replace(/-/g, "_") + "_decode_enabled";
|
||||||
const currentlyOn = !!st[statusKey];
|
const currentlyOn = !!st[statusKey];
|
||||||
@@ -488,41 +561,51 @@ async function bmApply(bm) {
|
|||||||
// Always disable decoders that don't apply to the new mode.
|
// Always disable decoders that don't apply to the new mode.
|
||||||
wanted = false;
|
wanted = false;
|
||||||
} else if (hasDecoders) {
|
} else if (hasDecoders) {
|
||||||
wanted = bm.decoders.includes(d.id);
|
wanted = bm.decoders?.includes(d.id) ?? false;
|
||||||
} else {
|
} else {
|
||||||
// Mode-compatible and no bookmark selection: leave as-is.
|
// Mode-compatible and no bookmark selection: leave as-is.
|
||||||
wanted = currentlyOn;
|
wanted = currentlyOn;
|
||||||
}
|
}
|
||||||
if (wanted !== currentlyOn) {
|
if (wanted !== currentlyOn) {
|
||||||
toggles.push(postPath("/toggle_" + d.id.replace(/-/g, "_") + "_decode"));
|
toggles.push(bridge.postPath("/toggle_" + d.id.replace(/-/g, "_") + "_decode"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (toggles.length) await Promise.all(toggles);
|
if (toggles.length) await Promise.all(toggles);
|
||||||
})() : Promise.resolve();
|
})() : Promise.resolve();
|
||||||
// Don't await — let the network calls settle in the background.
|
// Don't await — let the network calls settle in the background.
|
||||||
// Errors are logged but don't block the UI.
|
// Errors are logged but don't block the UI.
|
||||||
Promise.all([tunePromise, decoderPromise]).catch(
|
void Promise.all([tunePromise, decoderPromise]).catch((error: unknown) => {
|
||||||
(err) => console.error("Bookmark apply background error:", err)
|
console.error("Bookmark apply background error:", error);
|
||||||
);
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to apply bookmark:", err);
|
console.error("Failed to apply bookmark:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bridge.trx ??= {};
|
||||||
|
bridge.trx.modules ??= {};
|
||||||
|
bridge.trx.modules.bookmarks = {
|
||||||
|
get overlayList() { return bmOverlayList; },
|
||||||
|
get overlayRevision() { return bmOverlayRevision; },
|
||||||
|
refreshOverlay: bmFetchOverlay,
|
||||||
|
invalidateColors() { bmOverlayRevision += 1; },
|
||||||
|
apply: bmApply,
|
||||||
|
};
|
||||||
|
|
||||||
function bmUpdateSelectionUi() {
|
function bmUpdateSelectionUi() {
|
||||||
const count = bmSelected.size;
|
const count = bmSelected.size;
|
||||||
const canCtrl = bmCanControl();
|
const canCtrl = bmCanControl();
|
||||||
const visible = count > 0 && canCtrl;
|
const visible = count > 0 && canCtrl;
|
||||||
const btn = document.getElementById("bm-del-selected-btn");
|
const btn = bmEl("bm-del-selected-btn");
|
||||||
const countEl = document.getElementById("bm-del-selected-count");
|
const countEl = bmEl("bm-del-selected-count");
|
||||||
if (btn) btn.style.display = visible ? "" : "none";
|
if (btn) btn.style.display = visible ? "" : "none";
|
||||||
if (countEl) countEl.textContent = count;
|
if (countEl) countEl.textContent = String(count);
|
||||||
const moveWrap = document.getElementById("bm-move-selected-wrap");
|
const moveWrap = bmEl("bm-move-selected-wrap");
|
||||||
const moveCountEl = document.getElementById("bm-move-selected-count");
|
const moveCountEl = bmEl("bm-move-selected-count");
|
||||||
if (moveWrap) moveWrap.style.display = visible ? "" : "none";
|
if (moveWrap) moveWrap.style.display = visible ? "" : "none";
|
||||||
if (moveCountEl) moveCountEl.textContent = count;
|
if (moveCountEl) moveCountEl.textContent = String(count);
|
||||||
if (visible) bmPopulateMoveTarget();
|
if (visible) bmPopulateMoveTarget();
|
||||||
const selectAllBtn = document.getElementById("bm-select-all-btn");
|
const selectAllBtn = bmEl("bm-select-all-btn");
|
||||||
if (selectAllBtn && bmCanControl()) {
|
if (selectAllBtn && bmCanControl()) {
|
||||||
const allSelected = bmFilteredList.length > 0 && bmFilteredList.every((bm) => bmSelected.has(bm.id));
|
const allSelected = bmFilteredList.length > 0 && bmFilteredList.every((bm) => bmSelected.has(bm.id));
|
||||||
selectAllBtn.textContent = allSelected ? "Deselect All" : "Select All";
|
selectAllBtn.textContent = allSelected ? "Deselect All" : "Select All";
|
||||||
@@ -531,10 +614,10 @@ function bmUpdateSelectionUi() {
|
|||||||
|
|
||||||
/** Populate the move-target dropdown with all scopes except the current one. */
|
/** Populate the move-target dropdown with all scopes except the current one. */
|
||||||
function bmPopulateMoveTarget() {
|
function bmPopulateMoveTarget() {
|
||||||
const sel = document.getElementById("bm-move-target");
|
const sel = bmEl("bm-move-target");
|
||||||
if (!sel) return;
|
if (!sel) return;
|
||||||
const rigIds = (typeof lastRigIds !== "undefined" && Array.isArray(lastRigIds)) ? lastRigIds : [];
|
const rigIds = (typeof bridge.lastRigIds !== "undefined" && Array.isArray(bridge.lastRigIds)) ? bridge.lastRigIds : [];
|
||||||
const displayNames = (typeof lastRigDisplayNames !== "undefined") ? lastRigDisplayNames : {};
|
const displayNames = (typeof bridge.lastRigDisplayNames !== "undefined") ? bridge.lastRigDisplayNames : {};
|
||||||
const prev = sel.value;
|
const prev = sel.value;
|
||||||
sel.innerHTML = "";
|
sel.innerHTML = "";
|
||||||
if (bmScope !== "general") {
|
if (bmScope !== "general") {
|
||||||
@@ -558,10 +641,10 @@ function bmPopulateMoveTarget() {
|
|||||||
async function bmMoveSelected() {
|
async function bmMoveSelected() {
|
||||||
const ids = Array.from(bmSelected);
|
const ids = Array.from(bmSelected);
|
||||||
if (ids.length === 0) return;
|
if (ids.length === 0) return;
|
||||||
const target = document.getElementById("bm-move-target")?.value;
|
const target = bmEl("bm-move-target")?.value;
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
const targetLabel = document.getElementById("bm-move-target")?.selectedOptions[0]?.textContent || target;
|
const targetLabel = bmEl("bm-move-target")?.selectedOptions[0]?.textContent || target;
|
||||||
if (!await window.trxUi.confirm({
|
if (!await bridge.trxUi.confirm({
|
||||||
title: "Move selected bookmarks?",
|
title: "Move selected bookmarks?",
|
||||||
message: `${ids.length} bookmark${ids.length > 1 ? "s" : ""} will move to “${targetLabel}”.`,
|
message: `${ids.length} bookmark${ids.length > 1 ? "s" : ""} will move to “${targetLabel}”.`,
|
||||||
confirmLabel: "Move",
|
confirmLabel: "Move",
|
||||||
@@ -569,7 +652,7 @@ async function bmMoveSelected() {
|
|||||||
})) return;
|
})) return;
|
||||||
try {
|
try {
|
||||||
// Group selected IDs by their owning scope (skip if already in target).
|
// Group selected IDs by their owning scope (skip if already in target).
|
||||||
const byScope = {};
|
const byScope: Record<string, string[]> = {};
|
||||||
for (const id of ids) {
|
for (const id of ids) {
|
||||||
const bm = bmList.find((b) => b.id === id);
|
const bm = bmList.find((b) => b.id === id);
|
||||||
const scope = bm?.scope || bmScope;
|
const scope = bm?.scope || bmScope;
|
||||||
@@ -581,21 +664,21 @@ async function bmMoveSelected() {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ ids: scopeIds, to: target }),
|
body: JSON.stringify({ ids: scopeIds, to: target }),
|
||||||
}).then((r) => { if (!r.ok) throw new Error("HTTP " + r.status); })
|
}).then((r) => { if (!r.ok) throw new Error(`HTTP ${r.status}`); })
|
||||||
));
|
));
|
||||||
bmSelected.clear();
|
bmSelected.clear();
|
||||||
bmUpdateSelectionUi();
|
bmUpdateSelectionUi();
|
||||||
await bmFetch(document.getElementById("bm-category-filter").value);
|
await bmFetch(bmEl("bm-category-filter").value);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to move bookmarks:", err);
|
console.error("Failed to move bookmarks:", err);
|
||||||
window.trxUi?.notify("Failed to move bookmarks: " + err.message, { kind: "error" });
|
bridge.trxUi.notify?.("Failed to move bookmarks: " + errorMessage(err), { kind: "error" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function bmSyncSelectAllCheckbox() {
|
function bmSyncSelectAllCheckbox() {
|
||||||
const selectAll = document.getElementById("bm-select-all");
|
const selectAll = bmEl("bm-select-all");
|
||||||
if (!selectAll) return;
|
if (!selectAll) return;
|
||||||
const checkboxes = document.querySelectorAll(".bm-row-sel");
|
const checkboxes = document.querySelectorAll<HTMLInputElement>(".bm-row-sel");
|
||||||
if (checkboxes.length === 0) {
|
if (checkboxes.length === 0) {
|
||||||
selectAll.checked = false;
|
selectAll.checked = false;
|
||||||
selectAll.indeterminate = false;
|
selectAll.indeterminate = false;
|
||||||
@@ -609,14 +692,14 @@ function bmSyncSelectAllCheckbox() {
|
|||||||
async function bmDeleteSelected() {
|
async function bmDeleteSelected() {
|
||||||
const ids = Array.from(bmSelected);
|
const ids = Array.from(bmSelected);
|
||||||
if (ids.length === 0) return;
|
if (ids.length === 0) return;
|
||||||
if (!await window.trxUi.confirm({
|
if (!await bridge.trxUi.confirm({
|
||||||
title: "Delete selected bookmarks?",
|
title: "Delete selected bookmarks?",
|
||||||
message: `${ids.length} bookmark${ids.length > 1 ? "s" : ""} will be permanently removed.`,
|
message: `${ids.length} bookmark${ids.length > 1 ? "s" : ""} will be permanently removed.`,
|
||||||
confirmLabel: "Delete",
|
confirmLabel: "Delete",
|
||||||
})) return;
|
})) return;
|
||||||
try {
|
try {
|
||||||
// Group selected IDs by their owning scope.
|
// Group selected IDs by their owning scope.
|
||||||
const byScope = {};
|
const byScope: Record<string, string[]> = {};
|
||||||
for (const id of ids) {
|
for (const id of ids) {
|
||||||
const bm = bmList.find((b) => b.id === id);
|
const bm = bmList.find((b) => b.id === id);
|
||||||
const scope = bm?.scope || bmScope;
|
const scope = bm?.scope || bmScope;
|
||||||
@@ -627,23 +710,23 @@ async function bmDeleteSelected() {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ ids: scopeIds }),
|
body: JSON.stringify({ ids: scopeIds }),
|
||||||
}).then((r) => { if (!r.ok) throw new Error("HTTP " + r.status); })
|
}).then((r) => { if (!r.ok) throw new Error(`HTTP ${r.status}`); })
|
||||||
));
|
));
|
||||||
bmSelected.clear();
|
bmSelected.clear();
|
||||||
bmUpdateSelectionUi();
|
bmUpdateSelectionUi();
|
||||||
await bmFetch(document.getElementById("bm-category-filter").value);
|
await bmFetch(bmEl("bm-category-filter").value);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to delete bookmarks:", err);
|
console.error("Failed to delete bookmarks:", err);
|
||||||
window.trxUi?.notify("Failed to delete bookmarks: " + err.message, { kind: "error" });
|
bridge.trxUi.notify?.("Failed to delete bookmarks: " + errorMessage(err), { kind: "error" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Populate the scope picker with "General" + one option per rig. */
|
/** Populate the scope picker with "General" + one option per rig. */
|
||||||
function bmPopulateScopePicker() {
|
function bmPopulateScopePicker() {
|
||||||
const picker = document.getElementById("bm-scope-picker");
|
const picker = bmEl("bm-scope-picker");
|
||||||
if (!picker) return;
|
if (!picker) return;
|
||||||
const rigIds = (typeof lastRigIds !== "undefined" && Array.isArray(lastRigIds)) ? lastRigIds : [];
|
const rigIds = (typeof bridge.lastRigIds !== "undefined" && Array.isArray(bridge.lastRigIds)) ? bridge.lastRigIds : [];
|
||||||
const displayNames = (typeof lastRigDisplayNames !== "undefined") ? lastRigDisplayNames : {};
|
const displayNames = (typeof bridge.lastRigDisplayNames !== "undefined") ? bridge.lastRigDisplayNames : {};
|
||||||
// Preserve current selection if still valid.
|
// Preserve current selection if still valid.
|
||||||
const prev = picker.value;
|
const prev = picker.value;
|
||||||
while (picker.options.length > 1) picker.remove(1);
|
while (picker.options.length > 1) picker.remove(1);
|
||||||
@@ -670,63 +753,63 @@ function bmPopulateScopePicker() {
|
|||||||
// Build decoder checkboxes from registry. The registry is fetched async
|
// Build decoder checkboxes from registry. The registry is fetched async
|
||||||
// so we rebuild once it arrives to ensure checkboxes are present.
|
// so we rebuild once it arrives to ensure checkboxes are present.
|
||||||
bmBuildDecoderCheckboxes();
|
bmBuildDecoderCheckboxes();
|
||||||
if (typeof window.onDecoderRegistryReady === "function") {
|
if (typeof bridge.onDecoderRegistryReady === "function") {
|
||||||
window.onDecoderRegistryReady(bmBuildDecoderCheckboxes);
|
bridge.onDecoderRegistryReady(bmBuildDecoderCheckboxes);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scope picker
|
// Scope picker
|
||||||
bmPopulateScopePicker();
|
bmPopulateScopePicker();
|
||||||
const scopePicker = document.getElementById("bm-scope-picker");
|
const scopePicker = bmEl("bm-scope-picker");
|
||||||
if (scopePicker) {
|
if (scopePicker) {
|
||||||
scopePicker.addEventListener("change", (e) => {
|
scopePicker.addEventListener("change", (e) => {
|
||||||
bmScope = e.target.value;
|
bmScope = (e.currentTarget as HTMLSelectElement).value;
|
||||||
bmFetch(document.getElementById("bm-category-filter")?.value || "");
|
void bmFetch(bmEl("bm-category-filter").value || "");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh list and sync access when the Bookmarks tab is activated
|
// Refresh list and sync access when the Bookmarks tab is activated
|
||||||
document.querySelector(".tab-bar").addEventListener("click", (e) => {
|
document.querySelector(".tab-bar")?.addEventListener("click", (e) => {
|
||||||
const btn = e.target.closest('.tab[data-tab="bookmarks"]');
|
const btn = e.target instanceof Element ? e.target.closest('.tab[data-tab="bookmarks"]') : null;
|
||||||
if (!btn) return;
|
if (!btn) return;
|
||||||
bmFetch(document.getElementById("bm-category-filter").value);
|
void bmFetch(bmEl("bm-category-filter").value);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add Bookmark button — open form and prefill from current rig state
|
// Add Bookmark button — open form and prefill from current rig state
|
||||||
document.getElementById("bm-add-btn").addEventListener("click", () => {
|
bmEl("bm-add-btn").addEventListener("click", () => {
|
||||||
bmOpenForm(null);
|
bmOpenForm(null);
|
||||||
bmPrefillFromStatus();
|
bmPrefillFromStatus();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Category filter dropdown
|
// Category filter dropdown
|
||||||
document.getElementById("bm-category-filter").addEventListener("change", (e) => {
|
bmEl("bm-category-filter").addEventListener("change", (e) => {
|
||||||
bmFetch(e.target.value);
|
void bmFetch((e.currentTarget as HTMLSelectElement).value);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mode filter dropdown (client-side, no re-fetch)
|
// Mode filter dropdown (client-side, no re-fetch)
|
||||||
document.getElementById("bm-mode-filter").addEventListener("change", () => {
|
bmEl("bm-mode-filter").addEventListener("change", () => {
|
||||||
bmApplyFilters();
|
bmApplyFilters();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Text search filter (client-side, no re-fetch)
|
// Text search filter (client-side, no re-fetch)
|
||||||
document.getElementById("bm-text-filter").addEventListener("input", () => {
|
bmEl("bm-text-filter").addEventListener("input", () => {
|
||||||
bmApplyFilters();
|
bmApplyFilters();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById("bm-page-prev").addEventListener("click", () => {
|
bmEl("bm-page-prev").addEventListener("click", () => {
|
||||||
bmChangePage(-1);
|
bmChangePage(-1);
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById("bm-page-next").addEventListener("click", () => {
|
bmEl("bm-page-next").addEventListener("click", () => {
|
||||||
bmChangePage(1);
|
bmChangePage(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Form submit
|
// Form submit
|
||||||
document.getElementById("bm-form").addEventListener("submit", bmSave);
|
bmEl("bm-form").addEventListener("submit", (event) => { void bmSave(event); });
|
||||||
|
|
||||||
// Form cancel
|
// Form cancel
|
||||||
document.getElementById("bm-form-cancel").addEventListener("click", bmCloseForm);
|
bmEl("bm-form-cancel").addEventListener("click", bmCloseForm);
|
||||||
|
|
||||||
const formWrap = document.getElementById("bm-form-wrap");
|
const formWrap = bmEl("bm-form-wrap");
|
||||||
if (formWrap) {
|
if (formWrap) {
|
||||||
formWrap.addEventListener("click", (e) => {
|
formWrap.addEventListener("click", (e) => {
|
||||||
if (e.target === formWrap) bmCloseForm();
|
if (e.target === formWrap) bmCloseForm();
|
||||||
@@ -734,24 +817,26 @@ function bmPopulateScopePicker() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("keydown", (e) => {
|
document.addEventListener("keydown", (e) => {
|
||||||
if (e.key === "Escape" && document.getElementById("bm-form-wrap")?.style.display === "flex") {
|
if (e.key === "Escape" && bmEl("bm-form-wrap")?.style.display === "flex") {
|
||||||
bmCloseForm();
|
bmCloseForm();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Select-all checkbox
|
// Select-all checkbox
|
||||||
document.getElementById("bm-select-all").addEventListener("change", (e) => {
|
bmEl("bm-select-all").addEventListener("change", (e) => {
|
||||||
const checked = e.target.checked;
|
const checked = (e.currentTarget as HTMLInputElement).checked;
|
||||||
document.querySelectorAll(".bm-row-sel").forEach((cb) => {
|
document.querySelectorAll<HTMLInputElement>(".bm-row-sel").forEach((cb) => {
|
||||||
cb.checked = checked;
|
cb.checked = checked;
|
||||||
if (checked) bmSelected.add(cb.dataset.bmId);
|
const id = cb.dataset.bmId;
|
||||||
else bmSelected.delete(cb.dataset.bmId);
|
if (!id) return;
|
||||||
|
if (checked) bmSelected.add(id);
|
||||||
|
else bmSelected.delete(id);
|
||||||
});
|
});
|
||||||
bmUpdateSelectionUi();
|
bmUpdateSelectionUi();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Select All (across all pages) button
|
// Select All (across all pages) button
|
||||||
document.getElementById("bm-select-all-btn").addEventListener("click", () => {
|
bmEl("bm-select-all-btn").addEventListener("click", () => {
|
||||||
const allSelected = bmFilteredList.length > 0 && bmFilteredList.every((bm) => bmSelected.has(bm.id));
|
const allSelected = bmFilteredList.length > 0 && bmFilteredList.every((bm) => bmSelected.has(bm.id));
|
||||||
if (allSelected) {
|
if (allSelected) {
|
||||||
bmSelected.clear();
|
bmSelected.clear();
|
||||||
@@ -759,49 +844,53 @@ function bmPopulateScopePicker() {
|
|||||||
bmFilteredList.forEach((bm) => bmSelected.add(bm.id));
|
bmFilteredList.forEach((bm) => bmSelected.add(bm.id));
|
||||||
}
|
}
|
||||||
// Sync visible page checkboxes
|
// Sync visible page checkboxes
|
||||||
document.querySelectorAll(".bm-row-sel").forEach((cb) => {
|
document.querySelectorAll<HTMLInputElement>(".bm-row-sel").forEach((cb) => {
|
||||||
cb.checked = bmSelected.has(cb.dataset.bmId);
|
cb.checked = !!cb.dataset.bmId && bmSelected.has(cb.dataset.bmId);
|
||||||
});
|
});
|
||||||
bmSyncSelectAllCheckbox();
|
bmSyncSelectAllCheckbox();
|
||||||
bmUpdateSelectionUi();
|
bmUpdateSelectionUi();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Delete Selected button
|
// Delete Selected button
|
||||||
document.getElementById("bm-del-selected-btn").addEventListener("click", () => {
|
bmEl("bm-del-selected-btn").addEventListener("click", () => {
|
||||||
bmDeleteSelected();
|
void bmDeleteSelected();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Move Selected button
|
// Move Selected button
|
||||||
document.getElementById("bm-move-selected-btn").addEventListener("click", () => {
|
bmEl("bm-move-selected-btn").addEventListener("click", () => {
|
||||||
bmMoveSelected();
|
void bmMoveSelected();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Table action buttons and row checkboxes (event delegation)
|
// Table action buttons and row checkboxes (event delegation)
|
||||||
document.getElementById("bm-tbody").addEventListener("click", async (e) => {
|
bmEl("bm-tbody").addEventListener("click", (e) => { void (async () => {
|
||||||
const checkbox = e.target.closest(".bm-row-sel");
|
if (!(e.target instanceof Element)) return;
|
||||||
|
const checkbox = e.target.closest<HTMLInputElement>(".bm-row-sel");
|
||||||
if (checkbox) {
|
if (checkbox) {
|
||||||
if (checkbox.checked) bmSelected.add(checkbox.dataset.bmId);
|
const id = checkbox.dataset.bmId;
|
||||||
else bmSelected.delete(checkbox.dataset.bmId);
|
if (!id) return;
|
||||||
|
if (checkbox.checked) bmSelected.add(id);
|
||||||
|
else bmSelected.delete(id);
|
||||||
bmSyncSelectAllCheckbox();
|
bmSyncSelectAllCheckbox();
|
||||||
bmUpdateSelectionUi();
|
bmUpdateSelectionUi();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tuneBtn = e.target.closest(".bm-tune-btn");
|
const tuneBtn = e.target.closest<HTMLElement>(".bm-tune-btn");
|
||||||
const editBtn = e.target.closest(".bm-edit-btn");
|
const editBtn = e.target.closest<HTMLElement>(".bm-edit-btn");
|
||||||
const delBtn = e.target.closest(".bm-del-btn");
|
const delBtn = e.target.closest<HTMLElement>(".bm-del-btn");
|
||||||
|
|
||||||
if (tuneBtn) {
|
if (tuneBtn) {
|
||||||
const bm = bmList.find((b) => b.id === tuneBtn.dataset.bmId);
|
const bm = bmList.find((b) => b.id === tuneBtn.dataset.bmId);
|
||||||
if (bm) await bmApply(bm);
|
if (bm) bmApply(bm);
|
||||||
} else if (editBtn) {
|
} else if (editBtn) {
|
||||||
const bm = bmList.find((b) => b.id === editBtn.dataset.bmId);
|
const bm = bmList.find((b) => b.id === editBtn.dataset.bmId);
|
||||||
if (bm) bmOpenForm(bm);
|
if (bm) bmOpenForm(bm);
|
||||||
} else if (delBtn) {
|
} else if (delBtn) {
|
||||||
await bmDelete(delBtn.dataset.bmId);
|
const id = delBtn.dataset.bmId;
|
||||||
|
if (id) await bmDelete(id);
|
||||||
}
|
}
|
||||||
});
|
})(); });
|
||||||
|
|
||||||
// Pre-load bookmarks so spectrum markers are visible immediately.
|
// Pre-load bookmarks so spectrum markers are visible immediately.
|
||||||
bmFetch("");
|
void bmFetch("");
|
||||||
})();
|
})();
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
// 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 test from "node:test";
|
||||||
|
import vm from "node:vm";
|
||||||
|
|
||||||
|
class ElementFixture {
|
||||||
|
constructor() {
|
||||||
|
this.value = "";
|
||||||
|
this.textContent = "";
|
||||||
|
this.innerHTML = "";
|
||||||
|
this.style = {};
|
||||||
|
this.dataset = {};
|
||||||
|
this.options = [];
|
||||||
|
this.selectedOptions = [];
|
||||||
|
this.children = [];
|
||||||
|
}
|
||||||
|
addEventListener() {}
|
||||||
|
appendChild(child) { this.children.push(child); return child; }
|
||||||
|
add(child) { this.options.push(child); }
|
||||||
|
remove(index) { this.options.splice(index, 1); }
|
||||||
|
focus() {}
|
||||||
|
querySelector() { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
test("bookmarks register an explicit typed service for application consumers", async () => {
|
||||||
|
const elements = new Map();
|
||||||
|
const element = (id) => {
|
||||||
|
if (!elements.has(id)) elements.set(id, new ElementFixture());
|
||||||
|
return elements.get(id);
|
||||||
|
};
|
||||||
|
element("bm-scope-picker").value = "general";
|
||||||
|
element("bm-category-filter").options.push({ value: "" });
|
||||||
|
element("bm-mode-filter").options.push({ value: "" });
|
||||||
|
const bookmarks = [{ id: "one", name: "Local", freq_hz: 145_500_000, mode: "FM", scope: "general" }];
|
||||||
|
const window = {
|
||||||
|
trx: { modules: {} },
|
||||||
|
trxUi: { confirm: async () => true },
|
||||||
|
decoderRegistry: [],
|
||||||
|
};
|
||||||
|
const context = vm.createContext({
|
||||||
|
window,
|
||||||
|
document: {
|
||||||
|
getElementById: element,
|
||||||
|
querySelector: () => new ElementFixture(),
|
||||||
|
querySelectorAll: () => [],
|
||||||
|
createElement: () => new ElementFixture(),
|
||||||
|
createTextNode: (text) => ({ textContent: text }),
|
||||||
|
addEventListener() {},
|
||||||
|
},
|
||||||
|
fetch: async () => ({ ok: true, json: async () => bookmarks }),
|
||||||
|
CSS: { escape: (value) => value },
|
||||||
|
Element: ElementFixture,
|
||||||
|
Set,
|
||||||
|
Map,
|
||||||
|
Array,
|
||||||
|
Number,
|
||||||
|
String,
|
||||||
|
Promise,
|
||||||
|
console,
|
||||||
|
});
|
||||||
|
const source = await readFile(new URL("../../assets/web/generated/bookmarks.js", import.meta.url), "utf8");
|
||||||
|
new vm.Script(source).runInContext(context);
|
||||||
|
await new Promise((resolve) => setImmediate(resolve));
|
||||||
|
|
||||||
|
assert.equal(window.trx.modules.bookmarks.overlayList.length, 1);
|
||||||
|
assert.equal(window.trx.modules.bookmarks.overlayList[0].id, "one");
|
||||||
|
assert.equal(typeof window.trx.modules.bookmarks.apply, "function");
|
||||||
|
assert.equal(globalThis.bmOverlayList, undefined);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user