[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) {
|
if (typeof r.display_name === "string" && r.display_name.length > 0) {
|
||||||
displayNames[r.remote] = r.display_name;
|
displayNames[r.remote] = r.display_name;
|
||||||
} else {
|
} 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;
|
serverRigs = rigs;
|
||||||
|
|||||||
@@ -379,11 +379,11 @@
|
|||||||
</select>
|
</select>
|
||||||
<input type="search" id="bm-text-filter" class="status-input" placeholder="Search bookmarks…" aria-label="Search bookmarks" />
|
<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-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>
|
<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;">
|
<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>
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="bm-form-wrap" style="display:none;">
|
<div id="bm-form-wrap" style="display:none;">
|
||||||
|
|||||||
@@ -3,16 +3,17 @@
|
|||||||
/** 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 the current bookmark scope. */
|
/** Build the ?scope= query string for a given or current bookmark scope. */
|
||||||
function bmScopeParam(prefix) {
|
function bmScopeParam(prefix, scope) {
|
||||||
const sep = prefix ? "&" : "?";
|
const sep = prefix ? "&" : "?";
|
||||||
return sep + "scope=" + encodeURIComponent(bmScope);
|
return sep + "scope=" + encodeURIComponent(scope != null ? scope : bmScope);
|
||||||
}
|
}
|
||||||
|
|
||||||
var bmList = [];
|
var bmList = [];
|
||||||
var bmRevision = 0;
|
var bmRevision = 0;
|
||||||
let bmFilteredList = [];
|
let bmFilteredList = [];
|
||||||
let bmEditId = null;
|
let bmEditId = 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();
|
||||||
@@ -154,6 +155,7 @@ function bmRender(list) {
|
|||||||
const endIndex = Math.min(startIndex + BM_PAGE_SIZE, list.length);
|
const endIndex = Math.min(startIndex + BM_PAGE_SIZE, list.length);
|
||||||
const pageItems = list.slice(startIndex, endIndex);
|
const pageItems = list.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
const showScope = bmScope !== "general";
|
||||||
pageItems.forEach((bm) => {
|
pageItems.forEach((bm) => {
|
||||||
const tr = document.createElement("tr");
|
const tr = document.createElement("tr");
|
||||||
tr.dataset.bmId = bm.id;
|
tr.dataset.bmId = bm.id;
|
||||||
@@ -163,9 +165,10 @@ function bmRender(list) {
|
|||||||
const decoderCell = (bm.decoders || []).join(", ").toUpperCase() || "--";
|
const decoderCell = (bm.decoders || []).join(", ").toUpperCase() || "--";
|
||||||
const commentCell = bm.comment || "";
|
const commentCell = bm.comment || "";
|
||||||
const checked = bmSelected.has(bm.id) ? " checked" : "";
|
const checked = bmSelected.has(bm.id) ? " checked" : "";
|
||||||
|
const scopeBadge = showScope && bm.scope === "general" ? ' <span class="bm-scope-badge">G</span>' : "";
|
||||||
tr.innerHTML =
|
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-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-freq">${bmFmtFreq(bm.freq_hz)}</td>` +
|
||||||
`<td class="bm-col-mode">${bmEsc(bm.mode)}</td>` +
|
`<td class="bm-col-mode">${bmEsc(bm.mode)}</td>` +
|
||||||
`<td class="bm-col-bw">${bwCell}</td>` +
|
`<td class="bm-col-bw">${bwCell}</td>` +
|
||||||
@@ -228,6 +231,7 @@ function bmOpenForm(bm) {
|
|||||||
const wrap = document.getElementById("bm-form-wrap");
|
const wrap = document.getElementById("bm-form-wrap");
|
||||||
if (!wrap) return;
|
if (!wrap) return;
|
||||||
bmEditId = bm ? bm.id : null;
|
bmEditId = bm ? bm.id : null;
|
||||||
|
bmEditScope = bm ? (bm.scope || bmScope) : null;
|
||||||
|
|
||||||
document.getElementById("bm-id").value = bm ? bm.id : "";
|
document.getElementById("bm-id").value = bm ? bm.id : "";
|
||||||
document.getElementById("bm-name").value = bm ? bm.name : "";
|
document.getElementById("bm-name").value = bm ? bm.name : "";
|
||||||
@@ -296,7 +300,7 @@ async function bmSave(e) {
|
|||||||
try {
|
try {
|
||||||
let resp;
|
let resp;
|
||||||
if (id) {
|
if (id) {
|
||||||
resp = await fetch("/bookmarks/" + encodeURIComponent(id) + bmScopeParam(false), {
|
resp = await fetch("/bookmarks/" + encodeURIComponent(id) + bmScopeParam(false, bmEditScope), {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
@@ -325,8 +329,10 @@ async function bmSave(e) {
|
|||||||
|
|
||||||
async function bmDelete(id) {
|
async function bmDelete(id) {
|
||||||
if (!confirm("Delete this bookmark?")) return;
|
if (!confirm("Delete this bookmark?")) return;
|
||||||
|
const bm = bmList.find((b) => b.id === id);
|
||||||
|
const scope = bm ? bm.scope : undefined;
|
||||||
try {
|
try {
|
||||||
const resp = await fetch("/bookmarks/" + encodeURIComponent(id) + bmScopeParam(false), {
|
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);
|
||||||
@@ -471,12 +477,21 @@ async function bmMoveSelected() {
|
|||||||
const targetLabel = document.getElementById("bm-move-target")?.selectedOptions[0]?.textContent || target;
|
const targetLabel = document.getElementById("bm-move-target")?.selectedOptions[0]?.textContent || target;
|
||||||
if (!confirm(`Move ${ids.length} bookmark${ids.length > 1 ? "s" : ""} to "${targetLabel}"?`)) return;
|
if (!confirm(`Move ${ids.length} bookmark${ids.length > 1 ? "s" : ""} to "${targetLabel}"?`)) return;
|
||||||
try {
|
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",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ ids, to: target }),
|
body: JSON.stringify({ ids: scopeIds, to: target }),
|
||||||
});
|
}).then((r) => { if (!r.ok) throw new Error("HTTP " + r.status); })
|
||||||
if (!resp.ok) throw new Error("HTTP " + resp.status);
|
));
|
||||||
bmSelected.clear();
|
bmSelected.clear();
|
||||||
bmUpdateSelectionUi();
|
bmUpdateSelectionUi();
|
||||||
await bmFetch(document.getElementById("bm-category-filter").value);
|
await bmFetch(document.getElementById("bm-category-filter").value);
|
||||||
@@ -505,12 +520,20 @@ async function bmDeleteSelected() {
|
|||||||
if (ids.length === 0) return;
|
if (ids.length === 0) return;
|
||||||
if (!confirm(`Delete ${ids.length} selected bookmark${ids.length > 1 ? "s" : ""}?`)) return;
|
if (!confirm(`Delete ${ids.length} selected bookmark${ids.length > 1 ? "s" : ""}?`)) return;
|
||||||
try {
|
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",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ ids }),
|
body: JSON.stringify({ ids: scopeIds }),
|
||||||
});
|
}).then((r) => { if (!r.ok) throw new Error("HTTP " + r.status); })
|
||||||
if (!resp.ok) throw new Error("HTTP " + resp.status);
|
));
|
||||||
bmSelected.clear();
|
bmSelected.clear();
|
||||||
bmUpdateSelectionUi();
|
bmUpdateSelectionUi();
|
||||||
await bmFetch(document.getElementById("bm-category-filter").value);
|
await bmFetch(document.getElementById("bm-category-filter").value);
|
||||||
|
|||||||
@@ -3725,17 +3725,19 @@ button:focus-visible, input:focus-visible, select:focus-visible {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.35rem;
|
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 {
|
.bm-empty {
|
||||||
padding: 2rem 1rem;
|
padding: 2rem 1rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -1505,6 +1505,14 @@ where
|
|||||||
.body(body)
|
.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")]
|
#[get("/bookmarks")]
|
||||||
pub async fn list_bookmarks(
|
pub async fn list_bookmarks(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
@@ -1517,15 +1525,38 @@ pub async fn list_bookmarks(
|
|||||||
status::index_html(),
|
status::index_html(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let store = resolve_bookmark_store(query.scope.as_deref(), store_map.get_ref());
|
let scope = query.scope.as_deref().filter(|s| !s.is_empty() && *s != "general");
|
||||||
let mut list = store.list();
|
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 let Some(ref cat) = query.category {
|
||||||
if !cat.is_empty() {
|
if !cat.is_empty() {
|
||||||
let cat_lower = cat.to_lowercase();
|
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))
|
Ok(HttpResponse::Ok().json(list))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user