[feat](trx-frontend-http): merge general bookmarks into rig view, fix button styling, improve rig display names
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -1011,7 +1011,10 @@ async function refreshRigList() {
|
||||
if (typeof r.display_name === "string" && r.display_name.length > 0) {
|
||||
displayNames[r.remote] = r.display_name;
|
||||
} else {
|
||||
displayNames[r.remote] = r.remote;
|
||||
const mfg = (r.manufacturer || "").trim();
|
||||
const mdl = (r.model || "").trim();
|
||||
const hw = [mfg, mdl].filter(Boolean).join(" ");
|
||||
displayNames[r.remote] = hw || r.remote;
|
||||
}
|
||||
});
|
||||
serverRigs = rigs;
|
||||
|
||||
@@ -379,11 +379,11 @@
|
||||
</select>
|
||||
<input type="search" id="bm-text-filter" class="status-input" placeholder="Search bookmarks…" aria-label="Search bookmarks" />
|
||||
<button id="bm-add-btn" type="button" class="bm-add-btn" style="display:none;">+ Add Bookmark</button>
|
||||
<button id="bm-select-all-btn" type="button" class="bm-toolbar-btn" style="display:none;">Select All</button>
|
||||
<button id="bm-select-all-btn" type="button" class="bm-add-btn" style="display:none;">Select All</button>
|
||||
<button id="bm-del-selected-btn" type="button" class="bm-del-btn" style="display:none;">Delete Selected (<span id="bm-del-selected-count">0</span>)</button>
|
||||
<span id="bm-move-selected-wrap" class="bm-move-wrap" style="display:none;">
|
||||
<select id="bm-move-target" class="status-input" aria-label="Move destination"></select>
|
||||
<button id="bm-move-selected-btn" type="button" class="bm-toolbar-btn">Move (<span id="bm-move-selected-count">0</span>)</button>
|
||||
<button id="bm-move-selected-btn" type="button" class="bm-add-btn">Move (<span id="bm-move-selected-count">0</span>)</button>
|
||||
</span>
|
||||
</div>
|
||||
<div id="bm-form-wrap" style="display:none;">
|
||||
|
||||
@@ -3,16 +3,17 @@
|
||||
/** Current bookmark scope: "general" or a rig remote name. */
|
||||
let bmScope = "general";
|
||||
|
||||
/** Build the ?scope= query string for the current bookmark scope. */
|
||||
function bmScopeParam(prefix) {
|
||||
/** Build the ?scope= query string for a given or current bookmark scope. */
|
||||
function bmScopeParam(prefix, scope) {
|
||||
const sep = prefix ? "&" : "?";
|
||||
return sep + "scope=" + encodeURIComponent(bmScope);
|
||||
return sep + "scope=" + encodeURIComponent(scope != null ? scope : bmScope);
|
||||
}
|
||||
|
||||
var bmList = [];
|
||||
var bmRevision = 0;
|
||||
let bmFilteredList = [];
|
||||
let bmEditId = null;
|
||||
let bmEditScope = null;
|
||||
let bmCurrentPage = 1;
|
||||
const BM_PAGE_SIZE = 25;
|
||||
const bmSelected = new Set();
|
||||
@@ -154,6 +155,7 @@ function bmRender(list) {
|
||||
const endIndex = Math.min(startIndex + BM_PAGE_SIZE, list.length);
|
||||
const pageItems = list.slice(startIndex, endIndex);
|
||||
|
||||
const showScope = bmScope !== "general";
|
||||
pageItems.forEach((bm) => {
|
||||
const tr = document.createElement("tr");
|
||||
tr.dataset.bmId = bm.id;
|
||||
@@ -163,9 +165,10 @@ function bmRender(list) {
|
||||
const decoderCell = (bm.decoders || []).join(", ").toUpperCase() || "--";
|
||||
const commentCell = bm.comment || "";
|
||||
const checked = bmSelected.has(bm.id) ? " checked" : "";
|
||||
const scopeBadge = showScope && bm.scope === "general" ? ' <span class="bm-scope-badge">G</span>' : "";
|
||||
tr.innerHTML =
|
||||
`<td class="bm-col-sel"><input type="checkbox" class="bm-row-sel" data-bm-id="${bmEsc(bm.id)}"${checked} aria-label="Select ${bmEsc(bm.name)}" /></td>` +
|
||||
`<td class="bm-col-name">${bmEsc(bm.name)}</td>` +
|
||||
`<td class="bm-col-name">${bmEsc(bm.name)}${scopeBadge}</td>` +
|
||||
`<td class="bm-col-freq">${bmFmtFreq(bm.freq_hz)}</td>` +
|
||||
`<td class="bm-col-mode">${bmEsc(bm.mode)}</td>` +
|
||||
`<td class="bm-col-bw">${bwCell}</td>` +
|
||||
@@ -228,6 +231,7 @@ function bmOpenForm(bm) {
|
||||
const wrap = document.getElementById("bm-form-wrap");
|
||||
if (!wrap) return;
|
||||
bmEditId = bm ? bm.id : null;
|
||||
bmEditScope = bm ? (bm.scope || bmScope) : null;
|
||||
|
||||
document.getElementById("bm-id").value = bm ? bm.id : "";
|
||||
document.getElementById("bm-name").value = bm ? bm.name : "";
|
||||
@@ -296,7 +300,7 @@ async function bmSave(e) {
|
||||
try {
|
||||
let resp;
|
||||
if (id) {
|
||||
resp = await fetch("/bookmarks/" + encodeURIComponent(id) + bmScopeParam(false), {
|
||||
resp = await fetch("/bookmarks/" + encodeURIComponent(id) + bmScopeParam(false, bmEditScope), {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
@@ -325,8 +329,10 @@ async function bmSave(e) {
|
||||
|
||||
async function bmDelete(id) {
|
||||
if (!confirm("Delete this bookmark?")) return;
|
||||
const bm = bmList.find((b) => b.id === id);
|
||||
const scope = bm ? bm.scope : undefined;
|
||||
try {
|
||||
const resp = await fetch("/bookmarks/" + encodeURIComponent(id) + bmScopeParam(false), {
|
||||
const resp = await fetch("/bookmarks/" + encodeURIComponent(id) + bmScopeParam(false, scope), {
|
||||
method: "DELETE",
|
||||
});
|
||||
if (!resp.ok) throw new Error("HTTP " + resp.status);
|
||||
@@ -471,12 +477,21 @@ async function bmMoveSelected() {
|
||||
const targetLabel = document.getElementById("bm-move-target")?.selectedOptions[0]?.textContent || target;
|
||||
if (!confirm(`Move ${ids.length} bookmark${ids.length > 1 ? "s" : ""} to "${targetLabel}"?`)) return;
|
||||
try {
|
||||
const resp = await fetch("/bookmarks/batch_move" + bmScopeParam(false), {
|
||||
// Group selected IDs by their owning scope (skip if already in target).
|
||||
const byScope = {};
|
||||
for (const id of ids) {
|
||||
const bm = bmList.find((b) => b.id === id);
|
||||
const scope = bm?.scope || bmScope;
|
||||
if (scope === target) continue;
|
||||
(byScope[scope] ||= []).push(id);
|
||||
}
|
||||
await Promise.all(Object.entries(byScope).map(([scope, scopeIds]) =>
|
||||
fetch("/bookmarks/batch_move" + bmScopeParam(false, scope), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ ids, to: target }),
|
||||
});
|
||||
if (!resp.ok) throw new Error("HTTP " + resp.status);
|
||||
body: JSON.stringify({ ids: scopeIds, to: target }),
|
||||
}).then((r) => { if (!r.ok) throw new Error("HTTP " + r.status); })
|
||||
));
|
||||
bmSelected.clear();
|
||||
bmUpdateSelectionUi();
|
||||
await bmFetch(document.getElementById("bm-category-filter").value);
|
||||
@@ -505,12 +520,20 @@ async function bmDeleteSelected() {
|
||||
if (ids.length === 0) return;
|
||||
if (!confirm(`Delete ${ids.length} selected bookmark${ids.length > 1 ? "s" : ""}?`)) return;
|
||||
try {
|
||||
const resp = await fetch("/bookmarks/batch_delete" + bmScopeParam(false), {
|
||||
// Group selected IDs by their owning scope.
|
||||
const byScope = {};
|
||||
for (const id of ids) {
|
||||
const bm = bmList.find((b) => b.id === id);
|
||||
const scope = bm?.scope || bmScope;
|
||||
(byScope[scope] ||= []).push(id);
|
||||
}
|
||||
await Promise.all(Object.entries(byScope).map(([scope, scopeIds]) =>
|
||||
fetch("/bookmarks/batch_delete" + bmScopeParam(false, scope), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ ids }),
|
||||
});
|
||||
if (!resp.ok) throw new Error("HTTP " + resp.status);
|
||||
body: JSON.stringify({ ids: scopeIds }),
|
||||
}).then((r) => { if (!r.ok) throw new Error("HTTP " + r.status); })
|
||||
));
|
||||
bmSelected.clear();
|
||||
bmUpdateSelectionUi();
|
||||
await bmFetch(document.getElementById("bm-category-filter").value);
|
||||
|
||||
@@ -3725,17 +3725,19 @@ button:focus-visible, input:focus-visible, select:focus-visible {
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
.bm-toolbar-btn {
|
||||
white-space: nowrap;
|
||||
font-size: 0.78rem;
|
||||
padding: 0.25rem 0.6rem;
|
||||
border-radius: 0.25rem;
|
||||
border: 1px solid var(--border-light);
|
||||
background: var(--btn-bg);
|
||||
color: var(--text-heading);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.bm-scope-badge {
|
||||
display: inline-block;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
padding: 0.05rem 0.3rem;
|
||||
border-radius: 0.2rem;
|
||||
background: var(--btn-bg);
|
||||
border: 1px solid var(--border-light);
|
||||
color: var(--text-muted);
|
||||
vertical-align: middle;
|
||||
margin-left: 0.3rem;
|
||||
}
|
||||
.bm-empty {
|
||||
padding: 2rem 1rem;
|
||||
text-align: center;
|
||||
|
||||
@@ -1505,6 +1505,14 @@ where
|
||||
.body(body)
|
||||
}
|
||||
|
||||
/// A bookmark with its owning scope tag for the list response.
|
||||
#[derive(serde::Serialize)]
|
||||
struct BookmarkWithScope {
|
||||
#[serde(flatten)]
|
||||
bm: crate::server::bookmarks::Bookmark,
|
||||
scope: String,
|
||||
}
|
||||
|
||||
#[get("/bookmarks")]
|
||||
pub async fn list_bookmarks(
|
||||
req: HttpRequest,
|
||||
@@ -1517,15 +1525,38 @@ pub async fn list_bookmarks(
|
||||
status::index_html(),
|
||||
));
|
||||
}
|
||||
let store = resolve_bookmark_store(query.scope.as_deref(), store_map.get_ref());
|
||||
let mut list = store.list();
|
||||
let scope = query.scope.as_deref().filter(|s| !s.is_empty() && *s != "general");
|
||||
let mut list: Vec<BookmarkWithScope> = match scope {
|
||||
Some(remote) => {
|
||||
// Rig selected: merge general + rig-specific (rig wins on duplicate IDs).
|
||||
let mut map: std::collections::HashMap<String, BookmarkWithScope> = store_map
|
||||
.general()
|
||||
.list()
|
||||
.into_iter()
|
||||
.map(|bm| {
|
||||
let id = bm.id.clone();
|
||||
(id, BookmarkWithScope { bm, scope: "general".into() })
|
||||
})
|
||||
.collect();
|
||||
for bm in store_map.store_for(remote).list() {
|
||||
let id = bm.id.clone();
|
||||
map.insert(id, BookmarkWithScope { bm, scope: remote.to_owned() });
|
||||
}
|
||||
map.into_values().collect()
|
||||
}
|
||||
None => {
|
||||
store_map.general().list().into_iter()
|
||||
.map(|bm| BookmarkWithScope { bm, scope: "general".into() })
|
||||
.collect()
|
||||
}
|
||||
};
|
||||
if let Some(ref cat) = query.category {
|
||||
if !cat.is_empty() {
|
||||
let cat_lower = cat.to_lowercase();
|
||||
list.retain(|bm| bm.category.to_lowercase() == cat_lower);
|
||||
list.retain(|item| item.bm.category.to_lowercase() == cat_lower);
|
||||
}
|
||||
}
|
||||
list.sort_by_key(|bm| bm.freq_hz);
|
||||
list.sort_by_key(|item| item.bm.freq_hz);
|
||||
Ok(HttpResponse::Ok().json(list))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user