[feat](trx-frontend): add map fullscreen toggle

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-04 19:37:38 +01:00
parent 125237f53d
commit 8b174db04d
3 changed files with 108 additions and 5 deletions
@@ -3217,6 +3217,7 @@ let aprsMapBaseLayer = null;
let aprsMapReceiverMarker = null; let aprsMapReceiverMarker = null;
let aprsRadioPath = null; let aprsRadioPath = null;
let selectedLocatorMarker = null; let selectedLocatorMarker = null;
let mapFullscreenListenerBound = false;
const stationMarkers = new Map(); const stationMarkers = new Map();
const locatorMarkers = new Map(); const locatorMarkers = new Map();
const mapMarkers = new Set(); const mapMarkers = new Set();
@@ -3663,6 +3664,45 @@ function updateMapBaseLayerForTheme(theme) {
aprsMapBaseLayer = L.tileLayer(spec.url, spec.options).addTo(aprsMap); aprsMapBaseLayer = L.tileLayer(spec.url, spec.options).addTo(aprsMap);
} }
function mapStageEl() {
return document.getElementById("map-stage");
}
function mapIsFullscreen() {
const stage = mapStageEl();
if (!stage) return false;
return document.fullscreenElement === stage || document.webkitFullscreenElement === stage;
}
function updateMapFullscreenButton() {
const btn = document.getElementById("map-fullscreen-btn");
if (!btn) return;
btn.textContent = mapIsFullscreen() ? "Exit Fullscreen" : "Fullscreen";
}
async function toggleMapFullscreen() {
const stage = mapStageEl();
if (!stage) return;
try {
if (mapIsFullscreen()) {
if (document.exitFullscreen) {
await document.exitFullscreen();
} else if (document.webkitExitFullscreen) {
await document.webkitExitFullscreen();
}
} else if (stage.requestFullscreen) {
await stage.requestFullscreen();
} else if (stage.webkitRequestFullscreen) {
await stage.webkitRequestFullscreen();
}
} catch (err) {
console.error("Map fullscreen toggle failed", err);
} finally {
updateMapFullscreenButton();
sizeAprsMapToViewport();
}
}
function initAprsMap() { function initAprsMap() {
const mapEl = document.getElementById("aprs-map"); const mapEl = document.getElementById("aprs-map");
if (!mapEl) return; if (!mapEl) return;
@@ -3776,6 +3816,7 @@ function initAprsMap() {
const locatorPhaseEl = document.getElementById("map-locator-phase"); const locatorPhaseEl = document.getElementById("map-locator-phase");
const locatorChoiceEl = document.getElementById("map-locator-choice-filter"); const locatorChoiceEl = document.getElementById("map-locator-choice-filter");
const fullscreenBtn = document.getElementById("map-fullscreen-btn");
if (locatorPhaseEl) { if (locatorPhaseEl) {
locatorPhaseEl.addEventListener("click", (e) => { locatorPhaseEl.addEventListener("click", (e) => {
const btn = e.target.closest(".map-locator-phase-btn[data-phase]"); const btn = e.target.closest(".map-locator-phase-btn[data-phase]");
@@ -3815,6 +3856,21 @@ function initAprsMap() {
applyMapFilter(); applyMapFilter();
}); });
} }
if (fullscreenBtn) {
fullscreenBtn.addEventListener("click", () => {
toggleMapFullscreen();
});
updateMapFullscreenButton();
}
if (!mapFullscreenListenerBound) {
const onFullscreenChange = () => {
updateMapFullscreenButton();
sizeAprsMapToViewport();
};
document.addEventListener("fullscreenchange", onFullscreenChange);
document.addEventListener("webkitfullscreenchange", onFullscreenChange);
mapFullscreenListenerBound = true;
}
rebuildMapLocatorFilters(); rebuildMapLocatorFilters();
} }
@@ -3822,16 +3878,21 @@ function sizeAprsMapToViewport() {
const mapEl = document.getElementById("aprs-map"); const mapEl = document.getElementById("aprs-map");
if (!mapEl) return; if (!mapEl) return;
const mapRect = mapEl.getBoundingClientRect(); const mapRect = mapEl.getBoundingClientRect();
const stage = mapStageEl();
const width = mapEl.clientWidth || mapRect.width; const width = mapEl.clientWidth || mapRect.width;
const footer = document.querySelector(".footer"); const footer = document.querySelector(".footer");
let bottom = window.innerHeight; let bottom = mapIsFullscreen() && stage
if (footer) { ? stage.getBoundingClientRect().bottom
: window.innerHeight;
if (!mapIsFullscreen() && footer) {
const fr = footer.getBoundingClientRect(); const fr = footer.getBoundingClientRect();
if (fr.top > mapRect.top + 50) bottom = fr.top; if (fr.top > mapRect.top + 50) bottom = fr.top;
} }
const available = Math.max(0, Math.floor(bottom - mapRect.top - 8)); const available = Math.max(0, Math.floor(bottom - mapRect.top - 8));
const widthDriven = width > 0 ? Math.floor(width / 2.05) : available; const widthDriven = width > 0 ? Math.floor(width / 2.05) : available;
const viewportCap = Math.floor(window.innerHeight * 0.56); const viewportCap = mapIsFullscreen()
? Math.floor(window.innerHeight * 0.9)
: Math.floor(window.innerHeight * 0.56);
const minHeight = Math.min(260, available); const minHeight = Math.min(260, available);
const target = Math.max(minHeight, Math.min(available, viewportCap, widthDriven)); const target = Math.max(minHeight, Math.min(available, viewportCap, widthDriven));
mapEl.style.height = `${target}px`; mapEl.style.height = `${target}px`;
@@ -579,7 +579,10 @@
<div id="map-locator-choice-filter" class="map-locator-chip-row"></div> <div id="map-locator-choice-filter" class="map-locator-chip-row"></div>
</div> </div>
</div> </div>
<div id="aprs-map"></div> <div id="map-stage">
<button type="button" id="map-fullscreen-btn" class="map-fullscreen-btn">Fullscreen</button>
<div id="aprs-map"></div>
</div>
</div> </div>
<div id="tab-about" class="tab-panel" style="display:none;"> <div id="tab-about" class="tab-panel" style="display:none;">
<div id="auth-badge" style="display:none; margin-bottom: 1rem; padding: 0.5rem; background: var(--bg-secondary); border-radius: 0.25rem; color: var(--text-muted); font-size: 0.85rem;">Authenticated as: <strong id="auth-role-badge">--</strong></div> <div id="auth-badge" style="display:none; margin-bottom: 1rem; padding: 0.5rem; background: var(--bg-secondary); border-radius: 0.25rem; color: var(--text-muted); font-size: 0.85rem;">Authenticated as: <strong id="auth-role-badge">--</strong></div>
@@ -1094,12 +1094,51 @@ small { color: var(--text-muted); }
gap: 0.75rem; gap: 0.75rem;
min-height: 0; min-height: 0;
} }
#aprs-map { #map-stage {
position: relative;
flex: 0 1 auto; flex: 0 1 auto;
min-height: 0;
}
#aprs-map {
width: 100%; width: 100%;
min-height: 0; min-height: 0;
border-radius: 6px; border-radius: 6px;
} }
.map-fullscreen-btn {
position: absolute;
top: 0.7rem;
right: 0.7rem;
z-index: 410;
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 1.9rem;
padding: 0.18rem 0.65rem;
border-radius: 6px;
border: 1px solid color-mix(in srgb, var(--border-light) 74%, transparent);
background: color-mix(in srgb, var(--card-bg) 82%, transparent);
color: var(--text);
font-size: 0.76rem;
font-weight: 700;
letter-spacing: 0.02em;
cursor: pointer;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
.map-fullscreen-btn:hover {
border-color: color-mix(in srgb, var(--accent-green) 34%, var(--border-light));
color: var(--text-heading);
}
#map-stage:fullscreen,
#map-stage:-webkit-full-screen {
background: var(--bg);
padding: 0.75rem;
box-sizing: border-box;
}
#map-stage:fullscreen #aprs-map,
#map-stage:-webkit-full-screen #aprs-map {
border-radius: 8px;
}
.aprs-controls { display: flex; gap: 0.6rem; align-items: center; margin-bottom: 0.75rem; } .aprs-controls { display: flex; gap: 0.6rem; align-items: center; margin-bottom: 0.75rem; }
.aprs-summary { .aprs-summary {
display: grid; display: grid;