更新依赖项和配置以支持新版本的pnpm,优化PostCSS和Tailwind配置,添加延迟输入功能到EffectsPage,提升用户体验
This commit is contained in:
@@ -1,12 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en" class="dark">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1" />
|
|
||||||
<title>Luxsin X9 Controller</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -43,10 +43,6 @@ body,
|
|||||||
height: auto !important;
|
height: auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.min-h-dvh {
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.x9-app-shell-outer {
|
.x9-app-shell-outer {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
min-height: -webkit-fill-available;
|
min-height: -webkit-fill-available;
|
||||||
@@ -90,6 +86,11 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
|
.flex {
|
||||||
|
min-height: 0;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.x9-app-shell-outer {
|
.x9-app-shell-outer {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
min-height: -webkit-fill-available;
|
min-height: -webkit-fill-available;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
All dropdowns → navigate to /select page
|
All dropdowns → navigate to /select page
|
||||||
============================================================ */
|
============================================================ */
|
||||||
import { useDevice } from "@/contexts/DeviceContext";
|
import { useDevice } from "@/contexts/DeviceContext";
|
||||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
import { ChevronLeft, ChevronRight, X } from "lucide-react";
|
||||||
import { useLocation } from "wouter";
|
import { useLocation } from "wouter";
|
||||||
import { useState, useEffect, useRef, useMemo } from "react";
|
import { useState, useEffect, useRef, useMemo } from "react";
|
||||||
import BottomNav from "@/components/BottomNav";
|
import BottomNav from "@/components/BottomNav";
|
||||||
@@ -18,6 +18,7 @@ import { parseFirmwareVersion } from "@/lib/luxsinApi";
|
|||||||
import localeZh from "@/locales/data-zh.json";
|
import localeZh from "@/locales/data-zh.json";
|
||||||
import localeZhHK from "@/locales/data-zh-HK.json";
|
import localeZhHK from "@/locales/data-zh-HK.json";
|
||||||
import localeEn from "@/locales/data-en.json";
|
import localeEn from "@/locales/data-en.json";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
type EffectLocale = NonNullable<(typeof localeZh)["effect"]>;
|
type EffectLocale = NonNullable<(typeof localeZh)["effect"]>;
|
||||||
|
|
||||||
@@ -48,6 +49,44 @@ function formatSubwooferDelayValue(value: number) {
|
|||||||
return `${ms.toFixed(2)}ms(${cm.toFixed(1)}cm)`;
|
return `${ms.toFixed(2)}ms(${cm.toFixed(1)}cm)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseSubwooferDelayInput(raw: string, english: boolean) {
|
||||||
|
const trimmed = raw.trim().toLowerCase();
|
||||||
|
if (!trimmed) {
|
||||||
|
return {
|
||||||
|
ok: false as const,
|
||||||
|
message: english ? "Enter a value" : "请输入数值",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const cmMatch = trimmed.match(/^([\d.]+)\s*cm$/);
|
||||||
|
if (cmMatch) {
|
||||||
|
const cm = Number(cmMatch[1]);
|
||||||
|
if (!Number.isFinite(cm)) {
|
||||||
|
return { ok: false as const, message: english ? "Invalid value" : "无效数值" };
|
||||||
|
}
|
||||||
|
const value = Math.round((cm * SUBWOOFER_DELAY_MS_DIVISOR) / SUBWOOFER_DELAY_CM_NUMERATOR);
|
||||||
|
return { ok: true as const, value: clampSubwooferDelayValue(value) };
|
||||||
|
}
|
||||||
|
|
||||||
|
const msMatch = trimmed.match(/^([\d.]+)\s*ms?$/);
|
||||||
|
const plainMatch = trimmed.match(/^([\d.]+)$/);
|
||||||
|
const msStr = msMatch?.[1] ?? plainMatch?.[1];
|
||||||
|
if (msStr) {
|
||||||
|
const ms = Number(msStr);
|
||||||
|
if (!Number.isFinite(ms)) {
|
||||||
|
return { ok: false as const, message: english ? "Invalid value" : "无效数值" };
|
||||||
|
}
|
||||||
|
const value = Math.round(ms * SUBWOOFER_DELAY_MS_DIVISOR);
|
||||||
|
return { ok: true as const, value: clampSubwooferDelayValue(value) };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: false as const,
|
||||||
|
message: english ? "Use e.g. 10.5, 10.5ms, or 7.4cm" : "格式如 10.5、10.5ms 或 7.4cm",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type DelayInputTarget = "main" | "sub";
|
||||||
type DelayChannel = "L" | "R";
|
type DelayChannel = "L" | "R";
|
||||||
|
|
||||||
function DelaySpeakerIcon({ direction }: { direction: "left" | "right" }) {
|
function DelaySpeakerIcon({ direction }: { direction: "left" | "right" }) {
|
||||||
@@ -292,6 +331,48 @@ export default function EffectsPage() {
|
|||||||
const isDraggingSubDelayMain = useRef(false);
|
const isDraggingSubDelayMain = useRef(false);
|
||||||
const isDraggingSubDelaySub = useRef(false);
|
const isDraggingSubDelaySub = useRef(false);
|
||||||
|
|
||||||
|
const [delayInputTarget, setDelayInputTarget] = useState<DelayInputTarget | null>(null);
|
||||||
|
const [delayInputValue, setDelayInputValue] = useState("");
|
||||||
|
|
||||||
|
const delayInputEnglish = deviceState?.language === 0;
|
||||||
|
const maxDelayMs = SUBWOOFER_DELAY_MAX / SUBWOOFER_DELAY_MS_DIVISOR;
|
||||||
|
|
||||||
|
const openDelayInput = (target: DelayInputTarget) => {
|
||||||
|
const current = target === "main" ? localSubDelayMain : localSubDelaySub;
|
||||||
|
setDelayInputValue(subwooferDelayToMs(current).toFixed(2));
|
||||||
|
setDelayInputTarget(target);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeDelayInput = () => {
|
||||||
|
setDelayInputTarget(null);
|
||||||
|
setDelayInputValue("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyDelayInput = () => {
|
||||||
|
if (!delayInputTarget) return;
|
||||||
|
const parsed = parseSubwooferDelayInput(delayInputValue, delayInputEnglish);
|
||||||
|
if (!parsed.ok) {
|
||||||
|
toast.error(parsed.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const delayVal = parsed.value;
|
||||||
|
if (delayInputTarget === "main") {
|
||||||
|
setLocalSubDelayMain(delayVal);
|
||||||
|
updateSetting({ [subwooferDelayMainField]: delayVal });
|
||||||
|
} else {
|
||||||
|
setLocalSubDelaySub(delayVal);
|
||||||
|
updateSetting({ [subwooferDelaySubField]: delayVal });
|
||||||
|
}
|
||||||
|
closeDelayInput();
|
||||||
|
};
|
||||||
|
|
||||||
|
const delayInputTitle =
|
||||||
|
delayInputTarget === "main"
|
||||||
|
? subwooferDelayText?.mainSpeaker ?? "主机箱"
|
||||||
|
: delayInputTarget === "sub"
|
||||||
|
? subwooferDelayText?.subwoofer ?? "低音炮"
|
||||||
|
: "";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isDragging.current) setLocalWidth(Math.round(widthVal));
|
if (!isDragging.current) setLocalWidth(Math.round(widthVal));
|
||||||
}, [widthVal]);
|
}, [widthVal]);
|
||||||
@@ -727,9 +808,14 @@ export default function EffectsPage() {
|
|||||||
<span className="text-[14px] text-white/60">
|
<span className="text-[14px] text-white/60">
|
||||||
{subwooferDelayText?.mainSpeaker ?? "主机箱"}
|
{subwooferDelayText?.mainSpeaker ?? "主机箱"}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[14px] font-bold" style={{ color: "#00FFF6" }}>
|
<button
|
||||||
|
type="button"
|
||||||
|
className="text-[14px] font-bold active:opacity-70 transition-opacity"
|
||||||
|
style={{ color: "#00FFF6" }}
|
||||||
|
onClick={() => openDelayInput("main")}
|
||||||
|
>
|
||||||
{formatSubwooferDelayValue(localSubDelayMain)}
|
{formatSubwooferDelayValue(localSubDelayMain)}
|
||||||
</span>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<CyanSlider
|
<CyanSlider
|
||||||
value={localSubDelayMain}
|
value={localSubDelayMain}
|
||||||
@@ -754,9 +840,14 @@ export default function EffectsPage() {
|
|||||||
<span className="text-[14px] text-white/60">
|
<span className="text-[14px] text-white/60">
|
||||||
{subwooferDelayText?.subwoofer ?? "低音炮"}
|
{subwooferDelayText?.subwoofer ?? "低音炮"}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[14px] font-bold" style={{ color: "#00FFF6" }}>
|
<button
|
||||||
|
type="button"
|
||||||
|
className="text-[14px] font-bold active:opacity-70 transition-opacity"
|
||||||
|
style={{ color: "#00FFF6" }}
|
||||||
|
onClick={() => openDelayInput("sub")}
|
||||||
|
>
|
||||||
{formatSubwooferDelayValue(localSubDelaySub)}
|
{formatSubwooferDelayValue(localSubDelaySub)}
|
||||||
</span>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<CyanSlider
|
<CyanSlider
|
||||||
value={localSubDelaySub}
|
value={localSubDelaySub}
|
||||||
@@ -782,6 +873,72 @@ export default function EffectsPage() {
|
|||||||
</FeatureGate>
|
</FeatureGate>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{delayInputTarget && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-[120] flex items-center justify-center bg-black/65 px-4"
|
||||||
|
onClick={closeDelayInput}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="w-full max-w-[420px] rounded-[14px] p-5"
|
||||||
|
style={{
|
||||||
|
background: "linear-gradient(180deg, rgba(34,36,40,0.98) 0%, rgba(24,26,30,0.98) 100%)",
|
||||||
|
border: "1px solid rgba(255,255,255,0.12)",
|
||||||
|
boxShadow: "0 18px 48px rgba(0,0,0,0.55)",
|
||||||
|
}}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<h3 className="text-[18px] font-semibold leading-tight text-white/90">
|
||||||
|
{delayInputTitle} · {subwooferDelayText?.label ?? "延时"}
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="rounded-full p-1.5 text-white/45 transition-colors hover:bg-white/10 hover:text-white/80"
|
||||||
|
onClick={closeDelayInput}
|
||||||
|
aria-label={effectText.enableConfirmCancel ?? "取消"}
|
||||||
|
>
|
||||||
|
<X size={20} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p className="mb-4 text-[13px] leading-relaxed text-white/45">
|
||||||
|
{delayInputEnglish
|
||||||
|
? `Enter delay in ms (0–${maxDelayMs.toFixed(2)}), or cm, e.g. 10.5, 10.5ms, 7.4cm`
|
||||||
|
: `输入延时(ms,0–${maxDelayMs.toFixed(2)}),也可输入 cm,如 10.5、10.5ms、7.4cm`}
|
||||||
|
</p>
|
||||||
|
<input
|
||||||
|
value={delayInputValue}
|
||||||
|
onChange={(e) => setDelayInputValue(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
applyDelayInput();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
inputMode="decimal"
|
||||||
|
placeholder={delayInputEnglish ? "e.g. 10.50" : "如 10.50"}
|
||||||
|
className="h-11 w-full rounded-[10px] border border-white/12 bg-[#15171b] px-3 text-[15px] text-white/90 outline-none placeholder:text-white/30 focus:border-[#00FFF6]/40"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<div className="mt-6 flex items-center justify-center gap-4 sm:gap-8">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="min-w-[112px] rounded-full px-5 py-2.5 text-[15px] font-medium text-white/85 transition-colors bg-[#3f4349] hover:bg-[#4a4f56] active:scale-[0.98]"
|
||||||
|
onClick={closeDelayInput}
|
||||||
|
>
|
||||||
|
{effectText.enableConfirmCancel ?? "取消"}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="min-w-[112px] rounded-full px-5 py-2.5 text-[15px] font-semibold text-black transition-all bg-[#00FFF6] hover:brightness-95 active:scale-[0.98]"
|
||||||
|
onClick={applyDelayInput}
|
||||||
|
>
|
||||||
|
{delayInputEnglish ? "OK" : "确定"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<BottomNav />
|
<BottomNav />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+2
-10
@@ -94,13 +94,5 @@
|
|||||||
"vite-plugin-manus-runtime": "^0.0.57",
|
"vite-plugin-manus-runtime": "^0.0.57",
|
||||||
"vitest": "^2.1.4"
|
"vitest": "^2.1.4"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.4.1+sha512.c753b6c3ad7afa13af388fa6d808035a008e30ea9993f58c6663e2bc5ff21679aa834db094987129aa4d488b86df57f7b634981b2f827cdcacc698cc0cfb88af",
|
"packageManager": "pnpm@11.5.2+sha512.71c631e382066efc25625d5cf029075de07b61b37f6e27350fbd84b1bda5864c8c1967adc280776b45c30a715c0359a3be08fef42d5bb09e2b99029979692916"
|
||||||
"pnpm": {
|
}
|
||||||
"patchedDependencies": {
|
|
||||||
"wouter@3.7.1": "patches/wouter@3.7.1.patch"
|
|
||||||
},
|
|
||||||
"overrides": {
|
|
||||||
"tailwindcss>nanoid": "3.3.7"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Generated
+2133
-1313
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
|||||||
|
packages:
|
||||||
|
- .
|
||||||
|
|
||||||
|
allowBuilds:
|
||||||
|
esbuild: true
|
||||||
|
|
||||||
|
overrides:
|
||||||
|
tailwindcss>nanoid: 3.3.7
|
||||||
@@ -1,6 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* Legacy PostCSS is configured inline in vite.legacy.config.ts (same as x8).
|
||||||
|
* This file is kept for reference / tooling that expects a postcss config path.
|
||||||
|
*/
|
||||||
|
const path = require("node:path");
|
||||||
const tailwindcss3 = require("tailwindcss3");
|
const tailwindcss3 = require("tailwindcss3");
|
||||||
const autoprefixer = require("autoprefixer");
|
const autoprefixer = require("autoprefixer");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [tailwindcss3({ config: "./tailwind.legacy.config.cjs" }), autoprefixer()],
|
plugins: [
|
||||||
|
tailwindcss3({ config: path.join(__dirname, "tailwind.legacy.config.cjs") }),
|
||||||
|
autoprefixer(),
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -28,4 +28,10 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
|
safelist: [
|
||||||
|
"x9-app-shell-outer",
|
||||||
|
"x9-app-shell-column",
|
||||||
|
"x9-bottom-nav-outer",
|
||||||
|
"x9-bottom-nav-inner",
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
+34
-12
@@ -4,6 +4,7 @@ import path from "node:path";
|
|||||||
import { createRequire } from "node:module";
|
import { createRequire } from "node:module";
|
||||||
import { defineConfig, type Plugin } from "vite";
|
import { defineConfig, type Plugin } from "vite";
|
||||||
|
|
||||||
|
const PROJECT_ROOT = import.meta.dirname;
|
||||||
const require = createRequire(import.meta.url);
|
const require = createRequire(import.meta.url);
|
||||||
const tailwindcss3 = require("tailwindcss3");
|
const tailwindcss3 = require("tailwindcss3");
|
||||||
const autoprefixer = require("autoprefixer");
|
const autoprefixer = require("autoprefixer");
|
||||||
@@ -14,8 +15,8 @@ function legacyIndexRewritePlugin(): Plugin {
|
|||||||
apply: "serve",
|
apply: "serve",
|
||||||
configureServer(server) {
|
configureServer(server) {
|
||||||
server.middlewares.use((req, _res, next) => {
|
server.middlewares.use((req, _res, next) => {
|
||||||
const url = req.url ?? "";
|
const url = (req.url ?? "").split("?")[0];
|
||||||
if (url === "/" || url === "/index.html") {
|
if (url === "/" || url === "/index.html" || url === "/i.modern.html") {
|
||||||
req.url = "/i.legacy.html";
|
req.url = "/i.legacy.html";
|
||||||
}
|
}
|
||||||
next();
|
next();
|
||||||
@@ -24,38 +25,59 @@ function legacyIndexRewritePlugin(): Plugin {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Dev MPA may discover modern CSS — stub them so Tailwind v3 postcss does not choke. */
|
||||||
|
function legacySkipModernCssPlugin(): Plugin {
|
||||||
|
return {
|
||||||
|
name: "legacy-skip-modern-css",
|
||||||
|
enforce: "pre",
|
||||||
|
apply: "serve",
|
||||||
|
load(id) {
|
||||||
|
const norm = id.replace(/\\/g, "/");
|
||||||
|
if (norm.endsWith("/client/src/index.css")) {
|
||||||
|
return "/* legacy dev: skip modern index.css */";
|
||||||
|
}
|
||||||
|
if (/node_modules\/tailwindcss\/index\.css/.test(norm)) {
|
||||||
|
return "/* legacy dev: skip tailwind v4 */";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [legacyIndexRewritePlugin(), react(), jsxLocPlugin()],
|
plugins: [legacySkipModernCssPlugin(), legacyIndexRewritePlugin(), react(), jsxLocPlugin()],
|
||||||
define: {
|
define: {
|
||||||
"import.meta.env.VITE_X9_LEGACY": JSON.stringify("1"),
|
"import.meta.env.VITE_X9_LEGACY": JSON.stringify("1"),
|
||||||
},
|
},
|
||||||
base: process.env.NODE_ENV === "production" ? "/x9/v2/legacy/" : "/",
|
base: process.env.NODE_ENV === "production" ? "/x9/v2/legacy/" : "/",
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"@": path.resolve(import.meta.dirname, "client", "src"),
|
"@": path.resolve(PROJECT_ROOT, "client", "src"),
|
||||||
"@shared": path.resolve(import.meta.dirname, "shared"),
|
"@shared": path.resolve(PROJECT_ROOT, "shared"),
|
||||||
"@assets": path.resolve(import.meta.dirname, "attached_assets"),
|
"@assets": path.resolve(PROJECT_ROOT, "attached_assets"),
|
||||||
},
|
},
|
||||||
dedupe: ["react", "react-dom"],
|
dedupe: ["react", "react-dom"],
|
||||||
},
|
},
|
||||||
envDir: path.resolve(import.meta.dirname),
|
envDir: path.resolve(PROJECT_ROOT),
|
||||||
root: path.resolve(import.meta.dirname, "client"),
|
root: path.resolve(PROJECT_ROOT, "client"),
|
||||||
publicDir: path.resolve(import.meta.dirname, "client/public"),
|
publicDir: path.resolve(PROJECT_ROOT, "client/public"),
|
||||||
css: {
|
css: {
|
||||||
transformer: "postcss",
|
transformer: "postcss",
|
||||||
postcss: {
|
postcss: {
|
||||||
plugins: [tailwindcss3({ config: "./tailwind.legacy.config.cjs" }), autoprefixer()],
|
plugins: [
|
||||||
|
tailwindcss3({ config: path.join(PROJECT_ROOT, "tailwind.legacy.config.cjs") }),
|
||||||
|
autoprefixer(),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
target: ["chrome91", "edge91"],
|
target: ["chrome91", "edge91"],
|
||||||
cssTarget: "chrome91",
|
cssTarget: "chrome91",
|
||||||
outDir: path.resolve(import.meta.dirname, "dist/legacy"),
|
outDir: path.resolve(PROJECT_ROOT, "dist/legacy"),
|
||||||
emptyOutDir: true,
|
emptyOutDir: true,
|
||||||
minify: "terser",
|
minify: "terser",
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
input: {
|
input: {
|
||||||
i: path.resolve(import.meta.dirname, "client", "i.legacy.html"),
|
i: path.resolve(PROJECT_ROOT, "client", "i.legacy.html"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user