[feat](trx-frontend-http): add direct tab routes

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-12 23:54:25 +01:00
parent bdd3d29374
commit 963d527dfe
2 changed files with 87 additions and 12 deletions
@@ -113,23 +113,14 @@ function hideAuthGate() {
signalVisualBlock.style.display = "";
}
// Show Main tab by default and hide all other tabs
// Show the tab that matches the current route.
document.querySelectorAll(".tab-panel").forEach(panel => {
panel.style.display = "none";
});
const mainTab = document.getElementById("tab-main");
if (mainTab) {
mainTab.style.display = "";
}
// Mark Main tab button as active
document.querySelectorAll(".tab-bar .tab").forEach(btn => {
btn.classList.remove("active");
});
const mainTabBtn = document.querySelector(".tab-bar .tab[data-tab='main']");
if (mainTabBtn) {
mainTabBtn.classList.add("active");
}
navigateToTab(tabFromPath(), { updateHistory: false, replaceHistory: true });
syncTopBarAccess();
}
@@ -3525,8 +3516,39 @@ if (spectrumBwSweetBtn) {
// --- Tab navigation ---
const TAB_ORDER = ["main", "bookmarks", "decoders", "map", "settings", "about"];
const TAB_PATHS = {
main: "/",
bookmarks: "/bookmarks",
decoders: "/decoders",
map: "/map",
settings: "/settings",
about: "/about",
};
function navigateToTab(name) {
function normalizeTabPath(pathname) {
const raw = typeof pathname === "string" && pathname.length > 0 ? pathname : "/";
if (raw === "/") return "/";
return raw.replace(/\/+$/, "") || "/";
}
function tabFromPath(pathname = window.location.pathname) {
const normalized = normalizeTabPath(pathname);
for (const [tabName, tabPath] of Object.entries(TAB_PATHS)) {
if (normalized === tabPath) return tabName;
}
return "main";
}
function updateTabHistory(name, replaceHistory = false) {
const targetPath = TAB_PATHS[name] || "/";
if (normalizeTabPath(window.location.pathname) === targetPath) return;
const nextUrl = `${targetPath}${window.location.search}${window.location.hash}`;
const method = replaceHistory ? "replaceState" : "pushState";
window.history[method]({}, "", nextUrl);
}
function navigateToTab(name, options = {}) {
const { updateHistory = true, replaceHistory = false } = options;
if (authEnabled && !authRole && name !== "main") return;
const btn = document.querySelector(`.tab-bar .tab[data-tab="${name}"]`);
if (!btn) return;
@@ -3534,6 +3556,9 @@ function navigateToTab(name) {
btn.classList.add("active");
document.querySelectorAll(".tab-panel").forEach((p) => p.style.display = "none");
document.getElementById(`tab-${name}`).style.display = "";
if (updateHistory) {
updateTabHistory(name, replaceHistory);
}
scheduleSpectrumLayout();
if (name === "map") {
initAprsMap();
@@ -3548,6 +3573,10 @@ document.querySelector(".tab-bar").addEventListener("click", (e) => {
navigateToTab(btn.dataset.tab);
});
window.addEventListener("popstate", () => {
navigateToTab(tabFromPath(), { updateHistory: false });
});
// Swipe left/right on the main content area to switch tabs (mobile).
(function () {
let tx = 0, ty = 0;
@@ -938,11 +938,25 @@ fn normalize_bookmark_locator(locator: Option<String>) -> Option<String> {
})
}
fn request_accepts_html(req: &HttpRequest) -> bool {
req.headers()
.get(header::ACCEPT)
.and_then(|value| value.to_str().ok())
.map(|value| value.to_ascii_lowercase().contains("text/html"))
.unwrap_or(false)
}
#[get("/bookmarks")]
pub async fn list_bookmarks(
req: HttpRequest,
store: web::Data<Arc<crate::server::bookmarks::BookmarkStore>>,
query: web::Query<BookmarkQuery>,
) -> Result<HttpResponse, Error> {
if request_accepts_html(&req) {
return Ok(HttpResponse::Ok()
.insert_header((header::CONTENT_TYPE, "text/html; charset=utf-8"))
.body(status::index_html()));
}
let mut list = store.list();
if let Some(ref cat) = query.category {
if !cat.is_empty() {
@@ -1252,6 +1266,10 @@ pub async fn set_vchan_mode(
pub fn configure(cfg: &mut web::ServiceConfig) {
cfg.service(index)
.service(map_index)
.service(decoders_index)
.service(settings_index)
.service(about_index)
.service(status_api)
.service(list_rigs)
.service(events)
@@ -1344,6 +1362,34 @@ async fn index() -> impl Responder {
.body(status::index_html())
}
#[get("/map")]
async fn map_index() -> impl Responder {
HttpResponse::Ok()
.insert_header((header::CONTENT_TYPE, "text/html; charset=utf-8"))
.body(status::index_html())
}
#[get("/decoders")]
async fn decoders_index() -> impl Responder {
HttpResponse::Ok()
.insert_header((header::CONTENT_TYPE, "text/html; charset=utf-8"))
.body(status::index_html())
}
#[get("/settings")]
async fn settings_index() -> impl Responder {
HttpResponse::Ok()
.insert_header((header::CONTENT_TYPE, "text/html; charset=utf-8"))
.body(status::index_html())
}
#[get("/about")]
async fn about_index() -> impl Responder {
HttpResponse::Ok()
.insert_header((header::CONTENT_TYPE, "text/html; charset=utf-8"))
.body(status::index_html())
}
#[get("/favicon.ico")]
async fn favicon() -> impl Responder {
HttpResponse::Ok()