diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js index edb216b..624e6f6 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js @@ -329,6 +329,7 @@ const overviewCanvas = document.getElementById("overview-canvas"); const overviewPeakHoldEl = document.getElementById("overview-peak-hold"); const themeToggleBtn = document.getElementById("theme-toggle"); const headerRigSwitchSelect = document.getElementById("header-rig-switch-select"); +const headerStylePickSelect = document.getElementById("header-style-pick-select"); let overviewPeakHoldMs = Number(loadSetting("overviewPeakHoldMs", 2000)); let lastControl; @@ -377,6 +378,106 @@ function setTheme(theme) { } } +// ── Style / palette system ──────────────────────────────────────────────────── +const CANVAS_PALETTE = { + original: { + dark: { + bg: "#0a0f18", + spectrumLine: "#00e676", spectrumFill: "rgba(0,230,118,0.10)", + spectrumGrid: "rgba(255,255,255,0.06)", spectrumLabel: "rgba(180,200,220,0.45)", + waveformLine: "rgba(94,234,212,0.92)", waveformPeak: "rgba(251,191,36,0.88)", + waveformGrid: "rgba(148,163,184,0.12)", waveformLabel: "rgba(203,213,225,0.72)", + waterfallHue: [225, 30], waterfallSat: 88, waterfallLight: [16, 68], waterfallAlpha: [0.28, 0.86], + }, + light: { + bg: "#eef3fb", + spectrumLine: "#007a47", spectrumFill: "rgba(0,110,70,0.12)", + spectrumGrid: "rgba(0,30,80,0.10)", spectrumLabel: "rgba(30,50,90,0.55)", + waveformLine: "rgba(17,94,89,0.95)", waveformPeak: "rgba(217,119,6,0.9)", + waveformGrid: "rgba(71,85,105,0.14)", waveformLabel: "rgba(51,65,85,0.72)", + waterfallHue: [210, 35], waterfallSat: 82, waterfallLight: [92, 40], waterfallAlpha: [0.42, 0.80], + }, + }, + nord: { + dark: { + bg: "#1e2530", + spectrumLine: "#88c0d0", spectrumFill: "rgba(136,192,208,0.12)", + spectrumGrid: "rgba(216,222,233,0.08)", spectrumLabel: "rgba(216,222,233,0.55)", + waveformLine: "rgba(136,192,208,0.92)", waveformPeak: "rgba(235,203,139,0.88)", + waveformGrid: "rgba(216,222,233,0.10)", waveformLabel: "rgba(216,222,233,0.65)", + waterfallHue: [212, 188], waterfallSat: 70, waterfallLight: [14, 58], waterfallAlpha: [0.28, 0.82], + }, + light: { + bg: "#dde1e9", + spectrumLine: "#5e81ac", spectrumFill: "rgba(94,129,172,0.14)", + spectrumGrid: "rgba(46,52,64,0.08)", spectrumLabel: "rgba(46,52,64,0.55)", + waveformLine: "rgba(94,129,172,0.95)", waveformPeak: "rgba(208,135,112,0.9)", + waveformGrid: "rgba(46,52,64,0.12)", waveformLabel: "rgba(46,52,64,0.65)", + waterfallHue: [215, 195], waterfallSat: 65, waterfallLight: [88, 45], waterfallAlpha: [0.35, 0.78], + }, + }, + monokai: { + dark: { + bg: "#181815", + spectrumLine: "#a6e22e", spectrumFill: "rgba(166,226,46,0.10)", + spectrumGrid: "rgba(248,248,242,0.05)", spectrumLabel: "rgba(248,248,242,0.45)", + waveformLine: "rgba(166,226,46,0.92)", waveformPeak: "rgba(230,219,116,0.88)", + waveformGrid: "rgba(248,248,242,0.08)", waveformLabel: "rgba(248,248,242,0.65)", + waterfallHue: [70, 38], waterfallSat: 80, waterfallLight: [12, 62], waterfallAlpha: [0.25, 0.88], + }, + light: { + bg: "#ede8d8", + spectrumLine: "#5f8700", spectrumFill: "rgba(95,135,0,0.12)", + spectrumGrid: "rgba(39,40,34,0.08)", spectrumLabel: "rgba(39,40,34,0.50)", + waveformLine: "rgba(95,135,0,0.95)", waveformPeak: "rgba(176,120,0,0.9)", + waveformGrid: "rgba(39,40,34,0.10)", waveformLabel: "rgba(39,40,34,0.60)", + waterfallHue: [75, 42], waterfallSat: 75, waterfallLight: [90, 42], waterfallAlpha: [0.35, 0.78], + }, + }, + contrast: { + dark: { + bg: "#000000", + spectrumLine: "#00ff88", spectrumFill: "rgba(0,255,136,0.12)", + spectrumGrid: "rgba(255,255,255,0.12)", spectrumLabel: "rgba(255,255,255,0.70)", + waveformLine: "rgba(0,255,136,0.95)", waveformPeak: "rgba(255,204,0,0.92)", + waveformGrid: "rgba(255,255,255,0.15)", waveformLabel: "rgba(255,255,255,0.80)", + waterfallHue: [150, 60], waterfallSat: 100, waterfallLight: [8, 55], waterfallAlpha: [0.30, 0.95], + }, + light: { + bg: "#f4f4f4", + spectrumLine: "#005cc5", spectrumFill: "rgba(0,92,197,0.12)", + spectrumGrid: "rgba(0,0,0,0.12)", spectrumLabel: "rgba(0,0,0,0.65)", + waveformLine: "rgba(0,92,197,0.95)", waveformPeak: "rgba(180,60,0,0.9)", + waveformGrid: "rgba(0,0,0,0.14)", waveformLabel: "rgba(0,0,0,0.70)", + waterfallHue: [220, 180], waterfallSat: 100, waterfallLight: [90, 42], waterfallAlpha: [0.35, 0.82], + }, + }, +}; + +function currentStyle() { + return document.documentElement.getAttribute("data-style") || "original"; +} + +function canvasPalette() { + const s = currentStyle(); + const t = currentTheme(); + return (CANVAS_PALETTE[s] ?? CANVAS_PALETTE.original)[t]; +} + +function setStyle(style) { + const valid = ["original", "nord", "monokai", "contrast"]; + const next = valid.includes(style) ? style : "original"; + if (next === "original") { + document.documentElement.removeAttribute("data-style"); + } else { + document.documentElement.setAttribute("data-style", next); + } + saveSetting("style", next); + if (headerStylePickSelect) headerStylePickSelect.value = next; + scheduleOverviewDraw(); + if (typeof scheduleSpectrumDraw === "function" && lastSpectrumData) scheduleSpectrumDraw(); +} + if (overviewPeakHoldEl) { if (!Number.isFinite(overviewPeakHoldMs) || overviewPeakHoldMs <= 0) { overviewPeakHoldMs = 2000; @@ -396,11 +497,22 @@ if (savedTheme === "light" || savedTheme === "dark") { setTheme(prefersLight ? "light" : "dark"); } +const savedStyle = loadSetting("style", "original"); +setStyle(savedStyle); + if (themeToggleBtn) { themeToggleBtn.addEventListener("click", () => { setTheme(currentTheme() === "dark" ? "light" : "dark"); updateMapBaseLayerForTheme(currentTheme()); scheduleOverviewDraw(); + if (typeof scheduleSpectrumDraw === "function" && lastSpectrumData) scheduleSpectrumDraw(); + }); +} + +if (headerStylePickSelect) { + headerStylePickSelect.addEventListener("change", () => { + setStyle(headerStylePickSelect.value); + updateMapBaseLayerForTheme(currentTheme()); }); } @@ -590,7 +702,7 @@ function drawHeaderSignalGraph() { if (!overviewCanvas) return; const ctx = overviewCanvas.getContext("2d"); if (!ctx) return; - const isLight = currentTheme() === "light"; + const pal = canvasPalette(); const dpr = window.devicePixelRatio || 1; const w = overviewCanvas.width / dpr; const h = overviewCanvas.height / dpr; @@ -600,14 +712,14 @@ function drawHeaderSignalGraph() { ctx.scale(dpr, dpr); ctx.clearRect(0, 0, w, h); if (lastSpectrumData && overviewWaterfallRows.length > 0) { - drawOverviewWaterfall(ctx, w, h, isLight); + drawOverviewWaterfall(ctx, w, h, pal); } else { - drawOverviewSignalHistory(ctx, w, h, isLight); + drawOverviewSignalHistory(ctx, w, h, pal); } ctx.restore(); } -function drawOverviewWaterfall(ctx, w, h, isLight) { +function drawOverviewWaterfall(ctx, w, h, pal) { const rows = overviewWaterfallRows.slice(-Math.max(1, Math.floor(h))); if (rows.length === 0) return; const rowH = h / rows.length; @@ -621,13 +733,13 @@ function drawOverviewWaterfall(ctx, w, h, isLight) { for (let x = 0; x < w; x += columnStep) { const frac = x / Math.max(1, w - 1); const binIdx = Math.min(endIdx, startIdx + Math.floor(frac * spanBins)); - ctx.fillStyle = waterfallColor(bins[binIdx], isLight); + ctx.fillStyle = waterfallColor(bins[binIdx], pal); ctx.fillRect(x, y, columnStep + 0.75, rowH + 1); } } } -function drawOverviewSignalHistory(ctx, w, h, isLight) { +function drawOverviewSignalHistory(ctx, w, h, pal) { const now = Date.now(); const samples = overviewSignalSamples.filter((sample) => now - sample.t <= HEADER_SIG_WINDOW_MS); if (samples.length === 0) return; @@ -642,10 +754,10 @@ function drawOverviewSignalHistory(ctx, w, h, isLight) { { val: 9, label: "S9" }, { val: 18, label: "S9+" }, ]; - ctx.strokeStyle = isLight ? "rgba(71, 85, 105, 0.14)" : "rgba(148, 163, 184, 0.12)"; + ctx.strokeStyle = pal.waveformGrid; ctx.lineWidth = 1; ctx.font = "11px sans-serif"; - ctx.fillStyle = isLight ? "rgba(51, 65, 85, 0.72)" : "rgba(203, 213, 225, 0.72)"; + ctx.fillStyle = pal.waveformLabel; ctx.textAlign = "right"; ctx.textBaseline = "middle"; for (const marker of gridMarkers) { @@ -664,7 +776,7 @@ function drawOverviewSignalHistory(ctx, w, h, isLight) { if (idx === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); - ctx.strokeStyle = isLight ? "rgba(17, 94, 89, 0.95)" : "rgba(94, 234, 212, 0.92)"; + ctx.strokeStyle = pal.waveformLine; ctx.lineWidth = 1.6; ctx.stroke(); @@ -682,20 +794,19 @@ function drawOverviewSignalHistory(ctx, w, h, isLight) { if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } - ctx.strokeStyle = isLight ? "rgba(217, 119, 6, 0.9)" : "rgba(251, 191, 36, 0.88)"; + ctx.strokeStyle = pal.waveformPeak; ctx.lineWidth = 1; ctx.stroke(); } } -function waterfallColor(db, isLight) { +function waterfallColor(db, pal) { const clamped = Math.max(-120, Math.min(-10, Number.isFinite(db) ? db : -120)); const t = (clamped + 120) / 110; - const hue = isLight ? 210 - t * 175 : 225 - t * 195; - const sat = isLight ? 82 : 88; - const light = isLight ? 92 - t * 52 : 16 + t * 52; - const alpha = isLight ? 0.42 + t * 0.38 : 0.28 + t * 0.58; - return `hsla(${hue}, ${sat}%, ${light}%, ${alpha})`; + const hue = pal.waterfallHue[0] + t * (pal.waterfallHue[1] - pal.waterfallHue[0]); + const light = pal.waterfallLight[0] + t * (pal.waterfallLight[1] - pal.waterfallLight[0]); + const alpha = pal.waterfallAlpha[0] + t * (pal.waterfallAlpha[1] - pal.waterfallAlpha[0]); + return `hsla(${hue}, ${pal.waterfallSat}%, ${light}%, ${alpha})`; } function formatFreq(hz) { @@ -2631,7 +2742,7 @@ let _bwDragStartX = 0; let _bwDragStartBwHz = 0; function spectrumBgColor() { - return currentTheme() === "light" ? "#eef3fb" : "#0a0f18"; + return canvasPalette().bg; } // Returns { loHz, hiHz, visLoHz, visHiHz, fullSpanHz, visSpanHz } and clamps @@ -2751,12 +2862,13 @@ function drawSpectrum(data) { } const ctx = spectrumCanvas.getContext("2d"); + const pal = canvasPalette(); const range = spectrumVisibleRange(data); const bins = data.bins; const n = bins.length; // Background - ctx.fillStyle = spectrumBgColor(); + ctx.fillStyle = pal.bg; ctx.fillRect(0, 0, W, H); if (!n) return; @@ -2768,7 +2880,7 @@ function drawSpectrum(data) { const loHz = data.center_hz - fullSpanHz / 2; // Horizontal dB grid lines - ctx.strokeStyle = "rgba(255,255,255,0.06)"; + ctx.strokeStyle = pal.spectrumGrid; ctx.lineWidth = 1; const gridStep = spectrumRange > 100 ? 20 : 10; for (let db = Math.ceil(DB_MIN / gridStep) * gridStep; db <= DB_MAX; db += gridStep) { @@ -2779,7 +2891,7 @@ function drawSpectrum(data) { // Y-axis dB labels (left side) ctx.save(); ctx.font = `${Math.round(9 * dpr)}px monospace`; - ctx.fillStyle = "rgba(180,200,220,0.45)"; + ctx.fillStyle = pal.spectrumLabel; ctx.textAlign = "left"; for (let db = Math.ceil(DB_MIN / gridStep) * gridStep; db <= DB_MAX; db += gridStep) { const y = Math.round(H * (1 - (db - DB_MIN) / dbRange)); @@ -2868,14 +2980,14 @@ function drawSpectrum(data) { for (let i = 0; i < n; i++) ctx.lineTo(binX(i), binY(i)); ctx.lineTo(binX(n - 1), H); ctx.closePath(); - ctx.fillStyle = "rgba(0,230,118,0.10)"; + ctx.fillStyle = pal.spectrumFill; ctx.fill(); ctx.restore(); // ── Spectrum line ───────────────────────────────────────────────────────── ctx.save(); ctx.beginPath(); - ctx.strokeStyle = "#00e676"; + ctx.strokeStyle = pal.spectrumLine; ctx.lineWidth = Math.max(1, dpr); for (let i = 0; i < n; i++) { const x = binX(i), y = binY(i); diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html index 081f41c..5d9072b 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html @@ -22,6 +22,14 @@
+
+ +
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css index 1fc59eb..9034151 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css @@ -490,6 +490,19 @@ small { color: var(--text-muted); } font-size: 0.78rem; white-space: nowrap; } +.header-style-pick { + display: flex; + align-items: center; +} +.header-style-pick select { + height: 2rem; + padding: 0.15rem 0.35rem; + border: 1px solid var(--border-light); + border-radius: 6px; + background: var(--input-bg); + color: var(--text); + font-size: 0.85rem; +} .header-logo { height: 4.6em; width: auto; flex-shrink: 0; filter: drop-shadow(0 4px 12px rgba(0,0,0,0.35)); } .subtitle { color: var(--text-muted); font-size: 0.95rem; } .subtitle a { color: var(--accent-green); text-decoration: none; } @@ -887,3 +900,177 @@ button:focus-visible, input:focus-visible, select:focus-visible { font-size: 0.82rem; } } + +/* ── Nord style ───────────────────────────────────────────────────────── */ +[data-style="nord"] { + --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="nord"][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; +} + +/* ── Monokai style ────────────────────────────────────────────────────── */ +[data-style="monokai"] { + --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="monokai"][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; +}