diff --git a/.gitignore b/.gitignore index a52a726..ea73f11 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ # Build outputs dist/ +dist-modern/ +dist-legacy/ build/ *.dist diff --git a/client/i.html b/client/i.html index ed884c3..60a0095 100644 --- a/client/i.html +++ b/client/i.html @@ -1,12 +1,44 @@ - - - - - Luxsin X8 Controller - - -
- - + + + + + Luxsin X8 Controller + + + +
+ Loading… + + diff --git a/client/i.legacy.html b/client/i.legacy.html new file mode 100644 index 0000000..840ca9e --- /dev/null +++ b/client/i.legacy.html @@ -0,0 +1,13 @@ + + + + + + Luxsin X8 Controller (Legacy) + + +
+ + + + diff --git a/client/i.modern.html b/client/i.modern.html new file mode 100644 index 0000000..ed884c3 --- /dev/null +++ b/client/i.modern.html @@ -0,0 +1,12 @@ + + + + + + Luxsin X8 Controller + + +
+ + + diff --git a/client/src/index.legacy.css b/client/src/index.legacy.css new file mode 100644 index 0000000..8e593d4 --- /dev/null +++ b/client/src/index.legacy.css @@ -0,0 +1,230 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* Legacy baseline: Chrome 91 (no dvh/svh, no color-mix, no oklch). */ +html, +body, +#root { + height: 100%; +} + +body { + background: #000; + color: #f2f2f2; + -webkit-font-smoothing: antialiased; + font-family: -apple-system, "SF Pro Display", "SF Pro Text", "Helvetica Neue", + sans-serif; +} + +@layer utilities { + /* Provide stable fallbacks for modern dvh-based layouts used across the app. */ + .min-h-dvh { + min-height: 100vh; + min-height: -webkit-fill-available; + } + .h-dvh { + height: 100vh; + height: -webkit-fill-available; + } + .max-h-dvh { + max-height: 100vh; + max-height: -webkit-fill-available; + } + + /* Some screens used h-[100dvh] in modern — give an escape hatch if needed. */ + .h-dvh-full { + height: 100vh; + height: -webkit-fill-available; + } + .min-h-dvh-full { + min-height: 100vh; + min-height: -webkit-fill-available; + } +} + +@layer components { + /* Match modern .cyan-slider styling (input[type=range]) for legacy build. */ + .cyan-slider { + -webkit-appearance: none; + appearance: none; + width: 100%; + height: 4px; + border-radius: 2px; + /* Home renders its own fill overlay; keep a subtle base track. */ + background: rgba(255, 255, 255, 0.12); + outline: none; + cursor: pointer; + touch-action: pan-y; + } + + .cyan-slider::-webkit-slider-thumb { + -webkit-appearance: none; + width: 18px; + height: 18px; + border-radius: 9999px; + background: #ffffff; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5); + cursor: pointer; + transition: transform 0.1s ease; + border: none; + } + + .cyan-slider::-webkit-slider-thumb:active { + transform: scale(1.15); + } + + /* Firefox */ + .cyan-slider::-moz-range-track { + height: 4px; + border-radius: 2px; + background: rgba(255, 255, 255, 0.12); + } + .cyan-slider::-moz-range-thumb { + width: 18px; + height: 18px; + border-radius: 9999px; + background: #ffffff; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5); + border: none; + cursor: pointer; + } + + /* Home: iOS-style list group/rows (input/output/audio/system/vu/back) */ + .ios-list-group { + background: rgba(28, 28, 30, 0.92); + border-radius: 16px; + overflow: hidden; + border: 1px solid rgba(255, 255, 255, 0.06); + } + + .ios-list-row { + display: flex; + align-items: center; + padding: 13px 16px; + position: relative; + transition: background 0.15s ease; + min-height: 52px; + } + + .ios-list-row:active { + background: rgba(255, 255, 255, 0.05); + } + + .ios-list-row + .ios-list-row::before { + content: ""; + position: absolute; + top: 0; + left: 16px; + right: 0; + height: 1px; + transform: scaleY(0.5); + transform-origin: top left; + background: rgba(255, 255, 255, 0.08); + } + + .ios-chevron { + color: rgba(255, 255, 255, 0.25); + flex-shrink: 0; + margin-left: 6px; + } + + .ios-row-value { + color: rgba(255, 255, 255, 0.4); + font-size: 15px; + margin-right: 4px; + } + + /* Home: 4 quick-action circle buttons (Power / HP-EQ / Effect / Bypass) */ + .icon-circle-btn { + width: 64px; + height: 64px; + border-radius: 9999px; + background: rgba(44, 44, 46, 0.9); + border: 1px solid rgba(255, 255, 255, 0.12); + display: flex; + align-items: center; + justify-content: center; + transition: transform 0.15s ease, background 0.15s ease, box-shadow 0.15s ease, + border-color 0.15s ease; + cursor: pointer; + } + + .icon-circle-btn:active { + transform: scale(0.93); + background: rgba(60, 60, 62, 0.95); + } + + /* ── iOS-style Toggle Switch ── */ + .ios-toggle { + position: relative; + display: inline-block; + width: 46px; + height: 28px; + flex-shrink: 0; + } + .ios-toggle input { + opacity: 0; + width: 0; + height: 0; + position: absolute; + } + .ios-toggle-track { + position: absolute; + inset: 0; + border-radius: 999px; + background: rgba(120, 120, 128, 0.32); + transition: background 0.25s ease; + cursor: pointer; + } + .ios-toggle input:checked ~ .ios-toggle-track { + background: #00FFF6; + box-shadow: 0 0 12px rgba(0, 255, 246, 0.5); + } + .ios-toggle-thumb { + position: absolute; + top: 2px; + left: 2px; + width: 24px; + height: 24px; + border-radius: 50%; + background: #ffffff; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4); + transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1); + pointer-events: none; + } + .ios-toggle input:checked ~ .ios-toggle-track .ios-toggle-thumb { + transform: translateX(18px); + } + + /* ── Section header label (cyan) ── */ + .ios-section-label { + font-size: 13px; + font-weight: 600; + letter-spacing: 0.04em; + color: #00FFF6; + padding: 20px 4px 8px; + text-transform: uppercase; + } + + /* ── Page header bar ── */ + .page-header { + display: flex; + align-items: center; + padding: 16px 16px 8px; + position: sticky; + top: 0; + z-index: 10; + background: rgba(0, 0, 0, 0.85); + -webkit-backdrop-filter: blur(20px); + backdrop-filter: blur(20px); + } + + /* Active: cyan glow ring */ + .icon-circle-btn.active { + background: rgba(0, 255, 246, 0.12); + border-color: rgba(0, 255, 246, 0.55); + box-shadow: 0 0 0 2px rgba(0, 255, 246, 0.18), 0 0 18px rgba(0, 255, 246, 0.28); + } +} + diff --git a/client/src/lib/legacyBrowser.ts b/client/src/lib/legacyBrowser.ts new file mode 100644 index 0000000..17c390f --- /dev/null +++ b/client/src/lib/legacyBrowser.ts @@ -0,0 +1,47 @@ +/** + * Minimal shims for Chrome 91 WebView (and similar legacy Chromium). + * Import once from main.legacy.tsx before the app mounts. + */ + +if (typeof Object.hasOwn !== "function") { + Object.hasOwn = (obj, prop) => + Object.prototype.hasOwnProperty.call(obj, prop); +} + +if (!Array.prototype.at) { + // eslint-disable-next-line no-extend-native + Array.prototype.at = function at(index) { + const len = this.length; + const k = index >= 0 ? index : len + index; + if (k < 0 || k >= len) return undefined; + return this[k]; + }; +} + +if (typeof globalThis.structuredClone !== "function") { + globalThis.structuredClone = (value) => JSON.parse(JSON.stringify(value)); +} + +/** + * Minimal shims for Chrome 91 WebView (and similar legacy Chromium). + * Import once from main.tsx before the app mounts. + */ + +if (typeof Object.hasOwn !== "function") { + Object.hasOwn = (obj, prop) => + Object.prototype.hasOwnProperty.call(obj, prop); +} + +if (!Array.prototype.at) { + Array.prototype.at = function at(index) { + const len = this.length; + const k = index >= 0 ? index : len + index; + if (k < 0 || k >= len) return undefined; + return this[k]; + }; +} + +if (typeof globalThis.structuredClone !== "function") { + globalThis.structuredClone = (value: T): T => + JSON.parse(JSON.stringify(value)) as T; +} diff --git a/client/src/lib/luxsinApi.ts b/client/src/lib/luxsinApi.ts index 2878921..cc713d5 100644 --- a/client/src/lib/luxsinApi.ts +++ b/client/src/lib/luxsinApi.ts @@ -28,6 +28,15 @@ export function decodeCustomBase64(encoded: string): string { const index = ALPHABET_CUSTOM.indexOf(char); translated += index !== -1 ? ALPHABET_STANDARD.charAt(index) : char; } + // Strip whitespace / newlines that may be present in API responses. + // Chrome 91 atob() is stricter than modern browsers and rejects these. + translated = translated.replace(/\s/g, ""); + // Ensure correct Base64 padding (=). Modern atob() tolerates missing padding, + // but Chrome 91 throws "The string to be decoded is not correctly encoded". + const padNeeded = (4 - (translated.length % 4)) % 4; + if (padNeeded > 0) { + translated += "=".repeat(padNeeded); + } const decoded = atob(translated); const bytes = new Uint8Array(decoded.length); for (let i = 0; i < decoded.length; i++) { diff --git a/client/src/main.legacy.tsx b/client/src/main.legacy.tsx new file mode 100644 index 0000000..979dc81 --- /dev/null +++ b/client/src/main.legacy.tsx @@ -0,0 +1,7 @@ +import "@/lib/legacyBrowser"; +import { createRoot } from "react-dom/client"; +import App from "./App"; +import "./index.legacy.css"; + +createRoot(document.getElementById("root")!).render(); + diff --git a/client/src/pages/EQPage.tsx b/client/src/pages/EQPage.tsx index 20eaa5a..a667d5e 100644 --- a/client/src/pages/EQPage.tsx +++ b/client/src/pages/EQPage.tsx @@ -1917,14 +1917,14 @@ export default function EQPage() { {isBrandDrawerOpen && (
-