From 963d527dfece753d9fa4411702f013490366a294 Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Thu, 12 Mar 2026 23:54:25 +0100 Subject: [PATCH] [feat](trx-frontend-http): add direct tab routes Co-authored-by: OpenAI Codex Signed-off-by: Stan Grams --- .../trx-frontend-http/assets/web/app.js | 53 ++++++++++++++----- .../trx-frontend/trx-frontend-http/src/api.rs | 46 ++++++++++++++++ 2 files changed, 87 insertions(+), 12 deletions(-) diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js index 4d3708d..8927d5e 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js @@ -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; diff --git a/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs b/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs index 436578f..5dfce3b 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs +++ b/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs @@ -938,11 +938,25 @@ fn normalize_bookmark_locator(locator: Option) -> Option { }) } +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>, query: web::Query, ) -> Result { + 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()