[feat](trx-frontend-http): implement frontend styling & performance improvements
CSS: reduce backdrop-filter to modals only, add contain/content-visibility for inactive tabs, optimize transitions to background-color, pre-compute color-mix results, add container queries, split themes to lazy-loaded file. JS: cache DOM refs in render path, add field-level diffing for SSE updates, replace innerHTML with replaceChildren() in hot paths, add WebGL colour cache invalidation on theme switch. HTML: add defer to scripts, lazy-load plugin scripts on tab activation, SVG sprite sheet for tab icons, template elements for deferred tab content, improve aria-live/keyboard nav/colour contrast accessibility. Server: upgrade Cache-Control to immutable, add Brotli compression alongside gzip with Accept-Encoding negotiation. Implements all items from docs/frontend_improvements.md except app.js ES module split (P1, requires major refactor) and Web Worker migration (P3). https://claude.ai/code/session_015rQNMGvusj5jY66MPUgYqt Signed-off-by: Claude <noreply@anthropic.com>
This commit is contained in:
Generated
+24
-2
@@ -31,7 +31,7 @@ dependencies = [
|
|||||||
"actix-utils",
|
"actix-utils",
|
||||||
"base64",
|
"base64",
|
||||||
"bitflags 2.10.0",
|
"bitflags 2.10.0",
|
||||||
"brotli",
|
"brotli 8.0.2",
|
||||||
"bytes",
|
"bytes",
|
||||||
"bytestring",
|
"bytestring",
|
||||||
"derive_more 2.0.1",
|
"derive_more 2.0.1",
|
||||||
@@ -414,6 +414,17 @@ dependencies = [
|
|||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "brotli"
|
||||||
|
version = "7.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd"
|
||||||
|
dependencies = [
|
||||||
|
"alloc-no-stdlib",
|
||||||
|
"alloc-stdlib",
|
||||||
|
"brotli-decompressor 4.0.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "brotli"
|
name = "brotli"
|
||||||
version = "8.0.2"
|
version = "8.0.2"
|
||||||
@@ -422,7 +433,17 @@ checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"alloc-no-stdlib",
|
"alloc-no-stdlib",
|
||||||
"alloc-stdlib",
|
"alloc-stdlib",
|
||||||
"brotli-decompressor",
|
"brotli-decompressor 5.0.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "brotli-decompressor"
|
||||||
|
version = "4.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a334ef7c9e23abf0ce748e8cd309037da93e606ad52eb372e4ce327a0dcfbdfd"
|
||||||
|
dependencies = [
|
||||||
|
"alloc-no-stdlib",
|
||||||
|
"alloc-stdlib",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3117,6 +3138,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"actix-web",
|
"actix-web",
|
||||||
"actix-ws",
|
"actix-ws",
|
||||||
|
"brotli 7.0.0",
|
||||||
"bytes",
|
"bytes",
|
||||||
"dirs",
|
"dirs",
|
||||||
"flate2",
|
"flate2",
|
||||||
|
|||||||
@@ -327,21 +327,32 @@ quadrantChart
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Quick wins (low effort, high impact)
|
### Quick wins (low effort, high impact)
|
||||||
1. Reduce `backdrop-filter` usage (13 blur instances) -- immediate paint perf gain
|
1. ~~Reduce `backdrop-filter` usage (13 blur instances)~~ **DONE** -- replaced with solid backgrounds, blur preserved for modals only, `prefers-reduced-motion` gate added
|
||||||
2. Add `contain: content` / `content-visibility: auto` to inactive tabs
|
2. ~~Add `contain: content` / `content-visibility: auto` to inactive tabs~~ **DONE** -- containment added for inactive tabs, spectrum/waterfall containers, map, statistics
|
||||||
3. Add `Cache-Control` headers to static assets
|
3. ~~Add `Cache-Control` headers to static assets~~ **DONE** -- upgraded to `public, max-age=31536000, immutable`
|
||||||
4. Cache remaining DOM references in the render path
|
4. ~~Cache remaining DOM references in the render path~~ **DONE** -- `tabMainEl` and other hot-path refs cached at module level
|
||||||
|
|
||||||
### Next phase (moderate effort)
|
### Next phase (moderate effort)
|
||||||
5. Split theme CSS into a separate lazy-loaded file
|
5. ~~Split theme CSS into a separate lazy-loaded file~~ **DONE** -- theme blocks extracted to `/themes.css`, lazy-loaded via `<link rel="preload">`
|
||||||
6. Self-host DSEG14 font
|
6. ~~Self-host DSEG14 font~~ **DONE** -- `@font-face` with `font-display: swap` added to `style.css`, CDN preconnect/preload removed from HTML
|
||||||
7. Pre-compute `color-mix` results as CSS variables
|
7. ~~Pre-compute `color-mix` results as CSS variables~~ **DONE** -- common mixes pre-computed as `--btn-hover-bg`, `--btn-active-bg`, etc.
|
||||||
8. Field-level diffing in the SSE render function
|
8. ~~Field-level diffing in the SSE render function~~ **DONE** -- `prevRenderData` tracks freq/mode/ptt/meter, active-tab-aware skip logic added
|
||||||
9. Replace `innerHTML` with DOM APIs in hot paths
|
9. ~~Replace `innerHTML` with DOM APIs in hot paths~~ **DONE** -- 15+ `innerHTML = ""` replaced with `replaceChildren()`
|
||||||
|
|
||||||
### Longer-term
|
### Longer-term
|
||||||
10. Split `app.js` into ES modules with lazy loading
|
10. Split `app.js` into ES modules with lazy loading -- **DEFERRED** (requires major refactor, tracked separately)
|
||||||
11. Lazy-load plugin scripts and Leaflet on demand
|
11. ~~Lazy-load plugin scripts and Leaflet on demand~~ **DONE** -- plugin scripts loaded on tab activation, core plugins loaded immediately
|
||||||
12. Use `<template>` elements for deferred tab content
|
12. ~~Use `<template>` elements for deferred tab content~~ **DONE** -- map, statistics, about tabs wrapped in `<template>`, cloned on first activation
|
||||||
13. Migrate to Brotli compression
|
13. ~~Migrate to Brotli compression~~ **DONE** -- Brotli added alongside gzip, preferred when `Accept-Encoding: br` present
|
||||||
14. Move SSE parsing and spectrum processing to Web Workers
|
14. Move SSE parsing and spectrum processing to Web Workers -- **DEFERRED** (requires SharedWorker + MessagePort plumbing, tracked separately)
|
||||||
|
|
||||||
|
### Additional improvements implemented
|
||||||
|
15. ~~Optimize CSS transitions~~ **DONE** -- `background` shorthand → `background-color` for GPU compositing
|
||||||
|
16. ~~Add `defer` to script tags~~ **DONE** -- all external script tags use `defer`
|
||||||
|
17. ~~SVG sprite sheet~~ **DONE** -- inline SVGs moved to `<symbol>` defs, referenced via `<use>`
|
||||||
|
18. ~~aria-live regions~~ **DONE** -- `aria-live` added to power hint, loading indicator
|
||||||
|
19. ~~Keyboard navigation~~ **DONE** -- `tabindex`/`role`/`aria-label` on spectrum/waterfall canvases
|
||||||
|
20. ~~Colour contrast~~ **DONE** -- dark theme `--text-muted` improved to `#9bb0ca`
|
||||||
|
21. ~~WebGL colour cache invalidation~~ **DONE** -- `trxClearCssColorCache()` called on theme switch
|
||||||
|
22. ~~Container queries~~ **DONE** -- controls tray and decode history table respond to container size
|
||||||
|
23. ~~Cache-Control immutable~~ **DONE** -- versioned assets use `immutable` directive
|
||||||
@@ -22,6 +22,7 @@ tokio-stream = { version = "0.1", features = ["sync"] }
|
|||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
flate2 = { workspace = true }
|
flate2 = { workspace = true }
|
||||||
|
brotli = "7"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
pickledb = "0.5"
|
pickledb = "0.5"
|
||||||
|
|||||||
@@ -389,6 +389,34 @@ const themeToggleBtn = document.getElementById("theme-toggle");
|
|||||||
const headerRigSwitchSelect = document.getElementById("header-rig-switch-select");
|
const headerRigSwitchSelect = document.getElementById("header-rig-switch-select");
|
||||||
const headerStylePickSelect = document.getElementById("header-style-pick-select");
|
const headerStylePickSelect = document.getElementById("header-style-pick-select");
|
||||||
const rdsPsOverlay = document.getElementById("rds-ps-overlay");
|
const rdsPsOverlay = document.getElementById("rds-ps-overlay");
|
||||||
|
const tabMainEl = document.getElementById("tab-main");
|
||||||
|
// Cached About-tab elements (avoid getElementById on every SSE render)
|
||||||
|
const aboutServerVerEl = document.getElementById("about-server-ver");
|
||||||
|
const aboutServerBuildDateEl = document.getElementById("about-server-build-date");
|
||||||
|
const aboutServerAddrEl = document.getElementById("about-server-addr");
|
||||||
|
const aboutServerCallEl = document.getElementById("about-server-call");
|
||||||
|
const aboutServerLocationEl = document.getElementById("about-server-location");
|
||||||
|
const aboutRigInfoEl = document.getElementById("about-rig-info");
|
||||||
|
const aboutRigAccessEl = document.getElementById("about-rig-access");
|
||||||
|
const aboutModesEl = document.getElementById("about-modes");
|
||||||
|
const aboutVfosEl = document.getElementById("about-vfos");
|
||||||
|
const aboutActiveRigEl = document.getElementById("about-active-rig");
|
||||||
|
const aboutAudioCodecEl = document.getElementById("about-audio-codec");
|
||||||
|
const aboutAudioSamplerateEl = document.getElementById("about-audio-samplerate");
|
||||||
|
const aboutAudioChannelsEl = document.getElementById("about-audio-channels");
|
||||||
|
const aboutAudioBitrateEl = document.getElementById("about-audio-bitrate");
|
||||||
|
const aboutAudioFrameEl = document.getElementById("about-audio-frame");
|
||||||
|
const aboutAudioRxEl = document.getElementById("about-audio-rx");
|
||||||
|
const aboutAudioStreamsEl = document.getElementById("about-audio-streams");
|
||||||
|
const aboutPskreporterEl = document.getElementById("about-pskreporter");
|
||||||
|
const aboutAprsIsEl = document.getElementById("about-aprs-is");
|
||||||
|
const aboutRigctlClientsEl = document.getElementById("about-rigctl-clients");
|
||||||
|
const aboutRigctlEndpointEl = document.getElementById("about-rigctl-endpoint");
|
||||||
|
const aboutClientsEl = document.getElementById("about-clients");
|
||||||
|
// Cached CW elements (avoid getElementById on every SSE render)
|
||||||
|
const cwAutoEl = document.getElementById("cw-auto");
|
||||||
|
const cwWpmEl = document.getElementById("cw-wpm");
|
||||||
|
const cwToneEl = document.getElementById("cw-tone");
|
||||||
let overviewPeakHoldMs = Number(loadSetting("overviewPeakHoldMs", 2000));
|
let overviewPeakHoldMs = Number(loadSetting("overviewPeakHoldMs", 2000));
|
||||||
let decodeHistoryRetentionMin = 24 * 60;
|
let decodeHistoryRetentionMin = 24 * 60;
|
||||||
|
|
||||||
@@ -640,6 +668,7 @@ let lastControl;
|
|||||||
let lastTxEn = null;
|
let lastTxEn = null;
|
||||||
let lastHasTx = true;
|
let lastHasTx = true;
|
||||||
let lastRendered = null;
|
let lastRendered = null;
|
||||||
|
let prevRenderData = {};
|
||||||
let hintTimer = null;
|
let hintTimer = null;
|
||||||
let sigMeasuring = false;
|
let sigMeasuring = false;
|
||||||
let sigLastSUnits = null;
|
let sigLastSUnits = null;
|
||||||
@@ -777,6 +806,7 @@ function setTheme(theme) {
|
|||||||
themeToggleBtn.textContent = next === "dark" ? "☀️ Light" : "🌙 Dark";
|
themeToggleBtn.textContent = next === "dark" ? "☀️ Light" : "🌙 Dark";
|
||||||
themeToggleBtn.title = next === "dark" ? "Switch to light mode" : "Switch to dark mode";
|
themeToggleBtn.title = next === "dark" ? "Switch to light mode" : "Switch to dark mode";
|
||||||
}
|
}
|
||||||
|
if (typeof trxClearCssColorCache === 'function') trxClearCssColorCache();
|
||||||
invalidateBookmarkColors();
|
invalidateBookmarkColors();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -994,6 +1024,7 @@ function setStyle(style) {
|
|||||||
}
|
}
|
||||||
saveSetting("style", next);
|
saveSetting("style", next);
|
||||||
if (headerStylePickSelect) headerStylePickSelect.value = next;
|
if (headerStylePickSelect) headerStylePickSelect.value = next;
|
||||||
|
if (typeof trxClearCssColorCache === 'function') trxClearCssColorCache();
|
||||||
invalidateBookmarkColors();
|
invalidateBookmarkColors();
|
||||||
scheduleOverviewDraw();
|
scheduleOverviewDraw();
|
||||||
}
|
}
|
||||||
@@ -1068,7 +1099,7 @@ window.getDecodeRigMeta = function() {
|
|||||||
function populateRigPicker(selectEl, rigIds, activeRigId, disabled) {
|
function populateRigPicker(selectEl, rigIds, activeRigId, disabled) {
|
||||||
if (!selectEl) return;
|
if (!selectEl) return;
|
||||||
const selectedBefore = selectEl.value;
|
const selectedBefore = selectEl.value;
|
||||||
selectEl.innerHTML = "";
|
selectEl.replaceChildren();
|
||||||
rigIds.forEach((id) => {
|
rigIds.forEach((id) => {
|
||||||
const opt = document.createElement("option");
|
const opt = document.createElement("option");
|
||||||
opt.value = id;
|
opt.value = id;
|
||||||
@@ -1809,7 +1840,7 @@ function renderRdsOverlays() {
|
|||||||
}
|
}
|
||||||
const entries = collectRdsOverlayEntries();
|
const entries = collectRdsOverlayEntries();
|
||||||
rdsOverlayEntries = [];
|
rdsOverlayEntries = [];
|
||||||
rdsPsOverlay.innerHTML = "";
|
rdsPsOverlay.replaceChildren();
|
||||||
if (entries.length === 0) {
|
if (entries.length === 0) {
|
||||||
rdsPsOverlay.style.display = "none";
|
rdsPsOverlay.style.display = "none";
|
||||||
return;
|
return;
|
||||||
@@ -2819,8 +2850,6 @@ function spectrumHeightBoundsPx(tabMainEl, contentEl, overviewCanvasEl, spectrum
|
|||||||
|
|
||||||
function updateSpectrumAutoHeight() {
|
function updateSpectrumAutoHeight() {
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
const tabMainEl = document.getElementById("tab-main");
|
|
||||||
const contentEl = document.getElementById("content");
|
|
||||||
const overviewCanvasEl = document.getElementById("overview-canvas");
|
const overviewCanvasEl = document.getElementById("overview-canvas");
|
||||||
const spectrumPanelEl = document.getElementById("spectrum-panel");
|
const spectrumPanelEl = document.getElementById("spectrum-panel");
|
||||||
const spectrumCanvasEl = document.getElementById("spectrum-canvas");
|
const spectrumCanvasEl = document.getElementById("spectrum-canvas");
|
||||||
@@ -2881,8 +2910,6 @@ function updateSpectrumAutoHeight() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function beginSpectrumResize(clientY) {
|
function beginSpectrumResize(clientY) {
|
||||||
const tabMainEl = document.getElementById("tab-main");
|
|
||||||
const contentEl = document.getElementById("content");
|
|
||||||
const overviewCanvasEl = document.getElementById("overview-canvas");
|
const overviewCanvasEl = document.getElementById("overview-canvas");
|
||||||
const spectrumCanvasEl = document.getElementById("spectrum-canvas");
|
const spectrumCanvasEl = document.getElementById("spectrum-canvas");
|
||||||
const spectrumPanelEl = document.getElementById("spectrum-panel");
|
const spectrumPanelEl = document.getElementById("spectrum-panel");
|
||||||
@@ -3108,7 +3135,7 @@ function render(update) {
|
|||||||
const modes = update.info.capabilities.supported_modes.map(normalizeMode).filter(Boolean);
|
const modes = update.info.capabilities.supported_modes.map(normalizeMode).filter(Boolean);
|
||||||
if (JSON.stringify(modes) !== JSON.stringify(supportedModes)) {
|
if (JSON.stringify(modes) !== JSON.stringify(supportedModes)) {
|
||||||
supportedModes = modes;
|
supportedModes = modes;
|
||||||
modeEl.innerHTML = "";
|
modeEl.replaceChildren();
|
||||||
supportedModes.forEach((m) => {
|
supportedModes.forEach((m) => {
|
||||||
const opt = document.createElement("option");
|
const opt = document.createElement("option");
|
||||||
opt.value = m;
|
opt.value = m;
|
||||||
@@ -3226,19 +3253,23 @@ function render(update) {
|
|||||||
updateSdrGainInputState();
|
updateSdrGainInputState();
|
||||||
}
|
}
|
||||||
if (update.status && update.status.freq && typeof update.status.freq.hz === "number") {
|
if (update.status && update.status.freq && typeof update.status.freq.hz === "number") {
|
||||||
const sseHz = update.status.freq.hz;
|
if (update.status.freq.hz !== prevRenderData.freqHz) {
|
||||||
// While an optimistic set_freq is in flight, suppress SSE updates that
|
prevRenderData.freqHz = update.status.freq.hz;
|
||||||
// would snap the marker back to the stale server frequency.
|
const sseHz = update.status.freq.hz;
|
||||||
if (_freqOptimisticHz != null && Math.abs(sseHz - _freqOptimisticHz) > 1) {
|
// While an optimistic set_freq is in flight, suppress SSE updates that
|
||||||
// stale — skip
|
// would snap the marker back to the stale server frequency.
|
||||||
} else {
|
if (_freqOptimisticHz != null && Math.abs(sseHz - _freqOptimisticHz) > 1) {
|
||||||
if (_freqOptimisticHz != null && Math.abs(sseHz - _freqOptimisticHz) <= 1) {
|
// stale — skip
|
||||||
_freqOptimisticHz = null; // server confirmed — clear guard early
|
} else {
|
||||||
|
if (_freqOptimisticHz != null && Math.abs(sseHz - _freqOptimisticHz) <= 1) {
|
||||||
|
_freqOptimisticHz = null; // server confirmed — clear guard early
|
||||||
|
}
|
||||||
|
applyLocalTunedFrequency(sseHz);
|
||||||
}
|
}
|
||||||
applyLocalTunedFrequency(sseHz);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (update.status && update.status.mode) {
|
if (update.status && update.status.mode && update.status.mode !== prevRenderData.mode) {
|
||||||
|
prevRenderData.mode = update.status.mode;
|
||||||
const mode = normalizeMode(update.status.mode);
|
const mode = normalizeMode(update.status.mode);
|
||||||
const modeUpper = mode ? mode.toUpperCase() : "";
|
const modeUpper = mode ? mode.toUpperCase() : "";
|
||||||
const onVirtual = typeof vchanIsOnVirtual === "function" && vchanIsOnVirtual();
|
const onVirtual = typeof vchanIsOnVirtual === "function" && vchanIsOnVirtual();
|
||||||
@@ -3290,7 +3321,8 @@ function render(update) {
|
|||||||
el.textContent = "Connected, listening for packets";
|
el.textContent = "Connected, listening for packets";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (update.status && typeof update.status.tx_en === "boolean") {
|
if (update.status && typeof update.status.tx_en === "boolean" && update.status.tx_en !== prevRenderData.txEn) {
|
||||||
|
prevRenderData.txEn = update.status.tx_en;
|
||||||
lastTxEn = update.status.tx_en;
|
lastTxEn = update.status.tx_en;
|
||||||
pttBtn.textContent = update.status.tx_en ? "PTT On" : "PTT Off";
|
pttBtn.textContent = update.status.tx_en ? "PTT On" : "PTT Off";
|
||||||
if (update.status.tx_en) {
|
if (update.status.tx_en) {
|
||||||
@@ -3313,9 +3345,7 @@ function render(update) {
|
|||||||
window._syncRecorderState(update.recorder_enabled);
|
window._syncRecorderState(update.recorder_enabled);
|
||||||
}
|
}
|
||||||
if (window.updateSatLiveState) window.updateSatLiveState(update);
|
if (window.updateSatLiveState) window.updateSatLiveState(update);
|
||||||
const cwAutoEl = document.getElementById("cw-auto");
|
// cwAutoEl, cwWpmEl, cwToneEl are cached at module level
|
||||||
const cwWpmEl = document.getElementById("cw-wpm");
|
|
||||||
const cwToneEl = document.getElementById("cw-tone");
|
|
||||||
if (cwWpmEl && typeof update.cw_wpm === "number") {
|
if (cwWpmEl && typeof update.cw_wpm === "number") {
|
||||||
cwWpmEl.value = update.cw_wpm;
|
cwWpmEl.value = update.cw_wpm;
|
||||||
}
|
}
|
||||||
@@ -3340,7 +3370,7 @@ function render(update) {
|
|||||||
if (update.status && update.status.vfo && Array.isArray(update.status.vfo.entries)) {
|
if (update.status && update.status.vfo && Array.isArray(update.status.vfo.entries)) {
|
||||||
const entries = update.status.vfo.entries;
|
const entries = update.status.vfo.entries;
|
||||||
const activeIdx = Number.isInteger(update.status.vfo.active) ? update.status.vfo.active : null;
|
const activeIdx = Number.isInteger(update.status.vfo.active) ? update.status.vfo.active : null;
|
||||||
vfoPicker.innerHTML = "";
|
vfoPicker.replaceChildren();
|
||||||
entries.forEach((entry, idx) => {
|
entries.forEach((entry, idx) => {
|
||||||
const hz = entry && entry.freq && typeof entry.freq.hz === "number" ? entry.freq.hz : null;
|
const hz = entry && entry.freq && typeof entry.freq.hz === "number" ? entry.freq.hz : null;
|
||||||
if (hz === null) return;
|
if (hz === null) return;
|
||||||
@@ -3377,14 +3407,18 @@ function render(update) {
|
|||||||
freqEl.style.color = activeFreqColor;
|
freqEl.style.color = activeFreqColor;
|
||||||
}
|
}
|
||||||
if (update.status && update.status.rx && typeof update.status.rx.sig === "number") {
|
if (update.status && update.status.rx && typeof update.status.rx.sig === "number") {
|
||||||
const sUnits = dbmToSUnits(update.status.rx.sig);
|
if (update.status.rx.sig !== prevRenderData.sigDbm) {
|
||||||
sigLastSUnits = sUnits;
|
prevRenderData.sigDbm = update.status.rx.sig;
|
||||||
sigLastDbm = update.status.rx.sig;
|
const sUnits = dbmToSUnits(update.status.rx.sig);
|
||||||
const pct = sUnits <= 9 ? Math.max(0, Math.min(100, (sUnits / 9) * 100)) : 100;
|
sigLastSUnits = sUnits;
|
||||||
signalBar.style.width = `${pct}%`;
|
sigLastDbm = update.status.rx.sig;
|
||||||
signalValue.textContent = formatSignal(sUnits);
|
const pct = sUnits <= 9 ? Math.max(0, Math.min(100, (sUnits / 9) * 100)) : 100;
|
||||||
refreshSigStrengthDisplay();
|
signalBar.style.width = `${pct}%`;
|
||||||
} else {
|
signalValue.textContent = formatSignal(sUnits);
|
||||||
|
refreshSigStrengthDisplay();
|
||||||
|
}
|
||||||
|
} else if (prevRenderData.sigDbm !== null) {
|
||||||
|
prevRenderData.sigDbm = null;
|
||||||
sigLastSUnits = null;
|
sigLastSUnits = null;
|
||||||
sigLastDbm = null;
|
sigLastDbm = null;
|
||||||
signalBar.style.width = "0%";
|
signalBar.style.width = "0%";
|
||||||
@@ -3413,100 +3447,103 @@ function render(update) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (typeof update.clients === "number") lastClientCount = update.clients;
|
if (typeof update.clients === "number") lastClientCount = update.clients;
|
||||||
// Populate About tab — Server card
|
// Populate About tab — only update DOM when the about tab is visible
|
||||||
if (update.server_version) {
|
if (_activeTab === "about") {
|
||||||
document.getElementById("about-server-ver").textContent = `trx-server v${update.server_version}`;
|
// About — Server card (uses cached DOM refs)
|
||||||
}
|
if (update.server_version && aboutServerVerEl) {
|
||||||
if (update.server_build_date) {
|
aboutServerVerEl.textContent = `trx-server v${update.server_version}`;
|
||||||
document.getElementById("about-server-build-date").textContent = update.server_build_date;
|
}
|
||||||
}
|
if (update.server_build_date && aboutServerBuildDateEl) {
|
||||||
document.getElementById("about-server-addr").textContent = location.host;
|
aboutServerBuildDateEl.textContent = update.server_build_date;
|
||||||
if (update.server_callsign) {
|
}
|
||||||
document.getElementById("about-server-call").textContent = update.server_callsign;
|
if (aboutServerAddrEl) aboutServerAddrEl.textContent = location.host;
|
||||||
}
|
if (update.server_callsign && aboutServerCallEl) {
|
||||||
if (Number.isFinite(serverLat) && Number.isFinite(serverLon)) {
|
aboutServerCallEl.textContent = update.server_callsign;
|
||||||
const grid = latLonToMaidenhead(serverLat, serverLon);
|
}
|
||||||
document.getElementById("about-server-location").textContent = `${grid} (${serverLat.toFixed(4)}, ${serverLon.toFixed(4)})`;
|
if (Number.isFinite(serverLat) && Number.isFinite(serverLon) && aboutServerLocationEl) {
|
||||||
}
|
const grid = latLonToMaidenhead(serverLat, serverLon);
|
||||||
|
aboutServerLocationEl.textContent = `${grid} (${serverLat.toFixed(4)}, ${serverLon.toFixed(4)})`;
|
||||||
|
}
|
||||||
|
|
||||||
// About — Radio card
|
// About — Radio card
|
||||||
if (update.info) {
|
if (update.info) {
|
||||||
const parts = [update.info.manufacturer, update.info.model, update.info.revision].filter(Boolean).join(" ");
|
const parts = [update.info.manufacturer, update.info.model, update.info.revision].filter(Boolean).join(" ");
|
||||||
if (parts) document.getElementById("about-rig-info").textContent = parts;
|
if (parts && aboutRigInfoEl) aboutRigInfoEl.textContent = parts;
|
||||||
const access = update.info.access;
|
const access = update.info.access;
|
||||||
if (access) {
|
if (access) {
|
||||||
if (access.Serial) {
|
if (access.Serial) {
|
||||||
const serialPath = access.Serial.path || access.Serial.port || "?";
|
const serialPath = access.Serial.path || access.Serial.port || "?";
|
||||||
document.getElementById("about-rig-access").textContent = `Serial (${serialPath}, ${access.Serial.baud || "?"} baud)`;
|
if (aboutRigAccessEl) aboutRigAccessEl.textContent = `Serial (${serialPath}, ${access.Serial.baud || "?"} baud)`;
|
||||||
} else if (access.Tcp) {
|
} else if (access.Tcp) {
|
||||||
document.getElementById("about-rig-access").textContent = `TCP (${access.Tcp.host || "?"}:${access.Tcp.port || "?"})`;
|
if (aboutRigAccessEl) aboutRigAccessEl.textContent = `TCP (${access.Tcp.host || "?"}:${access.Tcp.port || "?"})`;
|
||||||
} else {
|
} else {
|
||||||
const key = Object.keys(access)[0];
|
const key = Object.keys(access)[0];
|
||||||
if (key) document.getElementById("about-rig-access").textContent = key;
|
if (key && aboutRigAccessEl) aboutRigAccessEl.textContent = key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (update.info.capabilities) {
|
||||||
|
const cap = update.info.capabilities;
|
||||||
|
if (Array.isArray(cap.supported_modes) && cap.supported_modes.length && aboutModesEl) {
|
||||||
|
aboutModesEl.textContent = cap.supported_modes.map(normalizeMode).filter(Boolean).join(", ");
|
||||||
|
}
|
||||||
|
if (typeof cap.num_vfos === "number" && aboutVfosEl) {
|
||||||
|
aboutVfosEl.textContent = cap.num_vfos;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (update.info.capabilities) {
|
if (lastActiveRigId && aboutActiveRigEl) {
|
||||||
const cap = update.info.capabilities;
|
aboutActiveRigEl.textContent = lastActiveRigId;
|
||||||
if (Array.isArray(cap.supported_modes) && cap.supported_modes.length) {
|
}
|
||||||
document.getElementById("about-modes").textContent = cap.supported_modes.map(normalizeMode).filter(Boolean).join(", ");
|
|
||||||
|
// About — Audio card
|
||||||
|
if (streamInfo) {
|
||||||
|
if (aboutAudioCodecEl) aboutAudioCodecEl.textContent = "Opus";
|
||||||
|
if (aboutAudioSamplerateEl) aboutAudioSamplerateEl.textContent = `${(streamInfo.sample_rate || 48000).toLocaleString()} Hz`;
|
||||||
|
if (aboutAudioChannelsEl) aboutAudioChannelsEl.textContent = (streamInfo.channels || 1) === 1 ? "Mono" : "Stereo";
|
||||||
|
if (streamInfo.bitrate_bps && aboutAudioBitrateEl) {
|
||||||
|
const kbps = (streamInfo.bitrate_bps / 1000).toFixed(0);
|
||||||
|
aboutAudioBitrateEl.textContent = `${kbps} kbps`;
|
||||||
}
|
}
|
||||||
if (typeof cap.num_vfos === "number") {
|
if (streamInfo.frame_duration_ms && aboutAudioFrameEl) {
|
||||||
document.getElementById("about-vfos").textContent = cap.num_vfos;
|
aboutAudioFrameEl.textContent = `${streamInfo.frame_duration_ms} ms`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if (aboutAudioRxEl) aboutAudioRxEl.textContent = rxActive ? "Active" : "Off";
|
||||||
if (lastActiveRigId) {
|
if (typeof update.audio_clients === "number" && aboutAudioStreamsEl) {
|
||||||
document.getElementById("about-active-rig").textContent = lastActiveRigId;
|
aboutAudioStreamsEl.textContent = update.audio_clients;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// About — Decoders card (only update when values change)
|
||||||
|
syncAboutDecoder(0, !!update.ft8_decode_enabled);
|
||||||
|
syncAboutDecoder(1, !!update.ft4_decode_enabled);
|
||||||
|
syncAboutDecoder(2, !!update.ft2_decode_enabled);
|
||||||
|
syncAboutDecoder(3, !!update.wspr_decode_enabled);
|
||||||
|
syncAboutDecoder(4, !!update.cw_decode_enabled);
|
||||||
|
syncAboutDecoder(5, !!(update.aprs_decode_enabled || update.hf_aprs_decode_enabled));
|
||||||
|
syncAboutDecoder(6, !!update.lrpt_decode_enabled);
|
||||||
|
|
||||||
|
// About — Integrations card
|
||||||
|
if (update.pskreporter_status && aboutPskreporterEl) {
|
||||||
|
aboutPskreporterEl.textContent = update.pskreporter_status;
|
||||||
|
}
|
||||||
|
if (update.aprs_is_status && aboutAprsIsEl) {
|
||||||
|
aboutAprsIsEl.textContent = update.aprs_is_status;
|
||||||
|
}
|
||||||
|
if (typeof update.rigctl_clients === "number" && aboutRigctlClientsEl) {
|
||||||
|
aboutRigctlClientsEl.textContent = update.rigctl_clients;
|
||||||
|
}
|
||||||
|
if (typeof update.rigctl_addr === "string" && update.rigctl_addr.length > 0 && aboutRigctlEndpointEl) {
|
||||||
|
aboutRigctlEndpointEl.textContent = update.rigctl_addr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// About — Clients card
|
||||||
|
if (typeof update.clients === "number" && aboutClientsEl) {
|
||||||
|
aboutClientsEl.textContent = update.clients;
|
||||||
|
}
|
||||||
|
} // end _activeTab === "about"
|
||||||
if (Array.isArray(update.remotes)) {
|
if (Array.isArray(update.remotes)) {
|
||||||
applyRigList(update.active_remote, update.remotes);
|
applyRigList(update.active_remote, update.remotes);
|
||||||
}
|
}
|
||||||
|
|
||||||
// About — Audio card
|
|
||||||
if (streamInfo) {
|
|
||||||
document.getElementById("about-audio-codec").textContent = "Opus";
|
|
||||||
document.getElementById("about-audio-samplerate").textContent = `${(streamInfo.sample_rate || 48000).toLocaleString()} Hz`;
|
|
||||||
document.getElementById("about-audio-channels").textContent = (streamInfo.channels || 1) === 1 ? "Mono" : "Stereo";
|
|
||||||
if (streamInfo.bitrate_bps) {
|
|
||||||
const kbps = (streamInfo.bitrate_bps / 1000).toFixed(0);
|
|
||||||
document.getElementById("about-audio-bitrate").textContent = `${kbps} kbps`;
|
|
||||||
}
|
|
||||||
if (streamInfo.frame_duration_ms) {
|
|
||||||
document.getElementById("about-audio-frame").textContent = `${streamInfo.frame_duration_ms} ms`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
document.getElementById("about-audio-rx").textContent = rxActive ? "Active" : "Off";
|
|
||||||
if (typeof update.audio_clients === "number") {
|
|
||||||
document.getElementById("about-audio-streams").textContent = update.audio_clients;
|
|
||||||
}
|
|
||||||
|
|
||||||
// About — Decoders card (only update when values change)
|
|
||||||
syncAboutDecoder(0, !!update.ft8_decode_enabled);
|
|
||||||
syncAboutDecoder(1, !!update.ft4_decode_enabled);
|
|
||||||
syncAboutDecoder(2, !!update.ft2_decode_enabled);
|
|
||||||
syncAboutDecoder(3, !!update.wspr_decode_enabled);
|
|
||||||
syncAboutDecoder(4, !!update.cw_decode_enabled);
|
|
||||||
syncAboutDecoder(5, !!(update.aprs_decode_enabled || update.hf_aprs_decode_enabled));
|
|
||||||
syncAboutDecoder(6, !!update.lrpt_decode_enabled);
|
|
||||||
|
|
||||||
// About — Integrations card
|
|
||||||
if (update.pskreporter_status) {
|
|
||||||
document.getElementById("about-pskreporter").textContent = update.pskreporter_status;
|
|
||||||
}
|
|
||||||
if (update.aprs_is_status) {
|
|
||||||
document.getElementById("about-aprs-is").textContent = update.aprs_is_status;
|
|
||||||
}
|
|
||||||
if (typeof update.rigctl_clients === "number") {
|
|
||||||
document.getElementById("about-rigctl-clients").textContent = update.rigctl_clients;
|
|
||||||
}
|
|
||||||
if (typeof update.rigctl_addr === "string" && update.rigctl_addr.length > 0) {
|
|
||||||
document.getElementById("about-rigctl-endpoint").textContent = update.rigctl_addr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// About — Clients card
|
|
||||||
if (typeof update.clients === "number") {
|
|
||||||
document.getElementById("about-clients").textContent = update.clients;
|
|
||||||
}
|
|
||||||
powerHint.textContent = readyText();
|
powerHint.textContent = readyText();
|
||||||
lastLocked = update.status && update.status.lock === true;
|
lastLocked = update.status && update.status.lock === true;
|
||||||
lockBtn.textContent = lastLocked ? "Unlock" : "Lock";
|
lockBtn.textContent = lastLocked ? "Unlock" : "Lock";
|
||||||
@@ -3572,8 +3609,7 @@ function connect() {
|
|||||||
lastEventAt = Date.now();
|
lastEventAt = Date.now();
|
||||||
es.onopen = () => {
|
es.onopen = () => {
|
||||||
setConnLostOverlay(false);
|
setConnLostOverlay(false);
|
||||||
const tm = document.getElementById("tab-main");
|
if (tabMainEl) tabMainEl.classList.remove("server-disconnected");
|
||||||
if (tm) tm.classList.remove("server-disconnected");
|
|
||||||
if (!aboutUptimeStart) aboutUptimeStart = Date.now();
|
if (!aboutUptimeStart) aboutUptimeStart = Date.now();
|
||||||
pollFreshSnapshot();
|
pollFreshSnapshot();
|
||||||
refreshRigList();
|
refreshRigList();
|
||||||
@@ -3585,12 +3621,11 @@ function connect() {
|
|||||||
lastRendered = evt.data;
|
lastRendered = evt.data;
|
||||||
render(data);
|
render(data);
|
||||||
lastEventAt = Date.now();
|
lastEventAt = Date.now();
|
||||||
const tabMain = document.getElementById("tab-main");
|
|
||||||
if (data.server_connected === false) {
|
if (data.server_connected === false) {
|
||||||
powerHint.textContent = "trx-server connection lost";
|
powerHint.textContent = "trx-server connection lost";
|
||||||
if (tabMain) tabMain.classList.add("server-disconnected");
|
if (tabMainEl) tabMainEl.classList.add("server-disconnected");
|
||||||
} else {
|
} else {
|
||||||
if (tabMain) tabMain.classList.remove("server-disconnected");
|
if (tabMainEl) tabMainEl.classList.remove("server-disconnected");
|
||||||
if (data.initialized) powerHint.textContent = readyText();
|
if (data.initialized) powerHint.textContent = readyText();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -4302,6 +4337,7 @@ if (spectrumBwSweetBtn) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Tab navigation ---
|
// --- Tab navigation ---
|
||||||
|
let _activeTab = "main"; // tracked for render-path tab awareness
|
||||||
const TAB_ORDER = ["main", "bookmarks", "digital-modes", "map", "statistics", "recorder", "settings", "about"];
|
const TAB_ORDER = ["main", "bookmarks", "digital-modes", "map", "statistics", "recorder", "settings", "about"];
|
||||||
const TAB_PATHS = {
|
const TAB_PATHS = {
|
||||||
main: "/",
|
main: "/",
|
||||||
@@ -4340,6 +4376,7 @@ function navigateToTab(name, 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;
|
||||||
|
_activeTab = name;
|
||||||
document.querySelectorAll(".tab-bar .tab").forEach((t) => t.classList.remove("active"));
|
document.querySelectorAll(".tab-bar .tab").forEach((t) => t.classList.remove("active"));
|
||||||
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");
|
||||||
@@ -5406,7 +5443,7 @@ function sendLocatorOverlayToBack(marker) {
|
|||||||
|
|
||||||
function renderMapLocatorChipRow(container, items, selectedSet, kind) {
|
function renderMapLocatorChipRow(container, items, selectedSet, kind) {
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
container.innerHTML = "";
|
container.replaceChildren();
|
||||||
if (!Array.isArray(items) || items.length === 0) {
|
if (!Array.isArray(items) || items.length === 0) {
|
||||||
container.innerHTML = `<span class="map-locator-empty">No ${kind === "band" ? "bands" : "sources"} available</span>`;
|
container.innerHTML = `<span class="map-locator-empty">No ${kind === "band" ? "bands" : "sources"} available</span>`;
|
||||||
return;
|
return;
|
||||||
@@ -5447,7 +5484,7 @@ function renderMapLocatorChipRow(container, items, selectedSet, kind) {
|
|||||||
|
|
||||||
function renderMapLocatorPhaseRow(container, phase) {
|
function renderMapLocatorPhaseRow(container, phase) {
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
container.innerHTML = "";
|
container.replaceChildren();
|
||||||
const phases = [
|
const phases = [
|
||||||
{ key: "type", label: "Source" },
|
{ key: "type", label: "Source" },
|
||||||
{ key: "band", label: "Band" },
|
{ key: "band", label: "Band" },
|
||||||
@@ -5472,7 +5509,7 @@ function renderMapLocatorLegend(phase, sourceItems, bandItems) {
|
|||||||
: [];
|
: [];
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
legendEl.classList.add("is-empty");
|
legendEl.classList.add("is-empty");
|
||||||
legendEl.innerHTML = "";
|
legendEl.replaceChildren();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
legendEl.classList.remove("is-empty");
|
legendEl.classList.remove("is-empty");
|
||||||
@@ -6338,6 +6375,7 @@ function aprsSymbolIcon(symbolTable, symbolCode) {
|
|||||||
|
|
||||||
window.navigateToAprsMap = function(lat, lon) {
|
window.navigateToAprsMap = function(lat, lon) {
|
||||||
// Activate the map tab
|
// Activate the map tab
|
||||||
|
_activeTab = "map";
|
||||||
document.querySelectorAll(".tab-bar .tab").forEach((t) => t.classList.remove("active"));
|
document.querySelectorAll(".tab-bar .tab").forEach((t) => t.classList.remove("active"));
|
||||||
const mapTabBtn = document.querySelector(".tab-bar .tab[data-tab='map']");
|
const mapTabBtn = document.querySelector(".tab-bar .tab[data-tab='map']");
|
||||||
if (mapTabBtn) mapTabBtn.classList.add("active");
|
if (mapTabBtn) mapTabBtn.classList.add("active");
|
||||||
@@ -6358,6 +6396,7 @@ window.navigateToMapLocator = function(grid, preferredType = null) {
|
|||||||
const normalizedGrid = String(grid || "").trim().toUpperCase();
|
const normalizedGrid = String(grid || "").trim().toUpperCase();
|
||||||
if (!/^[A-R]{2}\d{2}(?:[A-X]{2})?$/.test(normalizedGrid)) return false;
|
if (!/^[A-R]{2}\d{2}(?:[A-X]{2})?$/.test(normalizedGrid)) return false;
|
||||||
|
|
||||||
|
_activeTab = "map";
|
||||||
document.querySelectorAll(".tab-bar .tab").forEach((t) => t.classList.remove("active"));
|
document.querySelectorAll(".tab-bar .tab").forEach((t) => t.classList.remove("active"));
|
||||||
const mapTabBtn = document.querySelector(".tab-bar .tab[data-tab='map']");
|
const mapTabBtn = document.querySelector(".tab-bar .tab[data-tab='map']");
|
||||||
if (mapTabBtn) mapTabBtn.classList.add("active");
|
if (mapTabBtn) mapTabBtn.classList.add("active");
|
||||||
@@ -7653,7 +7692,7 @@ function _renderBarChart(containerId, data, emptyMsg) {
|
|||||||
const el = document.getElementById(containerId);
|
const el = document.getElementById(containerId);
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
if (!data || data.length === 0 || data.every((d) => d.count === 0)) {
|
if (!data || data.length === 0 || data.every((d) => d.count === 0)) {
|
||||||
el.innerHTML = "";
|
el.replaceChildren();
|
||||||
const empty = document.createElement("div");
|
const empty = document.createElement("div");
|
||||||
empty.className = "stats-bar-empty";
|
empty.className = "stats-bar-empty";
|
||||||
empty.textContent = emptyMsg || "No data available.";
|
empty.textContent = emptyMsg || "No data available.";
|
||||||
@@ -10046,7 +10085,7 @@ function clearSpectrumCanvas() {
|
|||||||
spectrumGl.ensureSize(cssW, cssH, window.devicePixelRatio || 1);
|
spectrumGl.ensureSize(cssW, cssH, window.devicePixelRatio || 1);
|
||||||
spectrumGl.clear(cssColorToRgba(spectrumBgColor()));
|
spectrumGl.clear(cssColorToRgba(spectrumBgColor()));
|
||||||
if (spectrumDbAxis) {
|
if (spectrumDbAxis) {
|
||||||
spectrumDbAxis.innerHTML = "";
|
spectrumDbAxis.replaceChildren();
|
||||||
spectrumDbAxisKey = "";
|
spectrumDbAxisKey = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10144,7 +10183,7 @@ function renderRdsAlternativeFrequencies(list) {
|
|||||||
}
|
}
|
||||||
if (afEl.dataset.afKey === afKey) return;
|
if (afEl.dataset.afKey === afKey) return;
|
||||||
afEl.dataset.afKey = afKey;
|
afEl.dataset.afKey = afKey;
|
||||||
afEl.innerHTML = "";
|
afEl.replaceChildren();
|
||||||
for (const hz of afs) {
|
for (const hz of afs) {
|
||||||
const btn = document.createElement("button");
|
const btn = document.createElement("button");
|
||||||
btn.type = "button";
|
btn.type = "button";
|
||||||
@@ -10707,7 +10746,7 @@ function updateSideBookmarkStack(container, bookmarks, colorMap) {
|
|||||||
const nextKey = Array.isArray(bookmarks) ? `${rev}:${bookmarks.map((bm) => bm.id).join(",")}` : "";
|
const nextKey = Array.isArray(bookmarks) ? `${rev}:${bookmarks.map((bm) => bm.id).join(",")}` : "";
|
||||||
if (!Array.isArray(bookmarks) || bookmarks.length === 0) {
|
if (!Array.isArray(bookmarks) || bookmarks.length === 0) {
|
||||||
if (container.dataset.bmKey) {
|
if (container.dataset.bmKey) {
|
||||||
container.innerHTML = "";
|
container.replaceChildren();
|
||||||
container.dataset.bmKey = "";
|
container.dataset.bmKey = "";
|
||||||
}
|
}
|
||||||
container.classList.remove("bm-side-visible");
|
container.classList.remove("bm-side-visible");
|
||||||
@@ -10716,7 +10755,7 @@ function updateSideBookmarkStack(container, bookmarks, colorMap) {
|
|||||||
|
|
||||||
if (container.dataset.bmKey !== nextKey) {
|
if (container.dataset.bmKey !== nextKey) {
|
||||||
container.dataset.bmKey = nextKey;
|
container.dataset.bmKey = nextKey;
|
||||||
container.innerHTML = "";
|
container.replaceChildren();
|
||||||
for (const bm of bookmarks) {
|
for (const bm of bookmarks) {
|
||||||
container.appendChild(createBookmarkChip(bm, colorMap, { sideStack: true }));
|
container.appendChild(createBookmarkChip(bm, colorMap, { sideStack: true }));
|
||||||
}
|
}
|
||||||
@@ -10751,7 +10790,7 @@ function updateBookmarkAxis(range) {
|
|||||||
axisEl.classList.toggle("bm-axis-visible", hasVisible);
|
axisEl.classList.toggle("bm-axis-visible", hasVisible);
|
||||||
|
|
||||||
if (!hasVisible) {
|
if (!hasVisible) {
|
||||||
if (axisEl.dataset.bmKey) { axisEl.innerHTML = ""; axisEl.dataset.bmKey = ""; }
|
if (axisEl.dataset.bmKey) { axisEl.replaceChildren(); axisEl.dataset.bmKey = ""; }
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -10761,7 +10800,7 @@ function updateBookmarkAxis(range) {
|
|||||||
const newKey = `${rev}:${visBookmarks.map((b) => b.id).join(",")}`;
|
const newKey = `${rev}:${visBookmarks.map((b) => b.id).join(",")}`;
|
||||||
if (axisEl.dataset.bmKey !== newKey) {
|
if (axisEl.dataset.bmKey !== newKey) {
|
||||||
axisEl.dataset.bmKey = newKey;
|
axisEl.dataset.bmKey = newKey;
|
||||||
axisEl.innerHTML = "";
|
axisEl.replaceChildren();
|
||||||
for (const bm of visBookmarks) {
|
for (const bm of visBookmarks) {
|
||||||
axisEl.appendChild(createBookmarkChip(bm, colorMap));
|
axisEl.appendChild(createBookmarkChip(bm, colorMap));
|
||||||
}
|
}
|
||||||
@@ -10806,7 +10845,7 @@ function updateSpectrumFreqAxis(range) {
|
|||||||
const firstHz = Math.ceil(range.visLoHz / stepHz) * stepHz;
|
const firstHz = Math.ceil(range.visLoHz / stepHz) * stepHz;
|
||||||
const leftShiftBtn = document.getElementById("spectrum-center-left-btn");
|
const leftShiftBtn = document.getElementById("spectrum-center-left-btn");
|
||||||
const rightShiftBtn = document.getElementById("spectrum-center-right-btn");
|
const rightShiftBtn = document.getElementById("spectrum-center-right-btn");
|
||||||
spectrumFreqAxis.innerHTML = "";
|
spectrumFreqAxis.replaceChildren();
|
||||||
if (leftShiftBtn) spectrumFreqAxis.appendChild(leftShiftBtn);
|
if (leftShiftBtn) spectrumFreqAxis.appendChild(leftShiftBtn);
|
||||||
if (rightShiftBtn) spectrumFreqAxis.appendChild(rightShiftBtn);
|
if (rightShiftBtn) spectrumFreqAxis.appendChild(rightShiftBtn);
|
||||||
const axisWidth = spectrumFreqAxis.clientWidth || 0;
|
const axisWidth = spectrumFreqAxis.clientWidth || 0;
|
||||||
@@ -10853,7 +10892,7 @@ function updateSpectrumDbAxis(dbMin, dbMax, gridStep, heightPx, dpr) {
|
|||||||
].join(":");
|
].join(":");
|
||||||
if (key === spectrumDbAxisKey) return;
|
if (key === spectrumDbAxisKey) return;
|
||||||
spectrumDbAxisKey = key;
|
spectrumDbAxisKey = key;
|
||||||
spectrumDbAxis.innerHTML = "";
|
spectrumDbAxis.replaceChildren();
|
||||||
|
|
||||||
const spanDb = Math.max(1, dbMax - dbMin);
|
const spanDb = Math.max(1, dbMax - dbMin);
|
||||||
const cssHeight = heightPx / Math.max(1, dpr || 1);
|
const cssHeight = heightPx / Math.max(1, dpr || 1);
|
||||||
@@ -11844,7 +11883,7 @@ function bandplanVisibleSegments(region, loHz, hiHz) {
|
|||||||
function _hideBandplanStrip() {
|
function _hideBandplanStrip() {
|
||||||
if (!bandplanStripEl) return;
|
if (!bandplanStripEl) return;
|
||||||
bandplanStripEl.classList.remove("bp-visible");
|
bandplanStripEl.classList.remove("bp-visible");
|
||||||
bandplanStripEl.innerHTML = "";
|
bandplanStripEl.replaceChildren();
|
||||||
bandplanCacheKey = "";
|
bandplanCacheKey = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11870,7 +11909,7 @@ function updateBandplanStrip(range) {
|
|||||||
|
|
||||||
if (bandplanCacheKey !== newKey) {
|
if (bandplanCacheKey !== newKey) {
|
||||||
bandplanCacheKey = newKey;
|
bandplanCacheKey = newKey;
|
||||||
bandplanStripEl.innerHTML = "";
|
bandplanStripEl.replaceChildren();
|
||||||
|
|
||||||
const seenBands = new Set();
|
const seenBands = new Set();
|
||||||
for (const seg of segments) {
|
for (const seg of segments) {
|
||||||
|
|||||||
@@ -7,15 +7,24 @@
|
|||||||
<link rel="icon" type="image/png" sizes="any" href="/favicon.ico?v=5" />
|
<link rel="icon" type="image/png" sizes="any" href="/favicon.ico?v=5" />
|
||||||
<link rel="shortcut icon" href="/favicon.ico?v=5" />
|
<link rel="shortcut icon" href="/favicon.ico?v=5" />
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/favicon.png?v=5" />
|
<link rel="apple-touch-icon" sizes="180x180" href="/favicon.png?v=5" />
|
||||||
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
|
|
||||||
<link rel="preconnect" href="https://unpkg.com" crossorigin />
|
<link rel="preconnect" href="https://unpkg.com" crossorigin />
|
||||||
<link rel="preload" as="style" href="https://cdn.jsdelivr.net/npm/@fontsource/dseg14-classic/400.css" onload="this.onload=null;this.rel='stylesheet'" />
|
|
||||||
<noscript><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource/dseg14-classic/400.css" /></noscript>
|
|
||||||
<link rel="stylesheet" href="/style.css" />
|
<link rel="stylesheet" href="/style.css" />
|
||||||
|
<link rel="preload" as="style" href="/themes.css" onload="this.onload=null;this.rel='stylesheet'" />
|
||||||
|
<noscript><link rel="stylesheet" href="/themes.css" /></noscript>
|
||||||
<link rel="preload" as="style" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" onload="this.onload=null;this.rel='stylesheet'" />
|
<link rel="preload" as="style" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" onload="this.onload=null;this.rel='stylesheet'" />
|
||||||
<noscript><link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" /></noscript>
|
<noscript><link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" /></noscript>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" style="display:none">
|
||||||
|
<symbol id="icon-home" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2.8 7 8 2.9 13.2 7"/><path d="M4.3 5.9V13h7.4V5.9"/><path d="M6.8 13V9.3h2.4V13"/></symbol>
|
||||||
|
<symbol id="icon-bookmark" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 2h8v12l-4-2.5L4 14V2z"/></symbol>
|
||||||
|
<symbol id="icon-signal" viewBox="0 0 16 16" fill="currentColor"><rect x="1" y="11" width="2.5" height="4" rx="0.5"/><rect x="4.75" y="8" width="2.5" height="7" rx="0.5"/><rect x="8.5" y="5" width="2.5" height="10" rx="0.5"/><rect x="12.25" y="2" width="2.5" height="13" rx="0.5"/></symbol>
|
||||||
|
<symbol id="icon-map" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2a4 4 0 0 1 4 4c0 3-4 8-4 8S4 9 4 6a4 4 0 0 1 4-4z"/><circle cx="8" cy="6" r="1.2" fill="currentColor" stroke="none"/></symbol>
|
||||||
|
<symbol id="icon-stats" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M2 14h12"/><rect x="3" y="8" width="2" height="6" rx="0.4" fill="currentColor" stroke="none" opacity="0.6"/><rect x="7" y="5" width="2" height="9" rx="0.4" fill="currentColor" stroke="none" opacity="0.75"/><rect x="11" y="2" width="2" height="12" rx="0.4" fill="currentColor" stroke="none" opacity="0.9"/></symbol>
|
||||||
|
<symbol id="icon-record" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="6"/><circle cx="8" cy="8" r="2.5" fill="currentColor" stroke="none"/></symbol>
|
||||||
|
<symbol id="icon-settings" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M9.8 3.1a2.6 2.6 0 0 0-2.2 3.9L3.4 11.2a1.2 1.2 0 1 0 1.7 1.7l4.2-4.2a2.6 2.6 0 0 0 3.9-2.2l-1.8.6-1.2-1.2z"/><path d="M10.2 5.8 12 4"/></symbol>
|
||||||
|
<symbol id="icon-about" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><circle cx="8" cy="8" r="6"/><path d="M8 7v5"/><circle cx="8" cy="5" r="0.5" fill="currentColor" stroke="none"/></symbol>
|
||||||
|
</svg>
|
||||||
<div class="card" id="card">
|
<div class="card" id="card">
|
||||||
<div class="tab-bar" style="display:none;" id="tab-bar">
|
<div class="tab-bar" style="display:none;" id="tab-bar">
|
||||||
<div class="tab-bar-left">
|
<div class="tab-bar-left">
|
||||||
@@ -32,34 +41,34 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="tab-bar-nav">
|
<div class="tab-bar-nav">
|
||||||
<button class="tab active" data-tab="main">
|
<button class="tab active" data-tab="main">
|
||||||
<svg class="tab-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2.8 7 8 2.9 13.2 7"/><path d="M4.3 5.9V13h7.4V5.9"/><path d="M6.8 13V9.3h2.4V13"/></svg>
|
<svg class="tab-icon" aria-hidden="true"><use href="#icon-home"/></svg>
|
||||||
<span class="tab-label">Main</span>
|
<span class="tab-label">Main</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tab" data-tab="bookmarks">
|
<button class="tab" data-tab="bookmarks">
|
||||||
<svg class="tab-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 2h8v12l-4-2.5L4 14V2z"/></svg>
|
<svg class="tab-icon" aria-hidden="true"><use href="#icon-bookmark"/></svg>
|
||||||
<span class="tab-label">Bookmarks</span>
|
<span class="tab-label">Bookmarks</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tab" data-tab="digital-modes">
|
<button class="tab" data-tab="digital-modes">
|
||||||
<svg class="tab-icon" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><rect x="1" y="11" width="2.5" height="4" rx="0.5"/><rect x="4.75" y="8" width="2.5" height="7" rx="0.5"/><rect x="8.5" y="5" width="2.5" height="10" rx="0.5"/><rect x="12.25" y="2" width="2.5" height="13" rx="0.5"/></svg>
|
<svg class="tab-icon" aria-hidden="true"><use href="#icon-signal"/></svg>
|
||||||
<span class="tab-label">Digital modes</span>
|
<span class="tab-label">Digital modes</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tab" data-tab="map">
|
<button class="tab" data-tab="map">
|
||||||
<svg class="tab-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 2a4 4 0 0 1 4 4c0 3-4 8-4 8S4 9 4 6a4 4 0 0 1 4-4z"/><circle cx="8" cy="6" r="1.2" fill="currentColor" stroke="none"/></svg>
|
<svg class="tab-icon" aria-hidden="true"><use href="#icon-map"/></svg>
|
||||||
<span class="tab-label">Map</span>
|
<span class="tab-label">Map</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tab" data-tab="statistics">
|
<button class="tab" data-tab="statistics">
|
||||||
<svg class="tab-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 14h12"/><rect x="3" y="8" width="2" height="6" rx="0.4" fill="currentColor" stroke="none" opacity="0.6"/><rect x="7" y="5" width="2" height="9" rx="0.4" fill="currentColor" stroke="none" opacity="0.75"/><rect x="11" y="2" width="2" height="12" rx="0.4" fill="currentColor" stroke="none" opacity="0.9"/></svg>
|
<svg class="tab-icon" aria-hidden="true"><use href="#icon-stats"/></svg>
|
||||||
<span class="tab-label">Statistics</span>
|
<span class="tab-label">Statistics</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tab" data-tab="recorder">
|
<button class="tab" data-tab="recorder">
|
||||||
<svg class="tab-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="8" cy="8" r="6"/><circle cx="8" cy="8" r="2.5" fill="currentColor" stroke="none"/></svg>
|
<svg class="tab-icon" aria-hidden="true"><use href="#icon-record"/></svg>
|
||||||
<span class="tab-label">Recorder</span>
|
<span class="tab-label">Recorder</span>
|
||||||
<button class="tab" data-tab="settings">
|
<button class="tab" data-tab="settings">
|
||||||
<svg class="tab-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9.8 3.1a2.6 2.6 0 0 0-2.2 3.9L3.4 11.2a1.2 1.2 0 1 0 1.7 1.7l4.2-4.2a2.6 2.6 0 0 0 3.9-2.2l-1.8.6-1.2-1.2z"/><path d="M10.2 5.8 12 4"/></svg>
|
<svg class="tab-icon" aria-hidden="true"><use href="#icon-settings"/></svg>
|
||||||
<span class="tab-label">Settings</span>
|
<span class="tab-label">Settings</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tab" data-tab="about">
|
<button class="tab" data-tab="about">
|
||||||
<svg class="tab-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><circle cx="8" cy="8" r="6"/><path d="M8 7v5"/><circle cx="8" cy="5" r="0.5" fill="currentColor" stroke="none"/></svg>
|
<svg class="tab-icon" aria-hidden="true"><use href="#icon-about"/></svg>
|
||||||
<span class="tab-label">About</span>
|
<span class="tab-label">About</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -105,7 +114,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="tab-main" class="tab-panel">
|
<div id="tab-main" class="tab-panel">
|
||||||
<div id="server-lost-banner" aria-live="assertive"><span class="banner-dot"></span>trx-server connection lost — waiting for reconnect</div>
|
<div id="server-lost-banner" aria-live="assertive"><span class="banner-dot"></span>trx-server connection lost — waiting for reconnect</div>
|
||||||
<div id="loading" style="text-align:center; padding:2rem 0;">
|
<div id="loading" role="status" aria-live="polite" style="text-align:center; padding:2rem 0;">
|
||||||
<div id="loading-title" style="margin-bottom:0.4rem; font-size:1.1rem; font-weight:600;">Initializing (rig)…</div>
|
<div id="loading-title" style="margin-bottom:0.4rem; font-size:1.1rem; font-weight:600;">Initializing (rig)…</div>
|
||||||
<div id="loading-sub" style="color:#9aa4b5;"></div>
|
<div id="loading-sub" style="color:#9aa4b5;"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -126,13 +135,13 @@
|
|||||||
<div class="spectrum-wrap">
|
<div class="spectrum-wrap">
|
||||||
<div id="spectrum-bookmark-axis"></div>
|
<div id="spectrum-bookmark-axis"></div>
|
||||||
<div id="spectrum-bookmark-side-left" class="spectrum-bookmark-side spectrum-bookmark-side-left" aria-hidden="true"></div>
|
<div id="spectrum-bookmark-side-left" class="spectrum-bookmark-side spectrum-bookmark-side-left" aria-hidden="true"></div>
|
||||||
<canvas id="spectrum-canvas"></canvas>
|
<canvas id="spectrum-canvas" tabindex="0" role="img" aria-label="Spectrum display"></canvas>
|
||||||
<div id="spectrum-zoom-indicator" aria-hidden="true"></div>
|
<div id="spectrum-zoom-indicator" aria-hidden="true"></div>
|
||||||
<div id="spectrum-minimap" aria-hidden="true"><div class="minimap-view"></div></div>
|
<div id="spectrum-minimap" aria-hidden="true"><div class="minimap-view"></div></div>
|
||||||
<div id="spectrum-db-axis" aria-hidden="true"></div>
|
<div id="spectrum-db-axis" aria-hidden="true"></div>
|
||||||
<div id="spectrum-bookmark-side-right" class="spectrum-bookmark-side spectrum-bookmark-side-right" aria-hidden="true"></div>
|
<div id="spectrum-bookmark-side-right" class="spectrum-bookmark-side spectrum-bookmark-side-right" aria-hidden="true"></div>
|
||||||
<div id="spectrum-tooltip"></div>
|
<div id="spectrum-tooltip"></div>
|
||||||
<canvas id="spectrum-waterfall-canvas" style="display:none;"></canvas>
|
<canvas id="spectrum-waterfall-canvas" tabindex="0" role="img" aria-label="Waterfall display" style="display:none;"></canvas>
|
||||||
<div id="spectrum-freq-axis">
|
<div id="spectrum-freq-axis">
|
||||||
<button id="spectrum-center-left-btn" class="spectrum-edge-shift spectrum-edge-shift-left" type="button" aria-label="Shift spectrum center left">‹</button>
|
<button id="spectrum-center-left-btn" class="spectrum-edge-shift spectrum-edge-shift-left" type="button" aria-label="Shift spectrum center left">‹</button>
|
||||||
<button id="spectrum-center-right-btn" class="spectrum-edge-shift spectrum-edge-shift-right" type="button" aria-label="Shift spectrum center right">›</button>
|
<button id="spectrum-center-right-btn" class="spectrum-edge-shift spectrum-edge-shift-right" type="button" aria-label="Shift spectrum center right">›</button>
|
||||||
@@ -911,7 +920,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="tab-map" class="tab-panel" style="display:none;">
|
<div id="tab-map" class="tab-panel" data-tab="map" style="display:none;">
|
||||||
|
<template id="tmpl-map">
|
||||||
<div id="map-stage">
|
<div id="map-stage">
|
||||||
<div class="map-overlay-panel">
|
<div class="map-overlay-panel">
|
||||||
<div class="map-locator-filter-group">
|
<div class="map-locator-filter-group">
|
||||||
@@ -960,8 +970,10 @@
|
|||||||
<div id="map-band-legend" class="map-band-legend" aria-label="Band color legend"></div>
|
<div id="map-band-legend" class="map-band-legend" aria-label="Band color legend"></div>
|
||||||
<div id="aprs-map"></div>
|
<div id="aprs-map"></div>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div id="tab-statistics" class="tab-panel" style="display:none;">
|
<div id="tab-statistics" class="tab-panel" style="display:none;">
|
||||||
|
<template id="tmpl-statistics">
|
||||||
<div class="stats-controls">
|
<div class="stats-controls">
|
||||||
<div class="stats-control-group">
|
<div class="stats-control-group">
|
||||||
<label class="stats-control-label" for="stats-rig-filter">Receiver</label>
|
<label class="stats-control-label" for="stats-rig-filter">Receiver</label>
|
||||||
@@ -1061,6 +1073,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="map-weak-signal-summary-list" class="map-qso-summary-list"></div>
|
<div id="map-weak-signal-summary-list" class="map-qso-summary-list"></div>
|
||||||
</section>
|
</section>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div id="tab-recorder" class="tab-panel" style="display:none;">
|
<div id="tab-recorder" class="tab-panel" style="display:none;">
|
||||||
<h2 class="section-heading">Recorder</h2>
|
<h2 class="section-heading">Recorder</h2>
|
||||||
@@ -1395,6 +1408,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="tab-about" class="tab-panel" style="display:none;">
|
<div id="tab-about" class="tab-panel" style="display:none;">
|
||||||
|
<template id="tmpl-about">
|
||||||
<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>
|
||||||
<div class="sub-tab-bar">
|
<div class="sub-tab-bar">
|
||||||
<button class="sub-tab active" data-subtab="about-server">Server</button>
|
<button class="sub-tab active" data-subtab="about-server">Server</button>
|
||||||
@@ -1495,12 +1509,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<div class="copyright">
|
<div class="copyright">
|
||||||
Built by <a href="https://www.qrzcq.com/call/SP2SJG" target="_blank" rel="noopener">SP2SJG</a> from <a href="https://haxx.space" target="_blank" rel="noopener">haxx.space</a> · <span class="gh-link-wrap"><a class="gh-link" href="https://github.com/sgrams/trx-rs" target="_blank" rel="noopener" aria-label="Open trx-rs on GitHub"><svg class="gh-link-icon" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 0.2a8 8 0 0 0-2.53 15.59c0.4 0.07 0.55-0.17 0.55-0.39l-0.01-1.37c-2.23 0.49-2.7-0.95-2.7-0.95-0.36-0.91-0.89-1.15-0.89-1.15-0.73-0.49 0.06-0.48 0.06-0.48 0.8 0.06 1.22 0.82 1.22 0.82 0.72 1.22 1.88 0.87 2.34 0.67 0.07-0.51 0.28-0.86 0.5-1.06-1.78-0.2-3.64-0.89-3.64-3.95 0-0.87 0.31-1.58 0.81-2.14-0.08-0.2-0.35-1.02 0.08-2.12 0 0 0.67-0.21 2.2 0.82a7.56 7.56 0 0 1 4.01 0c1.53-1.03 2.2-0.82 2.2-0.82 0.43 1.1 0.16 1.92 0.08 2.12 0.51 0.56 0.81 1.27 0.81 2.14 0 3.07-1.87 3.75-3.66 3.95 0.29 0.25 0.54 0.73 0.54 1.48l-0.01 2.2c0 0.22 0.14 0.47 0.55 0.39A8 8 0 0 0 8 0.2Z"></path></svg><span>trx-rs on GitHub</span></a></span> — <span id="copyright-year"></span>
|
Built by <a href="https://www.qrzcq.com/call/SP2SJG" target="_blank" rel="noopener">SP2SJG</a> from <a href="https://haxx.space" target="_blank" rel="noopener">haxx.space</a> · <span class="gh-link-wrap"><a class="gh-link" href="https://github.com/sgrams/trx-rs" target="_blank" rel="noopener" aria-label="Open trx-rs on GitHub"><svg class="gh-link-icon" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 0.2a8 8 0 0 0-2.53 15.59c0.4 0.07 0.55-0.17 0.55-0.39l-0.01-1.37c-2.23 0.49-2.7-0.95-2.7-0.95-0.36-0.91-0.89-1.15-0.89-1.15-0.73-0.49 0.06-0.48 0.06-0.48 0.8 0.06 1.22 0.82 1.22 0.82 0.72 1.22 1.88 0.87 2.34 0.67 0.07-0.51 0.28-0.86 0.5-1.06-1.78-0.2-3.64-0.89-3.64-3.95 0-0.87 0.31-1.58 0.81-2.14-0.08-0.2-0.35-1.02 0.08-2.12 0 0 0.67-0.21 2.2 0.82a7.56 7.56 0 0 1 4.01 0c1.53-1.03 2.2-0.82 2.2-0.82 0.43 1.1 0.16 1.92 0.08 2.12 0.51 0.56 0.81 1.27 0.81 2.14 0 3.07-1.87 3.75-3.66 3.95 0.29 0.25 0.54 0.73 0.54 1.48l-0.01 2.2c0 0.22 0.14 0.47 0.55 0.39A8 8 0 0 0 8 0.2Z"></path></svg><span>trx-rs on GitHub</span></a></span> — <span id="copyright-year"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="hint" id="power-hint">Connecting…</div>
|
<div class="hint" id="power-hint" aria-live="polite">Connecting…</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="conn-lost-overlay" class="decode-history-overlay content-overlay is-hidden" aria-live="assertive" aria-atomic="true">
|
<div id="conn-lost-overlay" class="decode-history-overlay content-overlay is-hidden" aria-live="assertive" aria-atomic="true">
|
||||||
<div class="decode-history-overlay-card">
|
<div class="decode-history-overlay-card">
|
||||||
@@ -1540,25 +1555,57 @@
|
|||||||
<div id="decode-history-overlay-sub" class="decode-history-overlay-sub">Preparing recent decodes for the UI</div>
|
<div id="decode-history-overlay-sub" class="decode-history-overlay-sub">Preparing recent decodes for the UI</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/opus-decoder@0.7.11/dist/opus-decoder.min.js" charset="UTF-8"></script>
|
<script defer src="https://cdn.jsdelivr.net/npm/opus-decoder@0.7.11/dist/opus-decoder.min.js" charset="UTF-8"></script>
|
||||||
<script src="/webgl-renderer.js"></script>
|
<script defer src="/webgl-renderer.js"></script>
|
||||||
<script src="/app.js"></script>
|
<script defer src="/app.js"></script>
|
||||||
<script src="/ais.js"></script>
|
<script>
|
||||||
<script src="/vdes.js"></script>
|
// Lazy plugin loader: loads plugin scripts when their tab/feature is first activated
|
||||||
<script src="/aprs.js"></script>
|
(function() {
|
||||||
<script src="/hf-aprs.js"></script>
|
var pluginScripts = {
|
||||||
<script src="/ft8.js"></script>
|
'digital-modes': ['/ft8.js', '/ft4.js', '/ft2.js', '/wspr.js', '/cw.js', '/background-decode.js'],
|
||||||
<script src="/ft4.js"></script>
|
'map': ['/leaflet-ais-tracksymbol.js', '/ais.js', '/vdes.js', '/aprs.js', '/hf-aprs.js', '/sat.js', '/sat-scheduler.js'],
|
||||||
<script src="/ft2.js"></script>
|
'bookmarks': ['/bookmarks.js'],
|
||||||
<script src="/wspr.js"></script>
|
'recorder': ['/scheduler.js'],
|
||||||
<script src="/cw.js"></script>
|
'settings': ['/vchan.js']
|
||||||
<script src="/sat.js"></script>
|
};
|
||||||
<script src="/bookmarks.js"></script>
|
var loaded = new Set();
|
||||||
<script src="/scheduler.js"></script>
|
function loadPlugins(tab) {
|
||||||
<script src="/sat-scheduler.js"></script>
|
var scripts = pluginScripts[tab];
|
||||||
<script src="/background-decode.js"></script>
|
if (!scripts) return;
|
||||||
<script src="/vchan.js"></script>
|
scripts.forEach(function(src) {
|
||||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
if (loaded.has(src)) return;
|
||||||
<script src="/leaflet-ais-tracksymbol.js"></script>
|
loaded.add(src);
|
||||||
|
var s = document.createElement('script');
|
||||||
|
s.src = src;
|
||||||
|
s.defer = true;
|
||||||
|
document.body.appendChild(s);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Load core plugins immediately (needed on main tab)
|
||||||
|
['digital-modes', 'bookmarks'].forEach(loadPlugins);
|
||||||
|
// Load others on tab switch
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
var tab = e.target.closest('[data-tab]');
|
||||||
|
if (tab) loadPlugins(tab.dataset.tab);
|
||||||
|
});
|
||||||
|
window.loadPluginsForTab = loadPlugins;
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
var tab = e.target.closest('[data-tab]');
|
||||||
|
if (!tab) return;
|
||||||
|
var panel = document.getElementById('tab-' + tab.dataset.tab);
|
||||||
|
if (!panel) return;
|
||||||
|
var tmpl = panel.querySelector('template');
|
||||||
|
if (tmpl) {
|
||||||
|
panel.appendChild(tmpl.content.cloneNode(true));
|
||||||
|
tmpl.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<script defer src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
--border: #22324a;
|
--border: #22324a;
|
||||||
--border-light: #304766;
|
--border-light: #304766;
|
||||||
--text: #e7edf9;
|
--text: #e7edf9;
|
||||||
--text-muted: #91a3bd;
|
--text-muted: #9bb0ca;
|
||||||
--text-heading: #c6d5ea;
|
--text-heading: #c6d5ea;
|
||||||
--btn-bg: #16243a;
|
--btn-bg: #16243a;
|
||||||
--btn-border: #3a5274;
|
--btn-border: #3a5274;
|
||||||
@@ -36,6 +36,11 @@
|
|||||||
--card-bookmark-gutter: 9.5rem;
|
--card-bookmark-gutter: 9.5rem;
|
||||||
--spectrum-bookmark-side-width: 6.5rem;
|
--spectrum-bookmark-side-width: 6.5rem;
|
||||||
--spectrum-bookmark-side-offset: 8.85rem;
|
--spectrum-bookmark-side-offset: 8.85rem;
|
||||||
|
--btn-hover-bg: color-mix(in srgb, var(--btn-bg) 85%, var(--text) 15%);
|
||||||
|
--btn-active-bg: color-mix(in srgb, var(--btn-bg) 75%, var(--text) 25%);
|
||||||
|
--border-hover: color-mix(in srgb, var(--border-light) 80%, var(--text) 20%);
|
||||||
|
--accent-green-hover: color-mix(in srgb, var(--accent-green) 85%, #fff 15%);
|
||||||
|
--accent-green-active: color-mix(in srgb, var(--accent-green) 75%, #fff 25%);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="light"] {
|
[data-theme="light"] {
|
||||||
@@ -67,6 +72,15 @@
|
|||||||
--spectrum-bg: #eef3fb;
|
--spectrum-bg: #eef3fb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'DSEG14 Classic';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('https://cdn.jsdelivr.net/npm/@fontsource/dseg14-classic/files/dseg14-classic-latin-400-normal.woff2') format('woff2');
|
||||||
|
unicode-range: U+0030-0039, U+002E, U+002D, U+0020, U+002B;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -290,7 +304,7 @@ input.status-input, select.status-input { width: 100%; padding: 0.45rem 0.5rem;
|
|||||||
width: 0%;
|
width: 0%;
|
||||||
border-radius: 5px 0 0 5px;
|
border-radius: 5px 0 0 5px;
|
||||||
background: color-mix(in srgb, #4fc3f7 45%, transparent);
|
background: color-mix(in srgb, #4fc3f7 45%, transparent);
|
||||||
transition: width 0.25s ease, background 0.35s ease;
|
transition: width 0.25s ease, background-color 0.35s ease;
|
||||||
}
|
}
|
||||||
.wfm-intf-fill.wfm-intf-warn {
|
.wfm-intf-fill.wfm-intf-warn {
|
||||||
background: color-mix(in srgb, #ffa726 55%, transparent);
|
background: color-mix(in srgb, #ffa726 55%, transparent);
|
||||||
@@ -514,7 +528,7 @@ input.status-input, select.status-input { width: 100%; padding: 0.45rem 0.5rem;
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
margin-top: 0.4rem;
|
margin-top: 0.4rem;
|
||||||
}
|
}
|
||||||
button { padding: 0.5rem 0.9rem; border-radius: 6px; border: 1px solid var(--btn-border); background: var(--btn-bg); color: var(--text); cursor: pointer; height: var(--control-height); transition: background 100ms ease, border-color 100ms ease, color 100ms ease, box-shadow 100ms ease; }
|
button { padding: 0.5rem 0.9rem; border-radius: 6px; border: 1px solid var(--btn-border); background: var(--btn-bg); color: var(--text); cursor: pointer; height: var(--control-height); transition: background-color 100ms ease, border-color 100ms ease, color 100ms ease, box-shadow 100ms ease; }
|
||||||
button:hover:not(:disabled) { background: color-mix(in srgb, var(--btn-bg) 75%, var(--accent-green)); border-color: color-mix(in srgb, var(--btn-border) 60%, var(--accent-green)); box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent-green) 18%, transparent); }
|
button:hover:not(:disabled) { background: color-mix(in srgb, var(--btn-bg) 75%, var(--accent-green)); border-color: color-mix(in srgb, var(--btn-border) 60%, var(--accent-green)); box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent-green) 18%, transparent); }
|
||||||
button:active:not(:disabled) { background: color-mix(in srgb, var(--btn-bg) 55%, var(--accent-green)); border-color: var(--accent-green); box-shadow: none; transform: translateY(1px); }
|
button:active:not(:disabled) { background: color-mix(in srgb, var(--btn-bg) 55%, var(--accent-green)); border-color: var(--accent-green); box-shadow: none; transform: translateY(1px); }
|
||||||
button:disabled { opacity: 0.6; cursor: not-allowed; }
|
button:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||||
@@ -664,9 +678,7 @@ small { color: var(--text-muted); }
|
|||||||
margin-left: -0.3rem;
|
margin-left: -0.3rem;
|
||||||
transform: translateY(-10px);
|
transform: translateY(-10px);
|
||||||
border-radius: 0.95rem;
|
border-radius: 0.95rem;
|
||||||
background: color-mix(in srgb, var(--card-bg) 56%, transparent);
|
background: rgba(15, 23, 42, 0.92);
|
||||||
backdrop-filter: blur(12px) saturate(125%);
|
|
||||||
-webkit-backdrop-filter: blur(12px) saturate(125%);
|
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 8px 20px color-mix(in srgb, #000000 18%, transparent),
|
0 8px 20px color-mix(in srgb, #000000 18%, transparent),
|
||||||
inset 0 1px 0 color-mix(in srgb, #ffffff 10%, transparent);
|
inset 0 1px 0 color-mix(in srgb, #ffffff 10%, transparent);
|
||||||
@@ -784,9 +796,7 @@ small { color: var(--text-muted); }
|
|||||||
padding: 0.34rem 0.9rem 0.28rem;
|
padding: 0.34rem 0.9rem 0.28rem;
|
||||||
border: 1px solid color-mix(in srgb, var(--border-light) 72%, transparent);
|
border: 1px solid color-mix(in srgb, var(--border-light) 72%, transparent);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background: color-mix(in srgb, var(--card-bg) 52%, transparent);
|
background: rgba(15, 23, 42, 0.92);
|
||||||
backdrop-filter: blur(14px) saturate(135%);
|
|
||||||
-webkit-backdrop-filter: blur(14px) saturate(135%);
|
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 8px 18px color-mix(in srgb, #000000 16%, transparent),
|
0 8px 18px color-mix(in srgb, #000000 16%, transparent),
|
||||||
inset 0 1px 0 color-mix(in srgb, #ffffff 10%, transparent);
|
inset 0 1px 0 color-mix(in srgb, #ffffff 10%, transparent);
|
||||||
@@ -883,9 +893,7 @@ small { color: var(--text-muted); }
|
|||||||
padding: 0.22rem 0.55rem 0.24rem;
|
padding: 0.22rem 0.55rem 0.24rem;
|
||||||
border: 1px solid color-mix(in srgb, var(--border-light) 72%, transparent);
|
border: 1px solid color-mix(in srgb, var(--border-light) 72%, transparent);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background: color-mix(in srgb, var(--card-bg) 68%, transparent);
|
background: rgba(15, 23, 42, 0.92);
|
||||||
backdrop-filter: blur(16px) saturate(130%);
|
|
||||||
-webkit-backdrop-filter: blur(16px) saturate(130%);
|
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 10px 24px color-mix(in srgb, #000000 14%, transparent),
|
0 10px 24px color-mix(in srgb, #000000 14%, transparent),
|
||||||
inset 0 1px 0 color-mix(in srgb, #ffffff 8%, transparent),
|
inset 0 1px 0 color-mix(in srgb, #ffffff 8%, transparent),
|
||||||
@@ -899,9 +907,7 @@ small { color: var(--text-muted); }
|
|||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
background: color-mix(in srgb, var(--card-bg) 88%, transparent);
|
background: rgba(15, 23, 42, 0.95);
|
||||||
backdrop-filter: blur(8px);
|
|
||||||
-webkit-backdrop-filter: blur(8px);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
@@ -1034,7 +1040,7 @@ small { color: var(--text-muted); }
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
opacity: 0.82;
|
opacity: 0.82;
|
||||||
transition: opacity 120ms, border-color 120ms, background 120ms;
|
transition: opacity 120ms, border-color 120ms, background-color 120ms;
|
||||||
}
|
}
|
||||||
.aprs-bar-pin:hover {
|
.aprs-bar-pin:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
@@ -1178,7 +1184,7 @@ small { color: var(--text-muted); }
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: border-color 0.15s, color 0.15s, background 0.15s;
|
transition: border-color 0.15s, color 0.15s, background-color 0.15s;
|
||||||
}
|
}
|
||||||
.recorder-action-btn:hover:not(:disabled) { border-color: var(--accent-green); color: var(--accent-green); }
|
.recorder-action-btn:hover:not(:disabled) { border-color: var(--accent-green); color: var(--accent-green); }
|
||||||
.recorder-action-btn:disabled { opacity: 0.4; cursor: default; }
|
.recorder-action-btn:disabled { opacity: 0.4; cursor: default; }
|
||||||
@@ -1521,9 +1527,7 @@ small { color: var(--text-muted); }
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 1.2rem;
|
padding: 1.2rem;
|
||||||
background: color-mix(in srgb, var(--bg) 36%, transparent);
|
background: rgba(7, 13, 26, 0.82);
|
||||||
backdrop-filter: blur(6px);
|
|
||||||
-webkit-backdrop-filter: blur(6px);
|
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
transition: opacity 140ms ease, visibility 140ms ease;
|
transition: opacity 140ms ease, visibility 140ms ease;
|
||||||
@@ -1597,9 +1601,7 @@ small { color: var(--text-muted); }
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 1.2rem;
|
padding: 1.2rem;
|
||||||
background: color-mix(in srgb, var(--bg) 36%, transparent);
|
background: rgba(7, 13, 26, 0.82);
|
||||||
backdrop-filter: blur(6px);
|
|
||||||
-webkit-backdrop-filter: blur(6px);
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
@@ -1757,7 +1759,7 @@ button.map-qso-card {
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
transition: border-color 120ms ease, background 120ms ease, transform 120ms ease, box-shadow 120ms ease;
|
transition: border-color 120ms ease, background-color 120ms ease, transform 120ms ease, box-shadow 120ms ease;
|
||||||
}
|
}
|
||||||
button.map-qso-card:hover:not(:disabled) {
|
button.map-qso-card:hover:not(:disabled) {
|
||||||
border-color: color-mix(in srgb, var(--accent-green) 38%, var(--border-light));
|
border-color: color-mix(in srgb, var(--accent-green) 38%, var(--border-light));
|
||||||
@@ -1870,8 +1872,9 @@ button.map-qso-card:focus-visible {
|
|||||||
border: 1px solid color-mix(in srgb, var(--border-light) 74%, transparent);
|
border: 1px solid color-mix(in srgb, var(--border-light) 74%, transparent);
|
||||||
background: color-mix(in srgb, var(--card-bg) 82%, transparent);
|
background: color-mix(in srgb, var(--card-bg) 82%, transparent);
|
||||||
box-shadow: 0 16px 30px rgba(0, 0, 0, 0.24);
|
box-shadow: 0 16px 30px rgba(0, 0, 0, 0.24);
|
||||||
backdrop-filter: blur(14px);
|
backdrop-filter: blur(6px);
|
||||||
-webkit-backdrop-filter: blur(14px);
|
-webkit-backdrop-filter: blur(6px);
|
||||||
|
will-change: backdrop-filter;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
transition: opacity 140ms ease, transform 140ms ease, visibility 140ms ease;
|
transition: opacity 140ms ease, transform 140ms ease, visibility 140ms ease;
|
||||||
}
|
}
|
||||||
@@ -1905,8 +1908,6 @@ button.map-qso-card:focus-visible {
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
backdrop-filter: blur(8px);
|
|
||||||
-webkit-backdrop-filter: blur(8px);
|
|
||||||
}
|
}
|
||||||
.map-fullscreen-btn:hover {
|
.map-fullscreen-btn:hover {
|
||||||
border-color: color-mix(in srgb, var(--accent-green) 34%, var(--border-light));
|
border-color: color-mix(in srgb, var(--accent-green) 34%, var(--border-light));
|
||||||
@@ -1926,8 +1927,6 @@ button.map-qso-card:focus-visible {
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
backdrop-filter: blur(8px);
|
|
||||||
-webkit-backdrop-filter: blur(8px);
|
|
||||||
}
|
}
|
||||||
.map-overlay-toggle-btn:hover {
|
.map-overlay-toggle-btn:hover {
|
||||||
border-color: color-mix(in srgb, var(--accent-green) 34%, var(--border-light));
|
border-color: color-mix(in srgb, var(--accent-green) 34%, var(--border-light));
|
||||||
@@ -1948,8 +1947,9 @@ button.map-qso-card:focus-visible {
|
|||||||
border: 1px solid color-mix(in srgb, var(--border-light) 74%, transparent);
|
border: 1px solid color-mix(in srgb, var(--border-light) 74%, transparent);
|
||||||
background: color-mix(in srgb, var(--card-bg) 78%, transparent);
|
background: color-mix(in srgb, var(--card-bg) 78%, transparent);
|
||||||
box-shadow: 0 16px 30px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 16px 30px rgba(0, 0, 0, 0.2);
|
||||||
backdrop-filter: blur(14px);
|
backdrop-filter: blur(6px);
|
||||||
-webkit-backdrop-filter: blur(14px);
|
-webkit-backdrop-filter: blur(6px);
|
||||||
|
will-change: backdrop-filter;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
.map-band-legend.is-empty {
|
.map-band-legend.is-empty {
|
||||||
@@ -2565,7 +2565,7 @@ body.map-fake-fullscreen-active {
|
|||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: border-color 120ms ease, background 120ms ease, color 120ms ease;
|
transition: border-color 120ms ease, background-color 120ms ease, color 120ms ease;
|
||||||
}
|
}
|
||||||
.map-locator-phase-btn:hover {
|
.map-locator-phase-btn:hover {
|
||||||
border-color: color-mix(in srgb, var(--accent-green) 24%, var(--border-light));
|
border-color: color-mix(in srgb, var(--accent-green) 24%, var(--border-light));
|
||||||
@@ -2595,7 +2595,7 @@ body.map-fake-fullscreen-active {
|
|||||||
background: color-mix(in srgb, var(--chip-color) 8%, var(--input-bg));
|
background: color-mix(in srgb, var(--chip-color) 8%, var(--input-bg));
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: border-color 120ms ease, background 120ms ease, opacity 120ms ease, color 120ms ease;
|
transition: border-color 120ms ease, background-color 120ms ease, opacity 120ms ease, color 120ms ease;
|
||||||
}
|
}
|
||||||
.map-locator-chip:hover {
|
.map-locator-chip:hover {
|
||||||
color: var(--text-heading);
|
color: var(--text-heading);
|
||||||
@@ -2813,9 +2813,7 @@ button:focus-visible, input:focus-visible, select:focus-visible {
|
|||||||
padding: 0.38rem;
|
padding: 0.38rem;
|
||||||
border: 1px solid color-mix(in srgb, var(--border-light) 82%, transparent);
|
border: 1px solid color-mix(in srgb, var(--border-light) 82%, transparent);
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
background: color-mix(in srgb, var(--card-bg) 90%, transparent);
|
background: rgba(15, 23, 42, 0.95);
|
||||||
backdrop-filter: blur(18px) saturate(135%);
|
|
||||||
-webkit-backdrop-filter: blur(18px) saturate(135%);
|
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 12px 28px color-mix(in srgb, #000000 22%, transparent),
|
0 12px 28px color-mix(in srgb, #000000 22%, transparent),
|
||||||
inset 0 1px 0 color-mix(in srgb, #ffffff 10%, transparent);
|
inset 0 1px 0 color-mix(in srgb, #ffffff 10%, transparent);
|
||||||
@@ -3766,237 +3764,8 @@ button:focus-visible, input:focus-visible, select:focus-visible {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Arctic style ─────────────────────────────────────────────────────── */
|
/* Theme styles have been moved to themes.css */
|
||||||
[data-style="arctic"] {
|
|
||||||
--bg: #242933;
|
|
||||||
--card-bg: #2e3440;
|
|
||||||
--input-bg: #242933;
|
|
||||||
--border: #3b4252;
|
|
||||||
--border-light: #4c566a;
|
|
||||||
--text: #d8dee9;
|
|
||||||
--text-muted: #8a9ab0;
|
|
||||||
--text-heading: #eceff4;
|
|
||||||
--btn-bg: #3b4252;
|
|
||||||
--btn-border: #5e6f88;
|
|
||||||
--accent-green: #88c0d0;
|
|
||||||
--accent-yellow: #ebcb8b;
|
|
||||||
--accent-red: #bf616a;
|
|
||||||
--jog-hi: #434c5e;
|
|
||||||
--jog-lo: #3b4252;
|
|
||||||
--jog-shadow: rgba(0,0,0,0.40);
|
|
||||||
--jog-inset: rgba(255,255,255,0.06);
|
|
||||||
--audio-level-bg: #2e3440;
|
|
||||||
--audio-level-border: #4c566a;
|
|
||||||
--audio-level-fill-start: #88c0d0;
|
|
||||||
--audio-level-fill-end: #ebcb8b;
|
|
||||||
--filter-bg: #3b4252;
|
|
||||||
--filter-fg: #d8dee9;
|
|
||||||
--filter-border: #5e6f88;
|
|
||||||
--wavelength-fg: #7a8ea8;
|
|
||||||
--spectrum-bg: #1e2530;
|
|
||||||
}
|
|
||||||
[data-style="arctic"][data-theme="light"] {
|
|
||||||
--bg: #e5e9f0;
|
|
||||||
--card-bg: #eceff4;
|
|
||||||
--input-bg: #d8dee9;
|
|
||||||
--border: #c5ccd8;
|
|
||||||
--border-light: #a8b2c0;
|
|
||||||
--text: #2e3440;
|
|
||||||
--text-muted: #4c566a;
|
|
||||||
--text-heading: #2e3440;
|
|
||||||
--btn-bg: #d8dee9;
|
|
||||||
--btn-border: #8fa3b8;
|
|
||||||
--accent-green: #5e81ac;
|
|
||||||
--accent-yellow: #c07a22;
|
|
||||||
--accent-red: #bf616a;
|
|
||||||
--jog-hi: #d8dee9;
|
|
||||||
--jog-lo: #c2cbd8;
|
|
||||||
--jog-shadow: rgba(46,52,64,0.18);
|
|
||||||
--jog-inset: rgba(255,255,255,0.70);
|
|
||||||
--audio-level-bg: #d0d6e0;
|
|
||||||
--audio-level-border: #a8b2c0;
|
|
||||||
--audio-level-fill-start: #5e81ac;
|
|
||||||
--audio-level-fill-end: #c07a22;
|
|
||||||
--filter-bg: #d8dee9;
|
|
||||||
--filter-fg: #2e3440;
|
|
||||||
--filter-border: #8fa3b8;
|
|
||||||
--wavelength-fg: #5a6a80;
|
|
||||||
--spectrum-bg: #dde1e9;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Lime style ───────────────────────────────────────────────────────── */
|
|
||||||
[data-style="lime"] {
|
|
||||||
--bg: #1c1c17;
|
|
||||||
--card-bg: #272822;
|
|
||||||
--input-bg: #1c1c17;
|
|
||||||
--border: #3e3d32;
|
|
||||||
--border-light: #5c5c45;
|
|
||||||
--text: #f8f8f2;
|
|
||||||
--text-muted: #908980;
|
|
||||||
--text-heading: #f8f8f2;
|
|
||||||
--btn-bg: #3e3d32;
|
|
||||||
--btn-border: #75715e;
|
|
||||||
--accent-green: #a6e22e;
|
|
||||||
--accent-yellow: #e6db74;
|
|
||||||
--accent-red: #f92672;
|
|
||||||
--jog-hi: #49483e;
|
|
||||||
--jog-lo: #3e3d32;
|
|
||||||
--jog-shadow: rgba(0,0,0,0.45);
|
|
||||||
--jog-inset: rgba(255,255,255,0.05);
|
|
||||||
--audio-level-bg: #272822;
|
|
||||||
--audio-level-border: #5c5c45;
|
|
||||||
--audio-level-fill-start: #a6e22e;
|
|
||||||
--audio-level-fill-end: #e6db74;
|
|
||||||
--filter-bg: #3e3d32;
|
|
||||||
--filter-fg: #f8f8f2;
|
|
||||||
--filter-border: #75715e;
|
|
||||||
--wavelength-fg: #9c8f78;
|
|
||||||
--spectrum-bg: #181815;
|
|
||||||
}
|
|
||||||
[data-style="lime"][data-theme="light"] {
|
|
||||||
--bg: #f5f0e4;
|
|
||||||
--card-bg: #fdf9f2;
|
|
||||||
--input-bg: #ede8d8;
|
|
||||||
--border: #d8d0bb;
|
|
||||||
--border-light: #c0b89e;
|
|
||||||
--text: #272822;
|
|
||||||
--text-muted: #6e6a56;
|
|
||||||
--text-heading: #272822;
|
|
||||||
--btn-bg: #ede8d8;
|
|
||||||
--btn-border: #b0a888;
|
|
||||||
--accent-green: #5f8700;
|
|
||||||
--accent-yellow: #9a7200;
|
|
||||||
--accent-red: #c60052;
|
|
||||||
--jog-hi: #ede8d8;
|
|
||||||
--jog-lo: #ddd8c8;
|
|
||||||
--jog-shadow: rgba(39,40,34,0.18);
|
|
||||||
--jog-inset: rgba(255,255,255,0.75);
|
|
||||||
--audio-level-bg: #ede8d8;
|
|
||||||
--audio-level-border: #c0b89e;
|
|
||||||
--audio-level-fill-start: #5f8700;
|
|
||||||
--audio-level-fill-end: #9a7200;
|
|
||||||
--filter-bg: #ede8d8;
|
|
||||||
--filter-fg: #272822;
|
|
||||||
--filter-border: #b0a888;
|
|
||||||
--wavelength-fg: #7a7260;
|
|
||||||
--spectrum-bg: #ede8d8;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Contrast style ───────────────────────────────────────────────────── */
|
|
||||||
[data-style="contrast"] {
|
|
||||||
--bg: #000000;
|
|
||||||
--card-bg: #0a0a0a;
|
|
||||||
--input-bg: #111111;
|
|
||||||
--border: #333333;
|
|
||||||
--border-light: #555555;
|
|
||||||
--text: #ffffff;
|
|
||||||
--text-muted: #bbbbbb;
|
|
||||||
--text-heading: #ffffff;
|
|
||||||
--btn-bg: #1a1a1a;
|
|
||||||
--btn-border: #666666;
|
|
||||||
--accent-green: #00ff88;
|
|
||||||
--accent-yellow: #ffcc00;
|
|
||||||
--accent-red: #ff3344;
|
|
||||||
--jog-hi: #2a2a2a;
|
|
||||||
--jog-lo: #1a1a1a;
|
|
||||||
--jog-shadow: rgba(0,0,0,0.60);
|
|
||||||
--jog-inset: rgba(255,255,255,0.08);
|
|
||||||
--audio-level-bg: #111111;
|
|
||||||
--audio-level-border: #555555;
|
|
||||||
--audio-level-fill-start: #00ff88;
|
|
||||||
--audio-level-fill-end: #ffcc00;
|
|
||||||
--filter-bg: #1a1a1a;
|
|
||||||
--filter-fg: #ffffff;
|
|
||||||
--filter-border: #666666;
|
|
||||||
--wavelength-fg: #aaaaaa;
|
|
||||||
--spectrum-bg: #000000;
|
|
||||||
}
|
|
||||||
[data-style="contrast"][data-theme="light"] {
|
|
||||||
--bg: #ffffff;
|
|
||||||
--card-bg: #f4f4f4;
|
|
||||||
--input-bg: #e8e8e8;
|
|
||||||
--border: #cccccc;
|
|
||||||
--border-light: #999999;
|
|
||||||
--text: #000000;
|
|
||||||
--text-muted: #333333;
|
|
||||||
--text-heading: #000000;
|
|
||||||
--btn-bg: #e0e0e0;
|
|
||||||
--btn-border: #777777;
|
|
||||||
--accent-green: #005cc5;
|
|
||||||
--accent-yellow: #cc5500;
|
|
||||||
--accent-red: #cc0000;
|
|
||||||
--jog-hi: #e0e0e0;
|
|
||||||
--jog-lo: #cccccc;
|
|
||||||
--jog-shadow: rgba(0,0,0,0.25);
|
|
||||||
--jog-inset: rgba(255,255,255,0.80);
|
|
||||||
--audio-level-bg: #e8e8e8;
|
|
||||||
--audio-level-border: #999999;
|
|
||||||
--audio-level-fill-start: #005cc5;
|
|
||||||
--audio-level-fill-end: #cc5500;
|
|
||||||
--filter-bg: #e8e8e8;
|
|
||||||
--filter-fg: #000000;
|
|
||||||
--filter-border: #999999;
|
|
||||||
--wavelength-fg: #444444;
|
|
||||||
--spectrum-bg: #f4f4f4;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Neon Disco style ─────────────────────────────────────────────────── */
|
|
||||||
[data-style="neon-disco"] {
|
|
||||||
--bg: #0d0015;
|
|
||||||
--card-bg: #180026;
|
|
||||||
--input-bg: #100018;
|
|
||||||
--border: #3d0060;
|
|
||||||
--border-light: #7700bb;
|
|
||||||
--text: #f5e0ff;
|
|
||||||
--text-muted: #b070d8;
|
|
||||||
--text-heading: #fce8ff;
|
|
||||||
--btn-bg: #2a0042;
|
|
||||||
--btn-border: #9900dd;
|
|
||||||
--accent-green: #ff10e0;
|
|
||||||
--accent-yellow: #39ff14;
|
|
||||||
--accent-red: #ff1460;
|
|
||||||
--jog-hi: #360058;
|
|
||||||
--jog-lo: #280042;
|
|
||||||
--jog-shadow: rgba(0,0,0,0.65);
|
|
||||||
--jog-inset: rgba(255,16,224,0.08);
|
|
||||||
--audio-level-bg: #180026;
|
|
||||||
--audio-level-border: #7700bb;
|
|
||||||
--audio-level-fill-start: #ff10e0;
|
|
||||||
--audio-level-fill-end: #39ff14;
|
|
||||||
--filter-bg: #2a0042;
|
|
||||||
--filter-fg: #f5e0ff;
|
|
||||||
--filter-border: #9900dd;
|
|
||||||
--wavelength-fg: #9055b8;
|
|
||||||
--spectrum-bg: #090010;
|
|
||||||
}
|
|
||||||
[data-style="neon-disco"][data-theme="light"] {
|
|
||||||
--bg: #faeeff;
|
|
||||||
--card-bg: #fff4ff;
|
|
||||||
--input-bg: #f2e0ff;
|
|
||||||
--border: #dda8f5;
|
|
||||||
--border-light: #cc80e8;
|
|
||||||
--text: #1a0030;
|
|
||||||
--text-muted: #7a30a0;
|
|
||||||
--text-heading: #1a0030;
|
|
||||||
--btn-bg: #f0d8ff;
|
|
||||||
--btn-border: #bb80dd;
|
|
||||||
--accent-green: #cc00a8;
|
|
||||||
--accent-yellow: #1f8800;
|
|
||||||
--accent-red: #cc0044;
|
|
||||||
--jog-hi: #f0d8ff;
|
|
||||||
--jog-lo: #e2c8f5;
|
|
||||||
--jog-shadow: rgba(60,0,100,0.18);
|
|
||||||
--jog-inset: rgba(255,255,255,0.72);
|
|
||||||
--audio-level-bg: #f0d8ff;
|
|
||||||
--audio-level-border: #cc80e8;
|
|
||||||
--audio-level-fill-start: #cc00a8;
|
|
||||||
--audio-level-fill-end: #1f8800;
|
|
||||||
--filter-bg: #f0d8ff;
|
|
||||||
--filter-fg: #1a0030;
|
|
||||||
--filter-border: #bb80dd;
|
|
||||||
--wavelength-fg: #7030a0;
|
|
||||||
--spectrum-bg: #f0d8ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
Bookmarks tab
|
Bookmarks tab
|
||||||
@@ -4025,9 +3794,7 @@ button:focus-visible, input:focus-visible, select:focus-visible {
|
|||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: 120;
|
z-index: 120;
|
||||||
padding: 1.25rem;
|
padding: 1.25rem;
|
||||||
background: rgba(7, 12, 18, 0.72);
|
background: rgba(7, 12, 18, 0.88);
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
-webkit-backdrop-filter: blur(10px);
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
@@ -4251,247 +4018,6 @@ button:focus-visible, input:focus-visible, select:focus-visible {
|
|||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Donald style ─────────────────────────────────────────────────────── */
|
|
||||||
[data-style="golden-rain"] {
|
|
||||||
--bg: #100c06;
|
|
||||||
--card-bg: #1a1209;
|
|
||||||
--input-bg: #140f08;
|
|
||||||
--border: #3f2d18;
|
|
||||||
--border-light: #6d4e23;
|
|
||||||
--text: #f3e4bf;
|
|
||||||
--text-muted: #aa9062;
|
|
||||||
--text-heading: #fff0ca;
|
|
||||||
--btn-bg: #2a1c0d;
|
|
||||||
--btn-border: #7c5928;
|
|
||||||
--accent-green: #dfac48;
|
|
||||||
--accent-yellow: #f4cd74;
|
|
||||||
--accent-red: #cf7d32;
|
|
||||||
--jog-hi: #392610;
|
|
||||||
--jog-lo: #24170b;
|
|
||||||
--jog-shadow: rgba(0,0,0,0.64);
|
|
||||||
--jog-inset: rgba(255,219,138,0.06);
|
|
||||||
--audio-level-bg: #1c130a;
|
|
||||||
--audio-level-border: #6d4e23;
|
|
||||||
--audio-level-fill-start: #dfac48;
|
|
||||||
--audio-level-fill-end: #f4cd74;
|
|
||||||
--filter-bg: #2b1d0f;
|
|
||||||
--filter-fg: #f3e4bf;
|
|
||||||
--filter-border: #7c5928;
|
|
||||||
--wavelength-fg: #ab8b52;
|
|
||||||
--spectrum-bg: #120d07;
|
|
||||||
}
|
|
||||||
[data-style="golden-rain"][data-theme="light"] {
|
|
||||||
--bg: #f7efdd;
|
|
||||||
--card-bg: #fff9ec;
|
|
||||||
--input-bg: #f0e3c6;
|
|
||||||
--border: #d4bc8a;
|
|
||||||
--border-light: #b99243;
|
|
||||||
--text: #3f2c10;
|
|
||||||
--text-muted: #7f6640;
|
|
||||||
--text-heading: #3a2609;
|
|
||||||
--btn-bg: #f0e3c6;
|
|
||||||
--btn-border: #b99243;
|
|
||||||
--accent-green: #a96d00;
|
|
||||||
--accent-yellow: #c88a16;
|
|
||||||
--accent-red: #b65316;
|
|
||||||
--jog-hi: #f2e5c8;
|
|
||||||
--jog-lo: #e3d1a8;
|
|
||||||
--jog-shadow: rgba(82,55,14,0.16);
|
|
||||||
--jog-inset: rgba(255,255,255,0.76);
|
|
||||||
--audio-level-bg: #f0e3c6;
|
|
||||||
--audio-level-border: #c5a15d;
|
|
||||||
--audio-level-fill-start: #a96d00;
|
|
||||||
--audio-level-fill-end: #d4a13a;
|
|
||||||
--filter-bg: #f0e3c6;
|
|
||||||
--filter-fg: #3f2c10;
|
|
||||||
--filter-border: #b99243;
|
|
||||||
--wavelength-fg: #87663a;
|
|
||||||
--spectrum-bg: #f5ecd9;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Amber style ──────────────────────────────────────────────────────── */
|
|
||||||
[data-style="amber"] {
|
|
||||||
--bg: #120706;
|
|
||||||
--card-bg: #1b0c0a;
|
|
||||||
--input-bg: #180907;
|
|
||||||
--border: #4c1a12;
|
|
||||||
--border-light: #7a2e1a;
|
|
||||||
--text: #ffe7d2;
|
|
||||||
--text-muted: #c78361;
|
|
||||||
--text-heading: #fff3e7;
|
|
||||||
--btn-bg: #2c110d;
|
|
||||||
--btn-border: #8f3a20;
|
|
||||||
--accent-green: #ff6f1f;
|
|
||||||
--accent-yellow: #ffb347;
|
|
||||||
--accent-red: #ff4a24;
|
|
||||||
--jog-hi: #381510;
|
|
||||||
--jog-lo: #24100c;
|
|
||||||
--jog-shadow: rgba(0,0,0,0.62);
|
|
||||||
--jog-inset: rgba(255,164,76,0.07);
|
|
||||||
--audio-level-bg: #1f0d0a;
|
|
||||||
--audio-level-border: #7a2e1a;
|
|
||||||
--audio-level-fill-start: #ff4a24;
|
|
||||||
--audio-level-fill-end: #ffb347;
|
|
||||||
--filter-bg: #2b120d;
|
|
||||||
--filter-fg: #ffe7d2;
|
|
||||||
--filter-border: #8f3a20;
|
|
||||||
--wavelength-fg: #d38d6a;
|
|
||||||
--spectrum-bg: #140907;
|
|
||||||
}
|
|
||||||
[data-style="amber"][data-theme="light"] {
|
|
||||||
--bg: #fff3ea;
|
|
||||||
--card-bg: #fff7f0;
|
|
||||||
--input-bg: #ffe9da;
|
|
||||||
--border: #efc7b1;
|
|
||||||
--border-light: #d9a487;
|
|
||||||
--text: #42180d;
|
|
||||||
--text-muted: #8a4b31;
|
|
||||||
--text-heading: #2f120a;
|
|
||||||
--btn-bg: #ffe2cf;
|
|
||||||
--btn-border: #cc8563;
|
|
||||||
--accent-green: #d24c12;
|
|
||||||
--accent-yellow: #d88400;
|
|
||||||
--accent-red: #c53114;
|
|
||||||
--jog-hi: #ffe2cf;
|
|
||||||
--jog-lo: #ffd5bc;
|
|
||||||
--jog-shadow: rgba(108,44,15,0.18);
|
|
||||||
--jog-inset: rgba(255,255,255,0.72);
|
|
||||||
--audio-level-bg: #ffe7d7;
|
|
||||||
--audio-level-border: #d9a487;
|
|
||||||
--audio-level-fill-start: #c53114;
|
|
||||||
--audio-level-fill-end: #d88400;
|
|
||||||
--filter-bg: #ffe2cf;
|
|
||||||
--filter-fg: #42180d;
|
|
||||||
--filter-border: #cc8563;
|
|
||||||
--wavelength-fg: #9a5a3a;
|
|
||||||
--spectrum-bg: #fff0e4;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Fire style ───────────────────────────────────────────────────────── */
|
|
||||||
[data-style="fire"] {
|
|
||||||
--bg: #140406;
|
|
||||||
--card-bg: #1d0708;
|
|
||||||
--input-bg: #1a0607;
|
|
||||||
--border: #551015;
|
|
||||||
--border-light: #8f1f26;
|
|
||||||
--text: #ffe6df;
|
|
||||||
--text-muted: #cf8d82;
|
|
||||||
--text-heading: #fff4ef;
|
|
||||||
--btn-bg: #2d0c0d;
|
|
||||||
--btn-border: #9d262b;
|
|
||||||
--accent-green: #d13a32;
|
|
||||||
--accent-yellow: #ff6a3d;
|
|
||||||
--accent-red: #c10f1f;
|
|
||||||
--jog-hi: #390f11;
|
|
||||||
--jog-lo: #25090b;
|
|
||||||
--jog-shadow: rgba(0,0,0,0.64);
|
|
||||||
--jog-inset: rgba(255,120,100,0.06);
|
|
||||||
--audio-level-bg: #22090b;
|
|
||||||
--audio-level-border: #8f1f26;
|
|
||||||
--audio-level-fill-start: #c10f1f;
|
|
||||||
--audio-level-fill-end: #ff6a3d;
|
|
||||||
--filter-bg: #2d0c0d;
|
|
||||||
--filter-fg: #ffe6df;
|
|
||||||
--filter-border: #9d262b;
|
|
||||||
--wavelength-fg: #d78d78;
|
|
||||||
--spectrum-bg: #150508;
|
|
||||||
}
|
|
||||||
[data-style="fire"][data-theme="light"] {
|
|
||||||
--bg: #fdf0ea;
|
|
||||||
--card-bg: #fff6f1;
|
|
||||||
--input-bg: #ffe5db;
|
|
||||||
--border: #e9b8aa;
|
|
||||||
--border-light: #d27c66;
|
|
||||||
--text: #4a110d;
|
|
||||||
--text-muted: #8a493f;
|
|
||||||
--text-heading: #340b08;
|
|
||||||
--btn-bg: #ffd9cc;
|
|
||||||
--btn-border: #c85b47;
|
|
||||||
--accent-green: #ba2d24;
|
|
||||||
--accent-yellow: #d95518;
|
|
||||||
--accent-red: #a80f1c;
|
|
||||||
--jog-hi: #ffd9cc;
|
|
||||||
--jog-lo: #ffcab9;
|
|
||||||
--jog-shadow: rgba(110,35,20,0.18);
|
|
||||||
--jog-inset: rgba(255,255,255,0.74);
|
|
||||||
--audio-level-bg: #ffe2d7;
|
|
||||||
--audio-level-border: #d27c66;
|
|
||||||
--audio-level-fill-start: #a80f1c;
|
|
||||||
--audio-level-fill-end: #d95518;
|
|
||||||
--filter-bg: #ffd9cc;
|
|
||||||
--filter-fg: #4a110d;
|
|
||||||
--filter-border: #c85b47;
|
|
||||||
--wavelength-fg: #9d5547;
|
|
||||||
--spectrum-bg: #ffede5;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Phosphor style ───────────────────────────────────────────────────── */
|
|
||||||
/* Classic green-phosphor CRT terminal aesthetic */
|
|
||||||
[data-style="phosphor"] {
|
|
||||||
--bg: #030a03;
|
|
||||||
--card-bg: #060e06;
|
|
||||||
--input-bg: #030a03;
|
|
||||||
--border: #0f2e0f;
|
|
||||||
--border-light: #1a4a1a;
|
|
||||||
--text: #a8e6a8;
|
|
||||||
--text-muted: #5a9a5a;
|
|
||||||
--text-heading: #c8f0c8;
|
|
||||||
--btn-bg: #0a1e0a;
|
|
||||||
--btn-border: #1e4a1e;
|
|
||||||
--accent-green: #39ff14;
|
|
||||||
--accent-yellow: #b8f060;
|
|
||||||
--accent-red: #ff4444;
|
|
||||||
--jog-hi: #0e2a0e;
|
|
||||||
--jog-lo: #081808;
|
|
||||||
--jog-shadow: rgba(0,0,0,0.65);
|
|
||||||
--jog-inset: rgba(57,255,20,0.07);
|
|
||||||
--audio-level-bg: #060e06;
|
|
||||||
--audio-level-border: #1a4a1a;
|
|
||||||
--audio-level-fill-start: #39ff14;
|
|
||||||
--audio-level-fill-end: #b8f060;
|
|
||||||
--filter-bg: #0a1e0a;
|
|
||||||
--filter-fg: #a8e6a8;
|
|
||||||
--filter-border: #1e4a1e;
|
|
||||||
--wavelength-fg: #4a8a4a;
|
|
||||||
--spectrum-bg: #010501;
|
|
||||||
}
|
|
||||||
[data-style="phosphor"] #freq {
|
|
||||||
color: #39ff14;
|
|
||||||
text-shadow: 0 0 8px rgba(57,255,20,0.55), 0 0 20px rgba(57,255,20,0.2);
|
|
||||||
}
|
|
||||||
[data-style="phosphor"] .signal-bar-fill,
|
|
||||||
[data-style="phosphor"] .meter-fill {
|
|
||||||
background: linear-gradient(90deg, #39ff14, #b8f060);
|
|
||||||
box-shadow: 0 0 6px rgba(57,255,20,0.45);
|
|
||||||
}
|
|
||||||
[data-style="phosphor"][data-theme="light"] {
|
|
||||||
--bg: #e8f5e8;
|
|
||||||
--card-bg: #f0faf0;
|
|
||||||
--input-bg: #dff0df;
|
|
||||||
--border: #b0d8b0;
|
|
||||||
--border-light: #80c080;
|
|
||||||
--text: #0a2a0a;
|
|
||||||
--text-muted: #2a6a2a;
|
|
||||||
--text-heading: #062006;
|
|
||||||
--btn-bg: #d0ebd0;
|
|
||||||
--btn-border: #4a8a4a;
|
|
||||||
--accent-green: #1a7a1a;
|
|
||||||
--accent-yellow: #4a8a00;
|
|
||||||
--accent-red: #cc2222;
|
|
||||||
--jog-hi: #d0ebd0;
|
|
||||||
--jog-lo: #bcdabc;
|
|
||||||
--jog-shadow: rgba(10,42,10,0.15);
|
|
||||||
--jog-inset: rgba(255,255,255,0.72);
|
|
||||||
--audio-level-bg: #d8edd8;
|
|
||||||
--audio-level-border: #80c080;
|
|
||||||
--audio-level-fill-start: #1a7a1a;
|
|
||||||
--audio-level-fill-end: #4a8a00;
|
|
||||||
--filter-bg: #d0ebd0;
|
|
||||||
--filter-fg: #0a2a0a;
|
|
||||||
--filter-border: #4a8a4a;
|
|
||||||
--wavelength-fg: #3a7a3a;
|
|
||||||
--spectrum-bg: #e0f0e0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =========================================================================
|
/* =========================================================================
|
||||||
Scheduler tab
|
Scheduler tab
|
||||||
@@ -5007,7 +4533,7 @@ button:focus-visible, input:focus-visible, select:focus-visible {
|
|||||||
border-bottom: 1px solid var(--border-light);
|
border-bottom: 1px solid var(--border-light);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
transition: background 0.1s;
|
transition: background-color 0.1s;
|
||||||
}
|
}
|
||||||
.bgd-checklist-row:last-child {
|
.bgd-checklist-row:last-child {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
@@ -5048,7 +4574,7 @@ button:focus-visible, input:focus-visible, select:focus-visible {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding: 0.25rem 0.65rem;
|
padding: 0.25rem 0.65rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.15s, color 0.15s;
|
transition: background-color 0.15s, color 0.15s;
|
||||||
}
|
}
|
||||||
.bgd-select-btn:hover {
|
.bgd-select-btn:hover {
|
||||||
background: var(--card-bg);
|
background: var(--card-bg);
|
||||||
@@ -5316,3 +4842,61 @@ button:focus-visible, input:focus-visible, select:focus-visible {
|
|||||||
.stats-bar-label { min-width: 4rem; font-size: 0.72rem; }
|
.stats-bar-label { min-width: 4rem; font-size: 0.72rem; }
|
||||||
.stats-section { padding: 0.8rem 0.85rem 0.9rem; }
|
.stats-section { padding: 0.8rem 0.85rem 0.9rem; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- Containment for off-screen/inactive content --- */
|
||||||
|
[data-tab]:not(.active) {
|
||||||
|
contain: content;
|
||||||
|
content-visibility: auto;
|
||||||
|
contain-intrinsic-size: auto 500px;
|
||||||
|
}
|
||||||
|
.spectrum-container, .waterfall-container {
|
||||||
|
contain: strict;
|
||||||
|
}
|
||||||
|
#tab-map:not(.active) {
|
||||||
|
content-visibility: auto;
|
||||||
|
contain-intrinsic-size: auto 600px;
|
||||||
|
}
|
||||||
|
#tab-statistics:not(.active) {
|
||||||
|
content-visibility: auto;
|
||||||
|
contain-intrinsic-size: auto 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Container queries for responsive components --- */
|
||||||
|
.controls-tray { container-type: inline-size; container-name: controls; }
|
||||||
|
.decode-history-table-wrap { container-type: inline-size; container-name: decode-table; }
|
||||||
|
|
||||||
|
@container controls (max-width: 600px) {
|
||||||
|
.controls-tray .controls-row { flex-wrap: wrap; }
|
||||||
|
}
|
||||||
|
@container decode-table (max-width: 500px) {
|
||||||
|
.decode-history-table th:nth-child(n+4),
|
||||||
|
.decode-history-table td:nth-child(n+4) { display: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Re-enable backdrop-filter blur for users who prefer full effects --- */
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
[data-effects="full"] .tab-bar {
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
}
|
||||||
|
[data-effects="full"] .controls-tray {
|
||||||
|
backdrop-filter: blur(14px);
|
||||||
|
-webkit-backdrop-filter: blur(14px);
|
||||||
|
}
|
||||||
|
[data-effects="full"] .freq-overlay {
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
-webkit-backdrop-filter: blur(16px);
|
||||||
|
}
|
||||||
|
[data-effects="full"] .shortcut-overlay-card {
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
[data-effects="full"] .decode-history-overlay {
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
[data-effects="full"] .mobile-bottom-nav {
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,472 @@
|
|||||||
|
/* ── Arctic style ─────────────────────────────────────────────────────── */
|
||||||
|
[data-style="arctic"] {
|
||||||
|
--bg: #242933;
|
||||||
|
--card-bg: #2e3440;
|
||||||
|
--input-bg: #242933;
|
||||||
|
--border: #3b4252;
|
||||||
|
--border-light: #4c566a;
|
||||||
|
--text: #d8dee9;
|
||||||
|
--text-muted: #8a9ab0;
|
||||||
|
--text-heading: #eceff4;
|
||||||
|
--btn-bg: #3b4252;
|
||||||
|
--btn-border: #5e6f88;
|
||||||
|
--accent-green: #88c0d0;
|
||||||
|
--accent-yellow: #ebcb8b;
|
||||||
|
--accent-red: #bf616a;
|
||||||
|
--jog-hi: #434c5e;
|
||||||
|
--jog-lo: #3b4252;
|
||||||
|
--jog-shadow: rgba(0,0,0,0.40);
|
||||||
|
--jog-inset: rgba(255,255,255,0.06);
|
||||||
|
--audio-level-bg: #2e3440;
|
||||||
|
--audio-level-border: #4c566a;
|
||||||
|
--audio-level-fill-start: #88c0d0;
|
||||||
|
--audio-level-fill-end: #ebcb8b;
|
||||||
|
--filter-bg: #3b4252;
|
||||||
|
--filter-fg: #d8dee9;
|
||||||
|
--filter-border: #5e6f88;
|
||||||
|
--wavelength-fg: #7a8ea8;
|
||||||
|
--spectrum-bg: #1e2530;
|
||||||
|
}
|
||||||
|
[data-style="arctic"][data-theme="light"] {
|
||||||
|
--bg: #e5e9f0;
|
||||||
|
--card-bg: #eceff4;
|
||||||
|
--input-bg: #d8dee9;
|
||||||
|
--border: #c5ccd8;
|
||||||
|
--border-light: #a8b2c0;
|
||||||
|
--text: #2e3440;
|
||||||
|
--text-muted: #4c566a;
|
||||||
|
--text-heading: #2e3440;
|
||||||
|
--btn-bg: #d8dee9;
|
||||||
|
--btn-border: #8fa3b8;
|
||||||
|
--accent-green: #5e81ac;
|
||||||
|
--accent-yellow: #c07a22;
|
||||||
|
--accent-red: #bf616a;
|
||||||
|
--jog-hi: #d8dee9;
|
||||||
|
--jog-lo: #c2cbd8;
|
||||||
|
--jog-shadow: rgba(46,52,64,0.18);
|
||||||
|
--jog-inset: rgba(255,255,255,0.70);
|
||||||
|
--audio-level-bg: #d0d6e0;
|
||||||
|
--audio-level-border: #a8b2c0;
|
||||||
|
--audio-level-fill-start: #5e81ac;
|
||||||
|
--audio-level-fill-end: #c07a22;
|
||||||
|
--filter-bg: #d8dee9;
|
||||||
|
--filter-fg: #2e3440;
|
||||||
|
--filter-border: #8fa3b8;
|
||||||
|
--wavelength-fg: #5a6a80;
|
||||||
|
--spectrum-bg: #dde1e9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Lime style ───────────────────────────────────────────────────────── */
|
||||||
|
[data-style="lime"] {
|
||||||
|
--bg: #1c1c17;
|
||||||
|
--card-bg: #272822;
|
||||||
|
--input-bg: #1c1c17;
|
||||||
|
--border: #3e3d32;
|
||||||
|
--border-light: #5c5c45;
|
||||||
|
--text: #f8f8f2;
|
||||||
|
--text-muted: #908980;
|
||||||
|
--text-heading: #f8f8f2;
|
||||||
|
--btn-bg: #3e3d32;
|
||||||
|
--btn-border: #75715e;
|
||||||
|
--accent-green: #a6e22e;
|
||||||
|
--accent-yellow: #e6db74;
|
||||||
|
--accent-red: #f92672;
|
||||||
|
--jog-hi: #49483e;
|
||||||
|
--jog-lo: #3e3d32;
|
||||||
|
--jog-shadow: rgba(0,0,0,0.45);
|
||||||
|
--jog-inset: rgba(255,255,255,0.05);
|
||||||
|
--audio-level-bg: #272822;
|
||||||
|
--audio-level-border: #5c5c45;
|
||||||
|
--audio-level-fill-start: #a6e22e;
|
||||||
|
--audio-level-fill-end: #e6db74;
|
||||||
|
--filter-bg: #3e3d32;
|
||||||
|
--filter-fg: #f8f8f2;
|
||||||
|
--filter-border: #75715e;
|
||||||
|
--wavelength-fg: #9c8f78;
|
||||||
|
--spectrum-bg: #181815;
|
||||||
|
}
|
||||||
|
[data-style="lime"][data-theme="light"] {
|
||||||
|
--bg: #f5f0e4;
|
||||||
|
--card-bg: #fdf9f2;
|
||||||
|
--input-bg: #ede8d8;
|
||||||
|
--border: #d8d0bb;
|
||||||
|
--border-light: #c0b89e;
|
||||||
|
--text: #272822;
|
||||||
|
--text-muted: #6e6a56;
|
||||||
|
--text-heading: #272822;
|
||||||
|
--btn-bg: #ede8d8;
|
||||||
|
--btn-border: #b0a888;
|
||||||
|
--accent-green: #5f8700;
|
||||||
|
--accent-yellow: #9a7200;
|
||||||
|
--accent-red: #c60052;
|
||||||
|
--jog-hi: #ede8d8;
|
||||||
|
--jog-lo: #ddd8c8;
|
||||||
|
--jog-shadow: rgba(39,40,34,0.18);
|
||||||
|
--jog-inset: rgba(255,255,255,0.75);
|
||||||
|
--audio-level-bg: #ede8d8;
|
||||||
|
--audio-level-border: #c0b89e;
|
||||||
|
--audio-level-fill-start: #5f8700;
|
||||||
|
--audio-level-fill-end: #9a7200;
|
||||||
|
--filter-bg: #ede8d8;
|
||||||
|
--filter-fg: #272822;
|
||||||
|
--filter-border: #b0a888;
|
||||||
|
--wavelength-fg: #7a7260;
|
||||||
|
--spectrum-bg: #ede8d8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Contrast style ───────────────────────────────────────────────────── */
|
||||||
|
[data-style="contrast"] {
|
||||||
|
--bg: #000000;
|
||||||
|
--card-bg: #0a0a0a;
|
||||||
|
--input-bg: #111111;
|
||||||
|
--border: #333333;
|
||||||
|
--border-light: #555555;
|
||||||
|
--text: #ffffff;
|
||||||
|
--text-muted: #bbbbbb;
|
||||||
|
--text-heading: #ffffff;
|
||||||
|
--btn-bg: #1a1a1a;
|
||||||
|
--btn-border: #666666;
|
||||||
|
--accent-green: #00ff88;
|
||||||
|
--accent-yellow: #ffcc00;
|
||||||
|
--accent-red: #ff3344;
|
||||||
|
--jog-hi: #2a2a2a;
|
||||||
|
--jog-lo: #1a1a1a;
|
||||||
|
--jog-shadow: rgba(0,0,0,0.60);
|
||||||
|
--jog-inset: rgba(255,255,255,0.08);
|
||||||
|
--audio-level-bg: #111111;
|
||||||
|
--audio-level-border: #555555;
|
||||||
|
--audio-level-fill-start: #00ff88;
|
||||||
|
--audio-level-fill-end: #ffcc00;
|
||||||
|
--filter-bg: #1a1a1a;
|
||||||
|
--filter-fg: #ffffff;
|
||||||
|
--filter-border: #666666;
|
||||||
|
--wavelength-fg: #aaaaaa;
|
||||||
|
--spectrum-bg: #000000;
|
||||||
|
}
|
||||||
|
[data-style="contrast"][data-theme="light"] {
|
||||||
|
--bg: #ffffff;
|
||||||
|
--card-bg: #f4f4f4;
|
||||||
|
--input-bg: #e8e8e8;
|
||||||
|
--border: #cccccc;
|
||||||
|
--border-light: #999999;
|
||||||
|
--text: #000000;
|
||||||
|
--text-muted: #333333;
|
||||||
|
--text-heading: #000000;
|
||||||
|
--btn-bg: #e0e0e0;
|
||||||
|
--btn-border: #777777;
|
||||||
|
--accent-green: #005cc5;
|
||||||
|
--accent-yellow: #cc5500;
|
||||||
|
--accent-red: #cc0000;
|
||||||
|
--jog-hi: #e0e0e0;
|
||||||
|
--jog-lo: #cccccc;
|
||||||
|
--jog-shadow: rgba(0,0,0,0.25);
|
||||||
|
--jog-inset: rgba(255,255,255,0.80);
|
||||||
|
--audio-level-bg: #e8e8e8;
|
||||||
|
--audio-level-border: #999999;
|
||||||
|
--audio-level-fill-start: #005cc5;
|
||||||
|
--audio-level-fill-end: #cc5500;
|
||||||
|
--filter-bg: #e8e8e8;
|
||||||
|
--filter-fg: #000000;
|
||||||
|
--filter-border: #999999;
|
||||||
|
--wavelength-fg: #444444;
|
||||||
|
--spectrum-bg: #f4f4f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Neon Disco style ─────────────────────────────────────────────────── */
|
||||||
|
[data-style="neon-disco"] {
|
||||||
|
--bg: #0d0015;
|
||||||
|
--card-bg: #180026;
|
||||||
|
--input-bg: #100018;
|
||||||
|
--border: #3d0060;
|
||||||
|
--border-light: #7700bb;
|
||||||
|
--text: #f5e0ff;
|
||||||
|
--text-muted: #b070d8;
|
||||||
|
--text-heading: #fce8ff;
|
||||||
|
--btn-bg: #2a0042;
|
||||||
|
--btn-border: #9900dd;
|
||||||
|
--accent-green: #ff10e0;
|
||||||
|
--accent-yellow: #39ff14;
|
||||||
|
--accent-red: #ff1460;
|
||||||
|
--jog-hi: #360058;
|
||||||
|
--jog-lo: #280042;
|
||||||
|
--jog-shadow: rgba(0,0,0,0.65);
|
||||||
|
--jog-inset: rgba(255,16,224,0.08);
|
||||||
|
--audio-level-bg: #180026;
|
||||||
|
--audio-level-border: #7700bb;
|
||||||
|
--audio-level-fill-start: #ff10e0;
|
||||||
|
--audio-level-fill-end: #39ff14;
|
||||||
|
--filter-bg: #2a0042;
|
||||||
|
--filter-fg: #f5e0ff;
|
||||||
|
--filter-border: #9900dd;
|
||||||
|
--wavelength-fg: #9055b8;
|
||||||
|
--spectrum-bg: #090010;
|
||||||
|
}
|
||||||
|
[data-style="neon-disco"][data-theme="light"] {
|
||||||
|
--bg: #faeeff;
|
||||||
|
--card-bg: #fff4ff;
|
||||||
|
--input-bg: #f2e0ff;
|
||||||
|
--border: #dda8f5;
|
||||||
|
--border-light: #cc80e8;
|
||||||
|
--text: #1a0030;
|
||||||
|
--text-muted: #7a30a0;
|
||||||
|
--text-heading: #1a0030;
|
||||||
|
--btn-bg: #f0d8ff;
|
||||||
|
--btn-border: #bb80dd;
|
||||||
|
--accent-green: #cc00a8;
|
||||||
|
--accent-yellow: #1f8800;
|
||||||
|
--accent-red: #cc0044;
|
||||||
|
--jog-hi: #f0d8ff;
|
||||||
|
--jog-lo: #e2c8f5;
|
||||||
|
--jog-shadow: rgba(60,0,100,0.18);
|
||||||
|
--jog-inset: rgba(255,255,255,0.72);
|
||||||
|
--audio-level-bg: #f0d8ff;
|
||||||
|
--audio-level-border: #cc80e8;
|
||||||
|
--audio-level-fill-start: #cc00a8;
|
||||||
|
--audio-level-fill-end: #1f8800;
|
||||||
|
--filter-bg: #f0d8ff;
|
||||||
|
--filter-fg: #1a0030;
|
||||||
|
--filter-border: #bb80dd;
|
||||||
|
--wavelength-fg: #7030a0;
|
||||||
|
--spectrum-bg: #f0d8ff;
|
||||||
|
}
|
||||||
|
/* ── Donald style ─────────────────────────────────────────────────────── */
|
||||||
|
[data-style="golden-rain"] {
|
||||||
|
--bg: #100c06;
|
||||||
|
--card-bg: #1a1209;
|
||||||
|
--input-bg: #140f08;
|
||||||
|
--border: #3f2d18;
|
||||||
|
--border-light: #6d4e23;
|
||||||
|
--text: #f3e4bf;
|
||||||
|
--text-muted: #aa9062;
|
||||||
|
--text-heading: #fff0ca;
|
||||||
|
--btn-bg: #2a1c0d;
|
||||||
|
--btn-border: #7c5928;
|
||||||
|
--accent-green: #dfac48;
|
||||||
|
--accent-yellow: #f4cd74;
|
||||||
|
--accent-red: #cf7d32;
|
||||||
|
--jog-hi: #392610;
|
||||||
|
--jog-lo: #24170b;
|
||||||
|
--jog-shadow: rgba(0,0,0,0.64);
|
||||||
|
--jog-inset: rgba(255,219,138,0.06);
|
||||||
|
--audio-level-bg: #1c130a;
|
||||||
|
--audio-level-border: #6d4e23;
|
||||||
|
--audio-level-fill-start: #dfac48;
|
||||||
|
--audio-level-fill-end: #f4cd74;
|
||||||
|
--filter-bg: #2b1d0f;
|
||||||
|
--filter-fg: #f3e4bf;
|
||||||
|
--filter-border: #7c5928;
|
||||||
|
--wavelength-fg: #ab8b52;
|
||||||
|
--spectrum-bg: #120d07;
|
||||||
|
}
|
||||||
|
[data-style="golden-rain"][data-theme="light"] {
|
||||||
|
--bg: #f7efdd;
|
||||||
|
--card-bg: #fff9ec;
|
||||||
|
--input-bg: #f0e3c6;
|
||||||
|
--border: #d4bc8a;
|
||||||
|
--border-light: #b99243;
|
||||||
|
--text: #3f2c10;
|
||||||
|
--text-muted: #7f6640;
|
||||||
|
--text-heading: #3a2609;
|
||||||
|
--btn-bg: #f0e3c6;
|
||||||
|
--btn-border: #b99243;
|
||||||
|
--accent-green: #a96d00;
|
||||||
|
--accent-yellow: #c88a16;
|
||||||
|
--accent-red: #b65316;
|
||||||
|
--jog-hi: #f2e5c8;
|
||||||
|
--jog-lo: #e3d1a8;
|
||||||
|
--jog-shadow: rgba(82,55,14,0.16);
|
||||||
|
--jog-inset: rgba(255,255,255,0.76);
|
||||||
|
--audio-level-bg: #f0e3c6;
|
||||||
|
--audio-level-border: #c5a15d;
|
||||||
|
--audio-level-fill-start: #a96d00;
|
||||||
|
--audio-level-fill-end: #d4a13a;
|
||||||
|
--filter-bg: #f0e3c6;
|
||||||
|
--filter-fg: #3f2c10;
|
||||||
|
--filter-border: #b99243;
|
||||||
|
--wavelength-fg: #87663a;
|
||||||
|
--spectrum-bg: #f5ecd9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Amber style ──────────────────────────────────────────────────────── */
|
||||||
|
[data-style="amber"] {
|
||||||
|
--bg: #120706;
|
||||||
|
--card-bg: #1b0c0a;
|
||||||
|
--input-bg: #180907;
|
||||||
|
--border: #4c1a12;
|
||||||
|
--border-light: #7a2e1a;
|
||||||
|
--text: #ffe7d2;
|
||||||
|
--text-muted: #c78361;
|
||||||
|
--text-heading: #fff3e7;
|
||||||
|
--btn-bg: #2c110d;
|
||||||
|
--btn-border: #8f3a20;
|
||||||
|
--accent-green: #ff6f1f;
|
||||||
|
--accent-yellow: #ffb347;
|
||||||
|
--accent-red: #ff4a24;
|
||||||
|
--jog-hi: #381510;
|
||||||
|
--jog-lo: #24100c;
|
||||||
|
--jog-shadow: rgba(0,0,0,0.62);
|
||||||
|
--jog-inset: rgba(255,164,76,0.07);
|
||||||
|
--audio-level-bg: #1f0d0a;
|
||||||
|
--audio-level-border: #7a2e1a;
|
||||||
|
--audio-level-fill-start: #ff4a24;
|
||||||
|
--audio-level-fill-end: #ffb347;
|
||||||
|
--filter-bg: #2b120d;
|
||||||
|
--filter-fg: #ffe7d2;
|
||||||
|
--filter-border: #8f3a20;
|
||||||
|
--wavelength-fg: #d38d6a;
|
||||||
|
--spectrum-bg: #140907;
|
||||||
|
}
|
||||||
|
[data-style="amber"][data-theme="light"] {
|
||||||
|
--bg: #fff3ea;
|
||||||
|
--card-bg: #fff7f0;
|
||||||
|
--input-bg: #ffe9da;
|
||||||
|
--border: #efc7b1;
|
||||||
|
--border-light: #d9a487;
|
||||||
|
--text: #42180d;
|
||||||
|
--text-muted: #8a4b31;
|
||||||
|
--text-heading: #2f120a;
|
||||||
|
--btn-bg: #ffe2cf;
|
||||||
|
--btn-border: #cc8563;
|
||||||
|
--accent-green: #d24c12;
|
||||||
|
--accent-yellow: #d88400;
|
||||||
|
--accent-red: #c53114;
|
||||||
|
--jog-hi: #ffe2cf;
|
||||||
|
--jog-lo: #ffd5bc;
|
||||||
|
--jog-shadow: rgba(108,44,15,0.18);
|
||||||
|
--jog-inset: rgba(255,255,255,0.72);
|
||||||
|
--audio-level-bg: #ffe7d7;
|
||||||
|
--audio-level-border: #d9a487;
|
||||||
|
--audio-level-fill-start: #c53114;
|
||||||
|
--audio-level-fill-end: #d88400;
|
||||||
|
--filter-bg: #ffe2cf;
|
||||||
|
--filter-fg: #42180d;
|
||||||
|
--filter-border: #cc8563;
|
||||||
|
--wavelength-fg: #9a5a3a;
|
||||||
|
--spectrum-bg: #fff0e4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Fire style ───────────────────────────────────────────────────────── */
|
||||||
|
[data-style="fire"] {
|
||||||
|
--bg: #140406;
|
||||||
|
--card-bg: #1d0708;
|
||||||
|
--input-bg: #1a0607;
|
||||||
|
--border: #551015;
|
||||||
|
--border-light: #8f1f26;
|
||||||
|
--text: #ffe6df;
|
||||||
|
--text-muted: #cf8d82;
|
||||||
|
--text-heading: #fff4ef;
|
||||||
|
--btn-bg: #2d0c0d;
|
||||||
|
--btn-border: #9d262b;
|
||||||
|
--accent-green: #d13a32;
|
||||||
|
--accent-yellow: #ff6a3d;
|
||||||
|
--accent-red: #c10f1f;
|
||||||
|
--jog-hi: #390f11;
|
||||||
|
--jog-lo: #25090b;
|
||||||
|
--jog-shadow: rgba(0,0,0,0.64);
|
||||||
|
--jog-inset: rgba(255,120,100,0.06);
|
||||||
|
--audio-level-bg: #22090b;
|
||||||
|
--audio-level-border: #8f1f26;
|
||||||
|
--audio-level-fill-start: #c10f1f;
|
||||||
|
--audio-level-fill-end: #ff6a3d;
|
||||||
|
--filter-bg: #2d0c0d;
|
||||||
|
--filter-fg: #ffe6df;
|
||||||
|
--filter-border: #9d262b;
|
||||||
|
--wavelength-fg: #d78d78;
|
||||||
|
--spectrum-bg: #150508;
|
||||||
|
}
|
||||||
|
[data-style="fire"][data-theme="light"] {
|
||||||
|
--bg: #fdf0ea;
|
||||||
|
--card-bg: #fff6f1;
|
||||||
|
--input-bg: #ffe5db;
|
||||||
|
--border: #e9b8aa;
|
||||||
|
--border-light: #d27c66;
|
||||||
|
--text: #4a110d;
|
||||||
|
--text-muted: #8a493f;
|
||||||
|
--text-heading: #340b08;
|
||||||
|
--btn-bg: #ffd9cc;
|
||||||
|
--btn-border: #c85b47;
|
||||||
|
--accent-green: #ba2d24;
|
||||||
|
--accent-yellow: #d95518;
|
||||||
|
--accent-red: #a80f1c;
|
||||||
|
--jog-hi: #ffd9cc;
|
||||||
|
--jog-lo: #ffcab9;
|
||||||
|
--jog-shadow: rgba(110,35,20,0.18);
|
||||||
|
--jog-inset: rgba(255,255,255,0.74);
|
||||||
|
--audio-level-bg: #ffe2d7;
|
||||||
|
--audio-level-border: #d27c66;
|
||||||
|
--audio-level-fill-start: #a80f1c;
|
||||||
|
--audio-level-fill-end: #d95518;
|
||||||
|
--filter-bg: #ffd9cc;
|
||||||
|
--filter-fg: #4a110d;
|
||||||
|
--filter-border: #c85b47;
|
||||||
|
--wavelength-fg: #9d5547;
|
||||||
|
--spectrum-bg: #ffede5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Phosphor style ───────────────────────────────────────────────────── */
|
||||||
|
/* Classic green-phosphor CRT terminal aesthetic */
|
||||||
|
[data-style="phosphor"] {
|
||||||
|
--bg: #030a03;
|
||||||
|
--card-bg: #060e06;
|
||||||
|
--input-bg: #030a03;
|
||||||
|
--border: #0f2e0f;
|
||||||
|
--border-light: #1a4a1a;
|
||||||
|
--text: #a8e6a8;
|
||||||
|
--text-muted: #5a9a5a;
|
||||||
|
--text-heading: #c8f0c8;
|
||||||
|
--btn-bg: #0a1e0a;
|
||||||
|
--btn-border: #1e4a1e;
|
||||||
|
--accent-green: #39ff14;
|
||||||
|
--accent-yellow: #b8f060;
|
||||||
|
--accent-red: #ff4444;
|
||||||
|
--jog-hi: #0e2a0e;
|
||||||
|
--jog-lo: #081808;
|
||||||
|
--jog-shadow: rgba(0,0,0,0.65);
|
||||||
|
--jog-inset: rgba(57,255,20,0.07);
|
||||||
|
--audio-level-bg: #060e06;
|
||||||
|
--audio-level-border: #1a4a1a;
|
||||||
|
--audio-level-fill-start: #39ff14;
|
||||||
|
--audio-level-fill-end: #b8f060;
|
||||||
|
--filter-bg: #0a1e0a;
|
||||||
|
--filter-fg: #a8e6a8;
|
||||||
|
--filter-border: #1e4a1e;
|
||||||
|
--wavelength-fg: #4a8a4a;
|
||||||
|
--spectrum-bg: #010501;
|
||||||
|
}
|
||||||
|
[data-style="phosphor"] #freq {
|
||||||
|
color: #39ff14;
|
||||||
|
text-shadow: 0 0 8px rgba(57,255,20,0.55), 0 0 20px rgba(57,255,20,0.2);
|
||||||
|
}
|
||||||
|
[data-style="phosphor"] .signal-bar-fill,
|
||||||
|
[data-style="phosphor"] .meter-fill {
|
||||||
|
background: linear-gradient(90deg, #39ff14, #b8f060);
|
||||||
|
box-shadow: 0 0 6px rgba(57,255,20,0.45);
|
||||||
|
}
|
||||||
|
[data-style="phosphor"][data-theme="light"] {
|
||||||
|
--bg: #e8f5e8;
|
||||||
|
--card-bg: #f0faf0;
|
||||||
|
--input-bg: #dff0df;
|
||||||
|
--border: #b0d8b0;
|
||||||
|
--border-light: #80c080;
|
||||||
|
--text: #0a2a0a;
|
||||||
|
--text-muted: #2a6a2a;
|
||||||
|
--text-heading: #062006;
|
||||||
|
--btn-bg: #d0ebd0;
|
||||||
|
--btn-border: #4a8a4a;
|
||||||
|
--accent-green: #1a7a1a;
|
||||||
|
--accent-yellow: #4a8a00;
|
||||||
|
--accent-red: #cc2222;
|
||||||
|
--jog-hi: #d0ebd0;
|
||||||
|
--jog-lo: #bcdabc;
|
||||||
|
--jog-shadow: rgba(10,42,10,0.15);
|
||||||
|
--jog-inset: rgba(255,255,255,0.72);
|
||||||
|
--audio-level-bg: #d8edd8;
|
||||||
|
--audio-level-border: #80c080;
|
||||||
|
--audio-level-fill-start: #1a7a1a;
|
||||||
|
--audio-level-fill-end: #4a8a00;
|
||||||
|
--filter-bg: #d0ebd0;
|
||||||
|
--filter-fg: #0a2a0a;
|
||||||
|
--filter-border: #4a8a4a;
|
||||||
|
--wavelength-fg: #3a7a3a;
|
||||||
|
--spectrum-bg: #e0f0e0;
|
||||||
|
}
|
||||||
@@ -4,6 +4,10 @@
|
|||||||
const cssColorCache = new Map();
|
const cssColorCache = new Map();
|
||||||
let cssColorProbe = null;
|
let cssColorProbe = null;
|
||||||
|
|
||||||
|
function clearCssColorCache() {
|
||||||
|
cssColorCache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
function ensureCssColorProbe() {
|
function ensureCssColorProbe() {
|
||||||
if (cssColorProbe) return cssColorProbe;
|
if (cssColorProbe) return cssColorProbe;
|
||||||
const el = document.createElement("span");
|
const el = document.createElement("span");
|
||||||
@@ -523,4 +527,5 @@
|
|||||||
global.trxParseCssColor = parseCssColor;
|
global.trxParseCssColor = parseCssColor;
|
||||||
global.trxHslToRgba = hslToRgba;
|
global.trxHslToRgba = hslToRgba;
|
||||||
global.createTrxWebGlRenderer = createRenderer;
|
global.createTrxWebGlRenderer = createRenderer;
|
||||||
|
global.trxClearCssColorCache = clearCssColorCache;
|
||||||
})(window);
|
})(window);
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ macro_rules! define_gz_cache {
|
|||||||
|
|
||||||
define_gz_cache!(gz_index_html, status::index_html(), "index.html");
|
define_gz_cache!(gz_index_html, status::index_html(), "index.html");
|
||||||
define_gz_cache!(gz_style_css, status::STYLE_CSS, "style.css");
|
define_gz_cache!(gz_style_css, status::STYLE_CSS, "style.css");
|
||||||
|
define_gz_cache!(gz_themes_css, status::THEMES_CSS, "themes.css");
|
||||||
define_gz_cache!(gz_app_js, status::APP_JS, "app.js");
|
define_gz_cache!(gz_app_js, status::APP_JS, "app.js");
|
||||||
define_gz_cache!(
|
define_gz_cache!(
|
||||||
gz_decode_history_worker_js,
|
gz_decode_history_worker_js,
|
||||||
@@ -74,37 +75,37 @@ define_gz_cache!(gz_bandplan_json, status::BANDPLAN_JSON, "bandplan.json");
|
|||||||
#[get("/")]
|
#[get("/")]
|
||||||
pub(crate) async fn index(req: HttpRequest) -> impl Responder {
|
pub(crate) async fn index(req: HttpRequest) -> impl Responder {
|
||||||
let c = gz_index_html();
|
let c = gz_index_html();
|
||||||
static_asset_response(&req, "text/html; charset=utf-8", &c.gz, &c.etag)
|
static_asset_response(&req, "text/html; charset=utf-8", c)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/map")]
|
#[get("/map")]
|
||||||
pub(crate) async fn map_index(req: HttpRequest) -> impl Responder {
|
pub(crate) async fn map_index(req: HttpRequest) -> impl Responder {
|
||||||
let c = gz_index_html();
|
let c = gz_index_html();
|
||||||
static_asset_response(&req, "text/html; charset=utf-8", &c.gz, &c.etag)
|
static_asset_response(&req, "text/html; charset=utf-8", c)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/digital-modes")]
|
#[get("/digital-modes")]
|
||||||
pub(crate) async fn digital_modes_index(req: HttpRequest) -> impl Responder {
|
pub(crate) async fn digital_modes_index(req: HttpRequest) -> impl Responder {
|
||||||
let c = gz_index_html();
|
let c = gz_index_html();
|
||||||
static_asset_response(&req, "text/html; charset=utf-8", &c.gz, &c.etag)
|
static_asset_response(&req, "text/html; charset=utf-8", c)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/recorder")]
|
#[get("/recorder")]
|
||||||
pub(crate) async fn recorder_index(req: HttpRequest) -> impl Responder {
|
pub(crate) async fn recorder_index(req: HttpRequest) -> impl Responder {
|
||||||
let c = gz_index_html();
|
let c = gz_index_html();
|
||||||
static_asset_response(&req, "text/html; charset=utf-8", &c.gz, &c.etag)
|
static_asset_response(&req, "text/html; charset=utf-8", c)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/settings")]
|
#[get("/settings")]
|
||||||
pub(crate) async fn settings_index(req: HttpRequest) -> impl Responder {
|
pub(crate) async fn settings_index(req: HttpRequest) -> impl Responder {
|
||||||
let c = gz_index_html();
|
let c = gz_index_html();
|
||||||
static_asset_response(&req, "text/html; charset=utf-8", &c.gz, &c.etag)
|
static_asset_response(&req, "text/html; charset=utf-8", c)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/about")]
|
#[get("/about")]
|
||||||
pub(crate) async fn about_index(req: HttpRequest) -> impl Responder {
|
pub(crate) async fn about_index(req: HttpRequest) -> impl Responder {
|
||||||
let c = gz_index_html();
|
let c = gz_index_html();
|
||||||
static_asset_response(&req, "text/html; charset=utf-8", &c.gz, &c.etag)
|
static_asset_response(&req, "text/html; charset=utf-8", c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -142,7 +143,13 @@ pub(crate) async fn logo() -> impl Responder {
|
|||||||
#[get("/style.css")]
|
#[get("/style.css")]
|
||||||
pub(crate) async fn style_css(req: HttpRequest) -> impl Responder {
|
pub(crate) async fn style_css(req: HttpRequest) -> impl Responder {
|
||||||
let c = gz_style_css();
|
let c = gz_style_css();
|
||||||
static_asset_response(&req, "text/css; charset=utf-8", &c.gz, &c.etag)
|
static_asset_response(&req, "text/css; charset=utf-8", c)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/themes.css")]
|
||||||
|
pub(crate) async fn themes_css(req: HttpRequest) -> impl Responder {
|
||||||
|
let c = gz_themes_css();
|
||||||
|
static_asset_response(&req, "text/css; charset=utf-8", c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -155,8 +162,7 @@ pub(crate) async fn app_js(req: HttpRequest) -> impl Responder {
|
|||||||
static_asset_response(
|
static_asset_response(
|
||||||
&req,
|
&req,
|
||||||
"application/javascript; charset=utf-8",
|
"application/javascript; charset=utf-8",
|
||||||
&c.gz,
|
c,
|
||||||
&c.etag,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,8 +172,7 @@ pub(crate) async fn decode_history_worker_js(req: HttpRequest) -> impl Responder
|
|||||||
static_asset_response(
|
static_asset_response(
|
||||||
&req,
|
&req,
|
||||||
"application/javascript; charset=utf-8",
|
"application/javascript; charset=utf-8",
|
||||||
&c.gz,
|
c,
|
||||||
&c.etag,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,8 +182,7 @@ pub(crate) async fn webgl_renderer_js(req: HttpRequest) -> impl Responder {
|
|||||||
static_asset_response(
|
static_asset_response(
|
||||||
&req,
|
&req,
|
||||||
"application/javascript; charset=utf-8",
|
"application/javascript; charset=utf-8",
|
||||||
&c.gz,
|
c,
|
||||||
&c.etag,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,8 +192,7 @@ pub(crate) async fn leaflet_ais_tracksymbol_js(req: HttpRequest) -> impl Respond
|
|||||||
static_asset_response(
|
static_asset_response(
|
||||||
&req,
|
&req,
|
||||||
"application/javascript; charset=utf-8",
|
"application/javascript; charset=utf-8",
|
||||||
&c.gz,
|
c,
|
||||||
&c.etag,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,8 +202,7 @@ pub(crate) async fn aprs_js(req: HttpRequest) -> impl Responder {
|
|||||||
static_asset_response(
|
static_asset_response(
|
||||||
&req,
|
&req,
|
||||||
"application/javascript; charset=utf-8",
|
"application/javascript; charset=utf-8",
|
||||||
&c.gz,
|
c,
|
||||||
&c.etag,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,8 +212,7 @@ pub(crate) async fn hf_aprs_js(req: HttpRequest) -> impl Responder {
|
|||||||
static_asset_response(
|
static_asset_response(
|
||||||
&req,
|
&req,
|
||||||
"application/javascript; charset=utf-8",
|
"application/javascript; charset=utf-8",
|
||||||
&c.gz,
|
c,
|
||||||
&c.etag,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,8 +222,7 @@ pub(crate) async fn ais_js(req: HttpRequest) -> impl Responder {
|
|||||||
static_asset_response(
|
static_asset_response(
|
||||||
&req,
|
&req,
|
||||||
"application/javascript; charset=utf-8",
|
"application/javascript; charset=utf-8",
|
||||||
&c.gz,
|
c,
|
||||||
&c.etag,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,8 +232,7 @@ pub(crate) async fn vdes_js(req: HttpRequest) -> impl Responder {
|
|||||||
static_asset_response(
|
static_asset_response(
|
||||||
&req,
|
&req,
|
||||||
"application/javascript; charset=utf-8",
|
"application/javascript; charset=utf-8",
|
||||||
&c.gz,
|
c,
|
||||||
&c.etag,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,8 +242,7 @@ pub(crate) async fn ft8_js(req: HttpRequest) -> impl Responder {
|
|||||||
static_asset_response(
|
static_asset_response(
|
||||||
&req,
|
&req,
|
||||||
"application/javascript; charset=utf-8",
|
"application/javascript; charset=utf-8",
|
||||||
&c.gz,
|
c,
|
||||||
&c.etag,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,8 +252,7 @@ pub(crate) async fn ft4_js(req: HttpRequest) -> impl Responder {
|
|||||||
static_asset_response(
|
static_asset_response(
|
||||||
&req,
|
&req,
|
||||||
"application/javascript; charset=utf-8",
|
"application/javascript; charset=utf-8",
|
||||||
&c.gz,
|
c,
|
||||||
&c.etag,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,8 +262,7 @@ pub(crate) async fn ft2_js(req: HttpRequest) -> impl Responder {
|
|||||||
static_asset_response(
|
static_asset_response(
|
||||||
&req,
|
&req,
|
||||||
"application/javascript; charset=utf-8",
|
"application/javascript; charset=utf-8",
|
||||||
&c.gz,
|
c,
|
||||||
&c.etag,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,8 +272,7 @@ pub(crate) async fn wspr_js(req: HttpRequest) -> impl Responder {
|
|||||||
static_asset_response(
|
static_asset_response(
|
||||||
&req,
|
&req,
|
||||||
"application/javascript; charset=utf-8",
|
"application/javascript; charset=utf-8",
|
||||||
&c.gz,
|
c,
|
||||||
&c.etag,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,8 +282,7 @@ pub(crate) async fn cw_js(req: HttpRequest) -> impl Responder {
|
|||||||
static_asset_response(
|
static_asset_response(
|
||||||
&req,
|
&req,
|
||||||
"application/javascript; charset=utf-8",
|
"application/javascript; charset=utf-8",
|
||||||
&c.gz,
|
c,
|
||||||
&c.etag,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,8 +292,7 @@ pub(crate) async fn sat_js(req: HttpRequest) -> impl Responder {
|
|||||||
static_asset_response(
|
static_asset_response(
|
||||||
&req,
|
&req,
|
||||||
"application/javascript; charset=utf-8",
|
"application/javascript; charset=utf-8",
|
||||||
&c.gz,
|
c,
|
||||||
&c.etag,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,8 +302,7 @@ pub(crate) async fn bookmarks_js(req: HttpRequest) -> impl Responder {
|
|||||||
static_asset_response(
|
static_asset_response(
|
||||||
&req,
|
&req,
|
||||||
"application/javascript; charset=utf-8",
|
"application/javascript; charset=utf-8",
|
||||||
&c.gz,
|
c,
|
||||||
&c.etag,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,8 +312,7 @@ pub(crate) async fn scheduler_js(req: HttpRequest) -> impl Responder {
|
|||||||
static_asset_response(
|
static_asset_response(
|
||||||
&req,
|
&req,
|
||||||
"application/javascript; charset=utf-8",
|
"application/javascript; charset=utf-8",
|
||||||
&c.gz,
|
c,
|
||||||
&c.etag,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,8 +322,7 @@ pub(crate) async fn sat_scheduler_js(req: HttpRequest) -> impl Responder {
|
|||||||
static_asset_response(
|
static_asset_response(
|
||||||
&req,
|
&req,
|
||||||
"application/javascript; charset=utf-8",
|
"application/javascript; charset=utf-8",
|
||||||
&c.gz,
|
c,
|
||||||
&c.etag,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,8 +332,7 @@ pub(crate) async fn background_decode_js(req: HttpRequest) -> impl Responder {
|
|||||||
static_asset_response(
|
static_asset_response(
|
||||||
&req,
|
&req,
|
||||||
"application/javascript; charset=utf-8",
|
"application/javascript; charset=utf-8",
|
||||||
&c.gz,
|
c,
|
||||||
&c.etag,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,13 +342,12 @@ pub(crate) async fn vchan_js(req: HttpRequest) -> impl Responder {
|
|||||||
static_asset_response(
|
static_asset_response(
|
||||||
&req,
|
&req,
|
||||||
"application/javascript; charset=utf-8",
|
"application/javascript; charset=utf-8",
|
||||||
&c.gz,
|
c,
|
||||||
&c.etag,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/bandplan.json")]
|
#[get("/bandplan.json")]
|
||||||
pub(crate) async fn bandplan_json(req: HttpRequest) -> impl Responder {
|
pub(crate) async fn bandplan_json(req: HttpRequest) -> impl Responder {
|
||||||
let c = gz_bandplan_json();
|
let c = gz_bandplan_json();
|
||||||
static_asset_response(&req, "application/json; charset=utf-8", &c.gz, &c.etag)
|
static_asset_response(&req, "application/json; charset=utf-8", c)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -306,13 +306,13 @@ where
|
|||||||
.body(body)
|
.body(body)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pre-compressed (gzip) + ETag-aware response for immutable embedded assets.
|
/// Pre-compressed (gzip + brotli) + ETag-aware response for immutable embedded assets.
|
||||||
fn static_asset_response(
|
fn static_asset_response(
|
||||||
req: &HttpRequest,
|
req: &HttpRequest,
|
||||||
content_type: &'static str,
|
content_type: &'static str,
|
||||||
gz_bytes: &[u8],
|
entry: &GzCacheEntry,
|
||||||
etag: &str,
|
|
||||||
) -> HttpResponse {
|
) -> HttpResponse {
|
||||||
|
let etag = &entry.etag;
|
||||||
// Check If-None-Match for conditional GET.
|
// Check If-None-Match for conditional GET.
|
||||||
if let Some(inm) = req.headers().get(header::IF_NONE_MATCH) {
|
if let Some(inm) = req.headers().get(header::IF_NONE_MATCH) {
|
||||||
if let Ok(val) = inm.to_str() {
|
if let Ok(val) = inm.to_str() {
|
||||||
@@ -321,36 +321,54 @@ fn static_asset_response(
|
|||||||
.insert_header((header::ETAG, etag.to_owned()))
|
.insert_header((header::ETAG, etag.to_owned()))
|
||||||
.insert_header((
|
.insert_header((
|
||||||
header::CACHE_CONTROL,
|
header::CACHE_CONTROL,
|
||||||
"public, max-age=86400, must-revalidate",
|
"public, max-age=31536000, immutable",
|
||||||
))
|
))
|
||||||
.finish();
|
.finish();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Prefer brotli if client supports it.
|
||||||
|
let accept_enc = req
|
||||||
|
.headers()
|
||||||
|
.get(header::ACCEPT_ENCODING)
|
||||||
|
.and_then(|v| v.to_str().ok())
|
||||||
|
.unwrap_or("");
|
||||||
|
let (body, encoding) = if accept_enc.contains("br") {
|
||||||
|
(&entry.br, "br")
|
||||||
|
} else {
|
||||||
|
(&entry.gz, "gzip")
|
||||||
|
};
|
||||||
HttpResponse::Ok()
|
HttpResponse::Ok()
|
||||||
.insert_header((header::CONTENT_TYPE, content_type))
|
.insert_header((header::CONTENT_TYPE, content_type))
|
||||||
.insert_header((header::CONTENT_ENCODING, "gzip"))
|
.insert_header((header::CONTENT_ENCODING, encoding))
|
||||||
.insert_header((header::ETAG, etag.to_owned()))
|
.insert_header((header::ETAG, etag.to_owned()))
|
||||||
.insert_header((
|
.insert_header((
|
||||||
header::CACHE_CONTROL,
|
header::CACHE_CONTROL,
|
||||||
"public, max-age=86400, must-revalidate",
|
"public, max-age=31536000, immutable",
|
||||||
))
|
))
|
||||||
.body(Bytes::copy_from_slice(gz_bytes))
|
.body(Bytes::copy_from_slice(body))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cache entry for a pre-compressed asset: gzip bytes + ETag string.
|
/// Cache entry for a pre-compressed asset: gzip + brotli bytes + ETag string.
|
||||||
struct GzCacheEntry {
|
struct GzCacheEntry {
|
||||||
gz: Vec<u8>,
|
gz: Vec<u8>,
|
||||||
|
br: Vec<u8>,
|
||||||
etag: String,
|
etag: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compress `src` with gzip and build an ETag from the build version + asset name.
|
/// Compress `src` with gzip and brotli, and build an ETag from the build version + asset name.
|
||||||
fn gz_cache_entry(src: &[u8], name: &str) -> GzCacheEntry {
|
fn gz_cache_entry(src: &[u8], name: &str) -> GzCacheEntry {
|
||||||
|
// gzip
|
||||||
let mut encoder = GzEncoder::new(Vec::with_capacity(src.len() / 2), Compression::best());
|
let mut encoder = GzEncoder::new(Vec::with_capacity(src.len() / 2), Compression::best());
|
||||||
encoder.write_all(src).expect("gzip compress");
|
encoder.write_all(src).expect("gzip compress");
|
||||||
let gz = encoder.finish().expect("gzip finish");
|
let gz = encoder.finish().expect("gzip finish");
|
||||||
|
// brotli
|
||||||
|
let mut br = Vec::with_capacity(src.len() / 2);
|
||||||
|
let mut br_encoder = brotli::CompressorWriter::new(&mut br, 4096, 11, 22);
|
||||||
|
br_encoder.write_all(src).expect("brotli compress");
|
||||||
|
drop(br_encoder);
|
||||||
let etag = format!("\"{}:{}\"", status::build_version_tag(), name);
|
let etag = format!("\"{}:{}\"", status::build_version_tag(), name);
|
||||||
GzCacheEntry { gz, etag }
|
GzCacheEntry { gz, br, etag }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn require_control(
|
fn require_control(
|
||||||
@@ -626,6 +644,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
|||||||
.service(assets::favicon_png)
|
.service(assets::favicon_png)
|
||||||
.service(assets::logo)
|
.service(assets::logo)
|
||||||
.service(assets::style_css)
|
.service(assets::style_css)
|
||||||
|
.service(assets::themes_css)
|
||||||
.service(assets::app_js)
|
.service(assets::app_js)
|
||||||
.service(assets::decode_history_worker_js)
|
.service(assets::decode_history_worker_js)
|
||||||
.service(assets::webgl_renderer_js)
|
.service(assets::webgl_renderer_js)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const CLIENT_BUILD_DATE: &str = env!("TRX_CLIENT_BUILD_DATE");
|
|||||||
|
|
||||||
const INDEX_HTML: &str = include_str!("../assets/web/index.html");
|
const INDEX_HTML: &str = include_str!("../assets/web/index.html");
|
||||||
pub const STYLE_CSS: &str = include_str!("../assets/web/style.css");
|
pub const STYLE_CSS: &str = include_str!("../assets/web/style.css");
|
||||||
|
pub const THEMES_CSS: &str = include_str!("../assets/web/themes.css");
|
||||||
pub const APP_JS: &str = include_str!("../assets/web/app.js");
|
pub const APP_JS: &str = include_str!("../assets/web/app.js");
|
||||||
pub const DECODE_HISTORY_WORKER_JS: &str = include_str!("../assets/web/decode-history-worker.js");
|
pub const DECODE_HISTORY_WORKER_JS: &str = include_str!("../assets/web/decode-history-worker.js");
|
||||||
pub const WEBGL_RENDERER_JS: &str = include_str!("../assets/web/webgl-renderer.js");
|
pub const WEBGL_RENDERER_JS: &str = include_str!("../assets/web/webgl-renderer.js");
|
||||||
|
|||||||
Reference in New Issue
Block a user