更新依赖项和配置以支持新版本的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;
|
||||
}
|
||||
|
||||
.min-h-dvh {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.x9-app-shell-outer {
|
||||
min-height: 100vh;
|
||||
min-height: -webkit-fill-available;
|
||||
@@ -90,6 +86,11 @@ body {
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.flex {
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.x9-app-shell-outer {
|
||||
min-height: 100vh;
|
||||
min-height: -webkit-fill-available;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
All dropdowns → navigate to /select page
|
||||
============================================================ */
|
||||
import { useDevice } from "@/contexts/DeviceContext";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { ChevronLeft, ChevronRight, X } from "lucide-react";
|
||||
import { useLocation } from "wouter";
|
||||
import { useState, useEffect, useRef, useMemo } from "react";
|
||||
import BottomNav from "@/components/BottomNav";
|
||||
@@ -18,6 +18,7 @@ import { parseFirmwareVersion } from "@/lib/luxsinApi";
|
||||
import localeZh from "@/locales/data-zh.json";
|
||||
import localeZhHK from "@/locales/data-zh-HK.json";
|
||||
import localeEn from "@/locales/data-en.json";
|
||||
import { toast } from "sonner";
|
||||
|
||||
type EffectLocale = NonNullable<(typeof localeZh)["effect"]>;
|
||||
|
||||
@@ -48,6 +49,44 @@ function formatSubwooferDelayValue(value: number) {
|
||||
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";
|
||||
|
||||
function DelaySpeakerIcon({ direction }: { direction: "left" | "right" }) {
|
||||
@@ -292,6 +331,48 @@ export default function EffectsPage() {
|
||||
const isDraggingSubDelayMain = 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(() => {
|
||||
if (!isDragging.current) setLocalWidth(Math.round(widthVal));
|
||||
}, [widthVal]);
|
||||
@@ -727,9 +808,14 @@ export default function EffectsPage() {
|
||||
<span className="text-[14px] text-white/60">
|
||||
{subwooferDelayText?.mainSpeaker ?? "主机箱"}
|
||||
</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)}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<CyanSlider
|
||||
value={localSubDelayMain}
|
||||
@@ -754,9 +840,14 @@ export default function EffectsPage() {
|
||||
<span className="text-[14px] text-white/60">
|
||||
{subwooferDelayText?.subwoofer ?? "低音炮"}
|
||||
</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)}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<CyanSlider
|
||||
value={localSubDelaySub}
|
||||
@@ -782,6 +873,72 @@ export default function EffectsPage() {
|
||||
</FeatureGate>
|
||||
</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 />
|
||||
</div>
|
||||
);
|
||||
|
||||
+1
-9
@@ -94,13 +94,5 @@
|
||||
"vite-plugin-manus-runtime": "^0.0.57",
|
||||
"vitest": "^2.1.4"
|
||||
},
|
||||
"packageManager": "pnpm@10.4.1+sha512.c753b6c3ad7afa13af388fa6d808035a008e30ea9993f58c6663e2bc5ff21679aa834db094987129aa4d488b86df57f7b634981b2f827cdcacc698cc0cfb88af",
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"wouter@3.7.1": "patches/wouter@3.7.1.patch"
|
||||
},
|
||||
"overrides": {
|
||||
"tailwindcss>nanoid": "3.3.7"
|
||||
}
|
||||
}
|
||||
"packageManager": "pnpm@11.5.2+sha512.71c631e382066efc25625d5cf029075de07b61b37f6e27350fbd84b1bda5864c8c1967adc280776b45c30a715c0359a3be08fef42d5bb09e2b99029979692916"
|
||||
}
|
||||
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 autoprefixer = require("autoprefixer");
|
||||
|
||||
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: [],
|
||||
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 { defineConfig, type Plugin } from "vite";
|
||||
|
||||
const PROJECT_ROOT = import.meta.dirname;
|
||||
const require = createRequire(import.meta.url);
|
||||
const tailwindcss3 = require("tailwindcss3");
|
||||
const autoprefixer = require("autoprefixer");
|
||||
@@ -14,8 +15,8 @@ function legacyIndexRewritePlugin(): Plugin {
|
||||
apply: "serve",
|
||||
configureServer(server) {
|
||||
server.middlewares.use((req, _res, next) => {
|
||||
const url = req.url ?? "";
|
||||
if (url === "/" || url === "/index.html") {
|
||||
const url = (req.url ?? "").split("?")[0];
|
||||
if (url === "/" || url === "/index.html" || url === "/i.modern.html") {
|
||||
req.url = "/i.legacy.html";
|
||||
}
|
||||
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({
|
||||
plugins: [legacyIndexRewritePlugin(), react(), jsxLocPlugin()],
|
||||
plugins: [legacySkipModernCssPlugin(), legacyIndexRewritePlugin(), react(), jsxLocPlugin()],
|
||||
define: {
|
||||
"import.meta.env.VITE_X9_LEGACY": JSON.stringify("1"),
|
||||
},
|
||||
base: process.env.NODE_ENV === "production" ? "/x9/v2/legacy/" : "/",
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(import.meta.dirname, "client", "src"),
|
||||
"@shared": path.resolve(import.meta.dirname, "shared"),
|
||||
"@assets": path.resolve(import.meta.dirname, "attached_assets"),
|
||||
"@": path.resolve(PROJECT_ROOT, "client", "src"),
|
||||
"@shared": path.resolve(PROJECT_ROOT, "shared"),
|
||||
"@assets": path.resolve(PROJECT_ROOT, "attached_assets"),
|
||||
},
|
||||
dedupe: ["react", "react-dom"],
|
||||
},
|
||||
envDir: path.resolve(import.meta.dirname),
|
||||
root: path.resolve(import.meta.dirname, "client"),
|
||||
publicDir: path.resolve(import.meta.dirname, "client/public"),
|
||||
envDir: path.resolve(PROJECT_ROOT),
|
||||
root: path.resolve(PROJECT_ROOT, "client"),
|
||||
publicDir: path.resolve(PROJECT_ROOT, "client/public"),
|
||||
css: {
|
||||
transformer: "postcss",
|
||||
postcss: {
|
||||
plugins: [tailwindcss3({ config: "./tailwind.legacy.config.cjs" }), autoprefixer()],
|
||||
plugins: [
|
||||
tailwindcss3({ config: path.join(PROJECT_ROOT, "tailwind.legacy.config.cjs") }),
|
||||
autoprefixer(),
|
||||
],
|
||||
},
|
||||
},
|
||||
build: {
|
||||
target: ["chrome91", "edge91"],
|
||||
cssTarget: "chrome91",
|
||||
outDir: path.resolve(import.meta.dirname, "dist/legacy"),
|
||||
outDir: path.resolve(PROJECT_ROOT, "dist/legacy"),
|
||||
emptyOutDir: true,
|
||||
minify: "terser",
|
||||
rollupOptions: {
|
||||
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