[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:
@@ -113,23 +113,14 @@ function hideAuthGate() {
|
|||||||
signalVisualBlock.style.display = "";
|
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 => {
|
document.querySelectorAll(".tab-panel").forEach(panel => {
|
||||||
panel.style.display = "none";
|
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 => {
|
document.querySelectorAll(".tab-bar .tab").forEach(btn => {
|
||||||
btn.classList.remove("active");
|
btn.classList.remove("active");
|
||||||
});
|
});
|
||||||
const mainTabBtn = document.querySelector(".tab-bar .tab[data-tab='main']");
|
navigateToTab(tabFromPath(), { updateHistory: false, replaceHistory: true });
|
||||||
if (mainTabBtn) {
|
|
||||||
mainTabBtn.classList.add("active");
|
|
||||||
}
|
|
||||||
syncTopBarAccess();
|
syncTopBarAccess();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3525,8 +3516,39 @@ if (spectrumBwSweetBtn) {
|
|||||||
|
|
||||||
// --- Tab navigation ---
|
// --- Tab navigation ---
|
||||||
const TAB_ORDER = ["main", "bookmarks", "decoders", "map", "settings", "about"];
|
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;
|
if (authEnabled && !authRole && name !== "main") return;
|
||||||
const btn = document.querySelector(`.tab-bar .tab[data-tab="${name}"]`);
|
const btn = document.querySelector(`.tab-bar .tab[data-tab="${name}"]`);
|
||||||
if (!btn) return;
|
if (!btn) return;
|
||||||
@@ -3534,6 +3556,9 @@ function navigateToTab(name) {
|
|||||||
btn.classList.add("active");
|
btn.classList.add("active");
|
||||||
document.querySelectorAll(".tab-panel").forEach((p) => p.style.display = "none");
|
document.querySelectorAll(".tab-panel").forEach((p) => p.style.display = "none");
|
||||||
document.getElementById(`tab-${name}`).style.display = "";
|
document.getElementById(`tab-${name}`).style.display = "";
|
||||||
|
if (updateHistory) {
|
||||||
|
updateTabHistory(name, replaceHistory);
|
||||||
|
}
|
||||||
scheduleSpectrumLayout();
|
scheduleSpectrumLayout();
|
||||||
if (name === "map") {
|
if (name === "map") {
|
||||||
initAprsMap();
|
initAprsMap();
|
||||||
@@ -3548,6 +3573,10 @@ document.querySelector(".tab-bar").addEventListener("click", (e) => {
|
|||||||
navigateToTab(btn.dataset.tab);
|
navigateToTab(btn.dataset.tab);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.addEventListener("popstate", () => {
|
||||||
|
navigateToTab(tabFromPath(), { updateHistory: false });
|
||||||
|
});
|
||||||
|
|
||||||
// Swipe left/right on the main content area to switch tabs (mobile).
|
// Swipe left/right on the main content area to switch tabs (mobile).
|
||||||
(function () {
|
(function () {
|
||||||
let tx = 0, ty = 0;
|
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")]
|
#[get("/bookmarks")]
|
||||||
pub async fn list_bookmarks(
|
pub async fn list_bookmarks(
|
||||||
|
req: HttpRequest,
|
||||||
store: web::Data<Arc<crate::server::bookmarks::BookmarkStore>>,
|
store: web::Data<Arc<crate::server::bookmarks::BookmarkStore>>,
|
||||||
query: web::Query<BookmarkQuery>,
|
query: web::Query<BookmarkQuery>,
|
||||||
) -> Result<HttpResponse, Error> {
|
) -> 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();
|
let mut list = store.list();
|
||||||
if let Some(ref cat) = query.category {
|
if let Some(ref cat) = query.category {
|
||||||
if !cat.is_empty() {
|
if !cat.is_empty() {
|
||||||
@@ -1252,6 +1266,10 @@ pub async fn set_vchan_mode(
|
|||||||
|
|
||||||
pub fn configure(cfg: &mut web::ServiceConfig) {
|
pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||||
cfg.service(index)
|
cfg.service(index)
|
||||||
|
.service(map_index)
|
||||||
|
.service(decoders_index)
|
||||||
|
.service(settings_index)
|
||||||
|
.service(about_index)
|
||||||
.service(status_api)
|
.service(status_api)
|
||||||
.service(list_rigs)
|
.service(list_rigs)
|
||||||
.service(events)
|
.service(events)
|
||||||
@@ -1344,6 +1362,34 @@ async fn index() -> impl Responder {
|
|||||||
.body(status::index_html())
|
.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")]
|
#[get("/favicon.ico")]
|
||||||
async fn favicon() -> impl Responder {
|
async fn favicon() -> impl Responder {
|
||||||
HttpResponse::Ok()
|
HttpResponse::Ok()
|
||||||
|
|||||||
Reference in New Issue
Block a user