当设备未连接时,各页面默认显示英文语言的文本,并在标题追加 (Disconnect)
This commit is contained in:
@@ -195,44 +195,6 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 显示与灯光页面(/display)
|
|
||||||
|
|
||||||
### 区块 1:页面头部
|
|
||||||
| 位置 | 组件 | 功能 | 说明 |
|
|
||||||
|------|------|------|------|
|
|
||||||
| 左侧 | 返回按钮 | 返回上一页 | 左箭头 |
|
|
||||||
| 中央 | 标题 | 显示 "显示与灯光" | 17px 字体 |
|
|
||||||
|
|
||||||
### 区块 2:iOS 列表行
|
|
||||||
| 序号 | 菜单项 | 右侧显示 | 功能 |
|
|
||||||
|------|--------|---------|------|
|
|
||||||
| 1 | 屏幕亮度 | 下拉选择 | 跳转 SelectPage 选择 |
|
|
||||||
| 2 | 自动锁屏 | 下拉选择 | 跳转 SelectPage 选择 |
|
|
||||||
| 3 | 旋钮亮度 | 下拉选择 | 跳转 SelectPage 选择 |
|
|
||||||
| 4 | 旋钮呼吸灯 | Toggle | 启用/禁用呼吸灯效果 |
|
|
||||||
| 5 | 按键灯 | Toggle | 启用/禁用按键灯 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## DSP & DAC 页面(/dsp)
|
|
||||||
|
|
||||||
### 区块 1:页面头部
|
|
||||||
| 位置 | 组件 | 功能 | 说明 |
|
|
||||||
|------|------|------|------|
|
|
||||||
| 左侧 | 返回按钮 | 返回上一页 | 左箭头 |
|
|
||||||
| 中央 | 标题 | 显示 "DSP & DAC" | 17px 字体 |
|
|
||||||
|
|
||||||
### 区块 2:iOS 列表行
|
|
||||||
| 序号 | 菜单项 | 右侧显示 | 功能 |
|
|
||||||
|------|--------|---------|------|
|
|
||||||
| 1 | DSP 引擎 | 下拉选择 | 跳转 SelectPage 选择 |
|
|
||||||
| 2 | DAC 模式 | 下拉选择 | 跳转 SelectPage 选择 |
|
|
||||||
| 3 | 模拟增益 | 下拉选择 | 跳转 SelectPage 选择 |
|
|
||||||
| 4 | XLR 端口阻抗 | 下拉选择 | 跳转 SelectPage 选择 |
|
|
||||||
| 5 | ARC 模式 | 下拉选择 | 跳转 SelectPage 选择 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 蓝牙页面(/bluetooth)
|
## 蓝牙页面(/bluetooth)
|
||||||
|
|
||||||
### 区块 1:页面头部
|
### 区块 1:页面头部
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ import NotFound from "./pages/NotFound";
|
|||||||
import Dashboard from "./pages/Dashboard";
|
import Dashboard from "./pages/Dashboard";
|
||||||
import AudioPage from "./pages/AudioPage";
|
import AudioPage from "./pages/AudioPage";
|
||||||
import BluetoothPage from "./pages/BluetoothPage";
|
import BluetoothPage from "./pages/BluetoothPage";
|
||||||
import DspPage from "./pages/DspPage";
|
|
||||||
import DisplayPage from "./pages/DisplayPage";
|
|
||||||
import SystemPage from "./pages/SystemPage";
|
import SystemPage from "./pages/SystemPage";
|
||||||
import IOPage from "./pages/IOPage";
|
import IOPage from "./pages/IOPage";
|
||||||
import VUPage from "./pages/VUPage";
|
import VUPage from "./pages/VUPage";
|
||||||
@@ -64,8 +62,6 @@ function AppRouter() {
|
|||||||
<Route path="/eq" component={EQPageRoute} />
|
<Route path="/eq" component={EQPageRoute} />
|
||||||
<Route path="/effects" component={EffectsPageRoute} />
|
<Route path="/effects" component={EffectsPageRoute} />
|
||||||
<Route path="/bluetooth" component={BluetoothPage} />
|
<Route path="/bluetooth" component={BluetoothPage} />
|
||||||
<Route path="/dsp" component={DspPage} />
|
|
||||||
<Route path="/display" component={DisplayPage} />
|
|
||||||
<Route path="/system" component={SystemPage} />
|
<Route path="/system" component={SystemPage} />
|
||||||
<Route path="/io" component={IOPage} />
|
<Route path="/io" component={IOPage} />
|
||||||
<Route path="/vu" component={VUPage} />
|
<Route path="/vu" component={VUPage} />
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import { cn } from "@/lib/utils";
|
|||||||
import { readAndClearAiSplitRestorePath, useAIDrawer } from "@/contexts/AIDrawerContext";
|
import { readAndClearAiSplitRestorePath, useAIDrawer } from "@/contexts/AIDrawerContext";
|
||||||
import { useIsLgUp } from "@/hooks/useIsLgUp";
|
import { useIsLgUp } from "@/hooks/useIsLgUp";
|
||||||
import localeZh from "@/locales/data-zh.json";
|
import localeZh from "@/locales/data-zh.json";
|
||||||
import localeZhHK from "@/locales/data-zh-HK.json";
|
|
||||||
import localeEn from "@/locales/data-en.json";
|
import localeEn from "@/locales/data-en.json";
|
||||||
|
import { resolveLocalePack } from "@/locales/resolveLocale";
|
||||||
import {
|
import {
|
||||||
Activity,
|
Activity,
|
||||||
Bot,
|
Bot,
|
||||||
@@ -49,10 +49,8 @@ export default function BottomNav() {
|
|||||||
const { deviceState } = useDevice();
|
const { deviceState } = useDevice();
|
||||||
|
|
||||||
const navText = useMemo((): BottomNavLocale => {
|
const navText = useMemo((): BottomNavLocale => {
|
||||||
const lang = deviceState?.language;
|
const pack = resolveLocalePack(deviceState?.language);
|
||||||
const pack =
|
return (pack.bottomNav ?? localeEn.bottomNav) as BottomNavLocale;
|
||||||
lang === 0 ? localeEn : lang === 1 ? localeZhHK : localeZh;
|
|
||||||
return (pack.bottomNav ?? localeZh.bottomNav) as BottomNavLocale;
|
|
||||||
}, [deviceState?.language]);
|
}, [deviceState?.language]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -14,9 +14,6 @@ function pickConnectLocale(): ConnectLocale {
|
|||||||
if (lang === "0") return localeEn.connect;
|
if (lang === "0") return localeEn.connect;
|
||||||
if (lang === "1") return localeZhHK.connect;
|
if (lang === "1") return localeZhHK.connect;
|
||||||
if (lang === "2") return localeZh.connect;
|
if (lang === "2") return localeZh.connect;
|
||||||
const nav = navigator.language.toLowerCase();
|
|
||||||
if (nav.startsWith("zh-hk") || nav.startsWith("zh-tw")) return localeZhHK.connect;
|
|
||||||
if (nav.startsWith("zh")) return localeZh.connect;
|
|
||||||
return localeEn.connect;
|
return localeEn.connect;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,8 @@
|
|||||||
import { useDevice } from "@/contexts/DeviceContext";
|
import { useDevice } from "@/contexts/DeviceContext";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
Activity,
|
|
||||||
Bluetooth,
|
Bluetooth,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Cpu,
|
|
||||||
Gauge,
|
Gauge,
|
||||||
Layers,
|
Layers,
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
@@ -30,8 +28,6 @@ const NAV_ITEMS: NavItem[] = [
|
|||||||
{ path: "/eq", icon: Sliders, label: "Parametric EQ" },
|
{ path: "/eq", icon: Sliders, label: "Parametric EQ" },
|
||||||
{ path: "/effects", icon: Layers, label: "Effects" },
|
{ path: "/effects", icon: Layers, label: "Effects" },
|
||||||
{ path: "/bluetooth", icon: Bluetooth, label: "Bluetooth" },
|
{ path: "/bluetooth", icon: Bluetooth, label: "Bluetooth" },
|
||||||
{ path: "/dsp", icon: Cpu, label: "DSP & DAC" },
|
|
||||||
{ path: "/display", icon: Activity, label: "Display & Lights" },
|
|
||||||
{ path: "/system", icon: Settings, label: "System" },
|
{ path: "/system", icon: Settings, label: "System" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import { useDevice } from "@/contexts/DeviceContext";
|
||||||
|
|
||||||
|
export function withDisconnectPageTitle(title: string): string {
|
||||||
|
return `${title}(Disconnect)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Append "(Disconnect)" to the page title when the device is not connected. */
|
||||||
|
export function usePageTitleWithConnection(title: string): string {
|
||||||
|
const { isConnected, isConnecting } = useDevice();
|
||||||
|
return useMemo(() => {
|
||||||
|
if (isConnected || isConnecting) return title;
|
||||||
|
return withDisconnectPageTitle(title);
|
||||||
|
}, [title, isConnected, isConnecting]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import localeEn from "./data-en.json";
|
||||||
|
import localeZh from "./data-zh.json";
|
||||||
|
import localeZhHK from "./data-zh-HK.json";
|
||||||
|
|
||||||
|
/** Device language index: 0=en, 1=zh-HK, 2=zh-CN */
|
||||||
|
export const DEFAULT_DEVICE_LANGUAGE = 0;
|
||||||
|
|
||||||
|
export type LocalePack = typeof localeEn;
|
||||||
|
|
||||||
|
export function resolveDeviceLanguage(language: number | undefined): number {
|
||||||
|
return language ?? DEFAULT_DEVICE_LANGUAGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveLocalePack(language: number | undefined): LocalePack {
|
||||||
|
const lang = resolveDeviceLanguage(language);
|
||||||
|
if (lang === 0) return localeEn;
|
||||||
|
if (lang === 1) return localeZhHK;
|
||||||
|
return localeZh;
|
||||||
|
}
|
||||||
@@ -59,15 +59,13 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|||||||
import { Streamdown } from "streamdown";
|
import { Streamdown } from "streamdown";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import localeZh from "@/locales/data-zh.json";
|
import localeZh from "@/locales/data-zh.json";
|
||||||
import localeZhHK from "@/locales/data-zh-HK.json";
|
|
||||||
import localeEn from "@/locales/data-en.json";
|
import localeEn from "@/locales/data-en.json";
|
||||||
|
import { resolveDeviceLanguage, resolveLocalePack } from "@/locales/resolveLocale";
|
||||||
|
|
||||||
type AILocale = NonNullable<(typeof localeZh)["ai"]>;
|
type AILocale = NonNullable<(typeof localeZh)["ai"]>;
|
||||||
|
|
||||||
function resolveAILocale(language: number | undefined): AILocale {
|
function resolveAILocale(language: number | undefined): AILocale {
|
||||||
const loc =
|
return resolveLocalePack(language).ai as AILocale;
|
||||||
language === 0 ? localeEn : language === 1 ? localeZhHK : localeZh;
|
|
||||||
return loc.ai as AILocale;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatText(template: string, vars: Record<string, string | number>): string {
|
function formatText(template: string, vars: Record<string, string | number>): string {
|
||||||
@@ -210,7 +208,7 @@ export default function AIPage({ variant = "page", onClose }: AIPageProps = {})
|
|||||||
const { setOpen: setAiPanelOpen } = useAIDrawer();
|
const { setOpen: setAiPanelOpen } = useAIDrawer();
|
||||||
const { isConnected, deviceState, api } = useDevice();
|
const { isConnected, deviceState, api } = useDevice();
|
||||||
const mac = deviceState?.mac ?? "";
|
const mac = deviceState?.mac ?? "";
|
||||||
const language = deviceState?.language ?? 2;
|
const language = resolveDeviceLanguage(deviceState?.language);
|
||||||
const deviceName = deviceState?.device ?? "Luxsin X9";
|
const deviceName = deviceState?.device ?? "Luxsin X9";
|
||||||
|
|
||||||
const aiText = useMemo(() => resolveAILocale(language), [language]);
|
const aiText = useMemo(() => resolveAILocale(language), [language]);
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ import {
|
|||||||
parseFirmwareVersion,
|
parseFirmwareVersion,
|
||||||
readBootSoundFromState,
|
readBootSoundFromState,
|
||||||
} from "@/lib/luxsinApi";
|
} 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 localeEn from "@/locales/data-en.json";
|
||||||
|
import { resolveLocalePack } from "@/locales/resolveLocale";
|
||||||
|
import { usePageTitleWithConnection } from "@/hooks/usePageTitleWithConnection";
|
||||||
|
|
||||||
function CyanSlider({ value, min, max, step = 1, onChange, onRelease }: {
|
function CyanSlider({ value, min, max, step = 1, onChange, onRelease }: {
|
||||||
value: number;
|
value: number;
|
||||||
@@ -123,12 +123,6 @@ type LocaleDac = {
|
|||||||
home?: { audioSet?: string };
|
home?: { audioSet?: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
const LOCALE_BY_LANG: Record<number, LocaleDac> = {
|
|
||||||
0: localeEn as LocaleDac,
|
|
||||||
1: localeZhHK as LocaleDac,
|
|
||||||
2: localeZh as LocaleDac,
|
|
||||||
};
|
|
||||||
|
|
||||||
function optionLabels(items: Array<{ index: number; label: string }> | undefined, fallback: string[]) {
|
function optionLabels(items: Array<{ index: number; label: string }> | undefined, fallback: string[]) {
|
||||||
if (!items || items.length === 0) return fallback;
|
if (!items || items.length === 0) return fallback;
|
||||||
const sorted = [...items].sort((a, b) => a.index - b.index);
|
const sorted = [...items].sort((a, b) => a.index - b.index);
|
||||||
@@ -182,8 +176,7 @@ export default function AudioPage() {
|
|||||||
const { deviceState, updateSetting, refresh, isConnected, api } = useDevice();
|
const { deviceState, updateSetting, refresh, isConnected, api } = useDevice();
|
||||||
const ds = deviceState;
|
const ds = deviceState;
|
||||||
const vuSensor = (ds as ({ vuSensor?: number } | null))?.vuSensor;
|
const vuSensor = (ds as ({ vuSensor?: number } | null))?.vuSensor;
|
||||||
const langIdx = ds?.language ?? 2;
|
const localeData = resolveLocalePack(ds?.language) as LocaleDac;
|
||||||
const localeData = LOCALE_BY_LANG[langIdx] ?? (localeZh as LocaleDac);
|
|
||||||
const dacText = localeData.dac ?? {};
|
const dacText = localeData.dac ?? {};
|
||||||
|
|
||||||
// balance 从接口获取的是乘以 10 后的值,需要除以 10 显示(例如:-130 → -13.0)
|
// balance 从接口获取的是乘以 10 后的值,需要除以 10 显示(例如:-130 → -13.0)
|
||||||
@@ -241,6 +234,8 @@ export default function AudioPage() {
|
|||||||
const DAC_ARC_OPTIONS = optionLabels(dacText.dacArc?.options, ["ARC", "EARC"]);
|
const DAC_ARC_OPTIONS = optionLabels(dacText.dacArc?.options, ["ARC", "EARC"]);
|
||||||
const XLR_OPTIONS = optionLabels(dacText.xlr?.options, ["正常", "反转"]);
|
const XLR_OPTIONS = optionLabels(dacText.xlr?.options, ["正常", "反转"]);
|
||||||
|
|
||||||
|
const pageTitle = usePageTitleWithConnection(localeData.home?.audioSet ?? "Audio");
|
||||||
|
|
||||||
const goSelect = (title: string, options: string[], selected: number, key: string) => {
|
const goSelect = (title: string, options: string[], selected: number, key: string) => {
|
||||||
navigateToSelect(title, options, selected, "/audio", key);
|
navigateToSelect(title, options, selected, "/audio", key);
|
||||||
setLocation("/select");
|
setLocation("/select");
|
||||||
@@ -253,7 +248,7 @@ export default function AudioPage() {
|
|||||||
<ChevronLeft size={24} />
|
<ChevronLeft size={24} />
|
||||||
</button>
|
</button>
|
||||||
<h1 className="flex-1 text-center text-[17px] font-semibold text-white">
|
<h1 className="flex-1 text-center text-[17px] font-semibold text-white">
|
||||||
{localeData.home?.audioSet ?? "音频设置"}
|
{pageTitle}
|
||||||
</h1>
|
</h1>
|
||||||
<div className="w-8" />
|
<div className="w-8" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import BottomNav from "@/components/BottomNav";
|
|||||||
import { useDevice } from "@/contexts/DeviceContext";
|
import { useDevice } from "@/contexts/DeviceContext";
|
||||||
import { useIsLgUp } from "@/hooks/useIsLgUp";
|
import { useIsLgUp } from "@/hooks/useIsLgUp";
|
||||||
import localeZh from "@/locales/data-zh.json";
|
import localeZh from "@/locales/data-zh.json";
|
||||||
import localeZhHK from "@/locales/data-zh-HK.json";
|
|
||||||
import localeEn from "@/locales/data-en.json";
|
import localeEn from "@/locales/data-en.json";
|
||||||
|
import { resolveLocalePack } from "@/locales/resolveLocale";
|
||||||
|
import { usePageTitleWithConnection } from "@/hooks/usePageTitleWithConnection";
|
||||||
import { ChevronLeft, Copy } from "lucide-react";
|
import { ChevronLeft, Copy } from "lucide-react";
|
||||||
import { useMemo, useCallback } from "react";
|
import { useMemo, useCallback } from "react";
|
||||||
import { useLocation } from "wouter";
|
import { useLocation } from "wouter";
|
||||||
@@ -19,11 +20,12 @@ export default function CommunityPage() {
|
|||||||
const { deviceState } = useDevice();
|
const { deviceState } = useDevice();
|
||||||
|
|
||||||
const text = useMemo((): CommunityPageLocale => {
|
const text = useMemo((): CommunityPageLocale => {
|
||||||
const lang = deviceState?.language;
|
const pack = resolveLocalePack(deviceState?.language);
|
||||||
const pack = lang === 0 ? localeEn : lang === 1 ? localeZhHK : localeZh;
|
return (pack.communityPage ?? localeEn.communityPage) as CommunityPageLocale;
|
||||||
return (pack.communityPage ?? localeZh.communityPage) as CommunityPageLocale;
|
|
||||||
}, [deviceState?.language]);
|
}, [deviceState?.language]);
|
||||||
|
|
||||||
|
const pageTitle = usePageTitleWithConnection(text.title ?? "Community");
|
||||||
|
|
||||||
const copyForumUrl = useCallback(async () => {
|
const copyForumUrl = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(FORUM_URL);
|
await navigator.clipboard.writeText(FORUM_URL);
|
||||||
@@ -76,7 +78,7 @@ export default function CommunityPage() {
|
|||||||
>
|
>
|
||||||
<ChevronLeft size={24} />
|
<ChevronLeft size={24} />
|
||||||
</button>
|
</button>
|
||||||
<h1 className="flex-1 text-center text-[17px] font-semibold text-white">{text.title}</h1>
|
<h1 className="flex-1 text-center text-[17px] font-semibold text-white">{pageTitle}</h1>
|
||||||
<div className="w-8" />
|
<div className="w-8" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,168 +0,0 @@
|
|||||||
/* ============================================================
|
|
||||||
DISPLAY & LIGHTS PAGE
|
|
||||||
Design: iOS Settings style, reference luxsin_x8_设置.png
|
|
||||||
Sections: 显示 / 灯光 / 电源管理
|
|
||||||
All selectable options → navigate to /select page
|
|
||||||
============================================================ */
|
|
||||||
import { useDevice } from "@/contexts/DeviceContext";
|
|
||||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
||||||
import { useLocation } from "wouter";
|
|
||||||
import BottomNav from "@/components/BottomNav";
|
|
||||||
import { navigateToSelect } from "./SelectPage";
|
|
||||||
|
|
||||||
function IOSToggle({ checked, onChange }: { checked: boolean; onChange: (v: boolean) => void }) {
|
|
||||||
return (
|
|
||||||
<label className="ios-toggle" onClick={(e) => e.stopPropagation()}>
|
|
||||||
<input type="checkbox" checked={checked} onChange={(e) => onChange(e.target.checked)} />
|
|
||||||
<span className="ios-toggle-track"><span className="ios-toggle-thumb" /></span>
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SectionHeader({ title }: { title: string }) {
|
|
||||||
return (
|
|
||||||
<div className="px-1 pt-5 pb-2">
|
|
||||||
<span className="text-[14px] font-semibold" style={{ color: "#00FFF6" }}>{title}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ListRow({ label, description, value, onClick, toggle, checked, onToggle }: {
|
|
||||||
label: string; description?: string; value?: string;
|
|
||||||
onClick?: () => void; toggle?: boolean; checked?: boolean; onToggle?: (v: boolean) => void;
|
|
||||||
}) {
|
|
||||||
if (toggle) {
|
|
||||||
return (
|
|
||||||
<div className="ios-list-row">
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="text-[16px] text-white">{label}</div>
|
|
||||||
{description && <div className="text-[12px] text-white/35 mt-0.5">{description}</div>}
|
|
||||||
</div>
|
|
||||||
<IOSToggle checked={!!checked} onChange={onToggle ?? (() => {})} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<button className="ios-list-row w-full text-left active:bg-white/5 transition-colors" onClick={onClick}>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="text-[16px] text-white">{label}</div>
|
|
||||||
{description && <div className="text-[12px] text-white/35 mt-0.5">{description}</div>}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
{value && <span className="ios-row-value">{value}</span>}
|
|
||||||
<ChevronRight size={16} className="ios-chevron ml-1" />
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const SCREEN_LIGHT_OPTIONS = ["亮", "中等", "暗"];
|
|
||||||
const KNOB_LIGHT_OPTIONS = ["关闭", "亮", "中等", "暗"];
|
|
||||||
const SCREEN_OFF_OPTIONS = ["常亮", "30秒", "1分钟", "3分钟", "5分钟"];
|
|
||||||
const SLEEP_OPTIONS = ["关闭", "1分钟", "5分钟", "10分钟"];
|
|
||||||
|
|
||||||
export default function DisplayPage() {
|
|
||||||
const [, setLocation] = useLocation();
|
|
||||||
const { deviceState, updateSetting } = useDevice();
|
|
||||||
const ds = deviceState;
|
|
||||||
|
|
||||||
const screenLightIdx = ds?.screenLight ?? 1;
|
|
||||||
const knobLightIdx = ds?.knob_breathlight ?? 1;
|
|
||||||
const screenOffIdx = ds?.screenOff ?? 2;
|
|
||||||
const sleepIdx = ds?.sleep ?? 0;
|
|
||||||
const buttonLightOn = (ds?.buttonLight ?? 0) === 0;
|
|
||||||
|
|
||||||
const goSelect = (title: string, options: string[], selected: number, key: string) => {
|
|
||||||
navigateToSelect(title, options, selected, "/display", key);
|
|
||||||
setLocation("/select");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-black">
|
|
||||||
<div className="page-header">
|
|
||||||
<button onClick={() => setLocation("/")} className="mr-4 text-white/60 active:text-white transition-colors">
|
|
||||||
<ChevronLeft size={24} />
|
|
||||||
</button>
|
|
||||||
<h1 className="flex-1 text-center text-[17px] font-semibold text-white">显示与灯光</h1>
|
|
||||||
<div className="w-8" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="px-4 pb-32">
|
|
||||||
{/* ── 显示 ── */}
|
|
||||||
<SectionHeader title="显示" />
|
|
||||||
<div className="ios-list-group">
|
|
||||||
<ListRow label="屏幕亮度" description="调节屏幕的整体亮度"
|
|
||||||
value={SCREEN_LIGHT_OPTIONS[screenLightIdx]}
|
|
||||||
onClick={() => goSelect("屏幕亮度", SCREEN_LIGHT_OPTIONS, screenLightIdx, "screenLight")} />
|
|
||||||
<ListRow label="自动锁屏" description="设备无操作后自动关闭屏幕"
|
|
||||||
value={SCREEN_OFF_OPTIONS[screenOffIdx]}
|
|
||||||
onClick={() => goSelect("自动锁屏", SCREEN_OFF_OPTIONS, screenOffIdx, "screenOff")} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ── 灯光 ── */}
|
|
||||||
<SectionHeader title="灯光" />
|
|
||||||
<div className="ios-list-group">
|
|
||||||
<ListRow label="旋钮亮度" description="调节旋钮指示灯的亮度"
|
|
||||||
value={KNOB_LIGHT_OPTIONS[knobLightIdx]}
|
|
||||||
onClick={() => goSelect("旋钮亮度", KNOB_LIGHT_OPTIONS, knobLightIdx, "knob_breathlight")} />
|
|
||||||
<ListRow label="旋钮熄屏呼吸灯" description="锁屏后旋钮是否显示呼吸灯效果"
|
|
||||||
toggle checked={knobLightIdx > 0}
|
|
||||||
onToggle={(v) => updateSetting({ knob_breathlight: v ? 2 : 0 })} />
|
|
||||||
<ListRow label="按键灯" description="前面板按键背光"
|
|
||||||
toggle checked={buttonLightOn}
|
|
||||||
onToggle={(v) => updateSetting({ buttonLight: v ? 0 : 1 })} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ── 电源管理 ── */}
|
|
||||||
<SectionHeader title="电源管理" />
|
|
||||||
<div className="ios-list-group">
|
|
||||||
<ListRow label="休眠" description="设备进入低功耗模式的时间"
|
|
||||||
value={SLEEP_OPTIONS[sleepIdx]}
|
|
||||||
onClick={() => goSelect("休眠时间", SLEEP_OPTIONS, sleepIdx, "sleep")} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ── 灯光预览 ── */}
|
|
||||||
<SectionHeader title="灯光预览" />
|
|
||||||
<div className="ios-list-group p-5">
|
|
||||||
<div className="flex items-center justify-center gap-10 py-2">
|
|
||||||
{/* Knob preview */}
|
|
||||||
<div className="flex flex-col items-center gap-3">
|
|
||||||
<div className="relative w-16 h-16">
|
|
||||||
<div className="w-16 h-16 rounded-full"
|
|
||||||
style={{
|
|
||||||
background: "radial-gradient(circle, #1a2a2a 40%, #0d1117 100%)",
|
|
||||||
border: "2px solid rgba(255,255,255,0.1)",
|
|
||||||
boxShadow: knobLightIdx > 0
|
|
||||||
? `0 0 ${knobLightIdx * 10}px rgba(0,255,246,${knobLightIdx * 0.2})`
|
|
||||||
: "none",
|
|
||||||
}} />
|
|
||||||
<div className="absolute inset-1 rounded-full"
|
|
||||||
style={{
|
|
||||||
border: `2px solid rgba(0,255,246,${knobLightIdx === 0 ? 0 : knobLightIdx === 1 ? 0.9 : knobLightIdx === 2 ? 0.55 : 0.3})`,
|
|
||||||
boxShadow: knobLightIdx > 0 ? `inset 0 0 8px rgba(0,255,246,0.15)` : "none",
|
|
||||||
}} />
|
|
||||||
</div>
|
|
||||||
<span className="text-[12px] text-white/40">旋钮</span>
|
|
||||||
</div>
|
|
||||||
{/* Button preview */}
|
|
||||||
<div className="flex flex-col items-center gap-3">
|
|
||||||
<div className="flex gap-2">
|
|
||||||
{[1, 2, 3].map(i => (
|
|
||||||
<div key={i} className="w-7 h-7 rounded-[8px]"
|
|
||||||
style={{
|
|
||||||
background: buttonLightOn ? "rgba(0,255,246,0.25)" : "rgba(255,255,255,0.04)",
|
|
||||||
border: "1px solid rgba(255,255,255,0.1)",
|
|
||||||
boxShadow: buttonLightOn ? "0 0 8px rgba(0,255,246,0.35)" : "none",
|
|
||||||
}} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<span className="text-[12px] text-white/40">按键</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<BottomNav />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
/* ============================================================
|
|
||||||
DSP & DAC PAGE
|
|
||||||
Design: iOS Settings style
|
|
||||||
Sections: DSP引擎 / DAC配置 / 模拟级 / 音频格式
|
|
||||||
All selectable options → navigate to /select page
|
|
||||||
============================================================ */
|
|
||||||
import { useDevice } from "@/contexts/DeviceContext";
|
|
||||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
||||||
import { useLocation } from "wouter";
|
|
||||||
import BottomNav from "@/components/BottomNav";
|
|
||||||
import { navigateToSelect } from "./SelectPage";
|
|
||||||
|
|
||||||
function IOSToggle({ checked, onChange }: { checked: boolean; onChange: (v: boolean) => void }) {
|
|
||||||
return (
|
|
||||||
<label className="ios-toggle" onClick={(e) => e.stopPropagation()}>
|
|
||||||
<input type="checkbox" checked={checked} onChange={(e) => onChange(e.target.checked)} />
|
|
||||||
<span className="ios-toggle-track"><span className="ios-toggle-thumb" /></span>
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SectionHeader({ title }: { title: string }) {
|
|
||||||
return (
|
|
||||||
<div className="px-1 pt-5 pb-2">
|
|
||||||
<span className="text-[14px] font-semibold" style={{ color: "#00FFF6" }}>{title}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ListRow({ label, description, value, onClick, toggle, checked, onToggle }: {
|
|
||||||
label: string; description?: string; value?: string;
|
|
||||||
onClick?: () => void; toggle?: boolean; checked?: boolean; onToggle?: (v: boolean) => void;
|
|
||||||
}) {
|
|
||||||
if (toggle) {
|
|
||||||
return (
|
|
||||||
<div className="ios-list-row">
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="text-[16px] text-white">{label}</div>
|
|
||||||
{description && <div className="text-[12px] text-white/35 mt-0.5">{description}</div>}
|
|
||||||
</div>
|
|
||||||
<IOSToggle checked={!!checked} onChange={onToggle ?? (() => {})} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<button className="ios-list-row w-full text-left active:bg-white/5 transition-colors" onClick={onClick}>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="text-[16px] text-white">{label}</div>
|
|
||||||
{description && <div className="text-[12px] text-white/35 mt-0.5">{description}</div>}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
{value && <span className="ios-row-value">{value}</span>}
|
|
||||||
<ChevronRight size={16} className="ios-chevron ml-1" />
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const DAC_ARC_OPTIONS = ["关闭", "模式1", "模式2", "模式3"];
|
|
||||||
const DAC_GAIN_OPTIONS = ["0dB", "+3dB", "+6dB", "+9dB"];
|
|
||||||
const DAC_IMP_OPTIONS = ["32Ω", "150Ω", "300Ω"];
|
|
||||||
const ANALOG_GAIN_OPTIONS = ["低", "中低", "中高", "高"];
|
|
||||||
const XLR_OPTIONS = ["正常", "反转"];
|
|
||||||
|
|
||||||
export default function DspPage() {
|
|
||||||
const [, setLocation] = useLocation();
|
|
||||||
const { deviceState, updateSetting } = useDevice();
|
|
||||||
const ds = deviceState;
|
|
||||||
|
|
||||||
const dspOn = (ds?.dsp_enable ?? 0) === 0;
|
|
||||||
const peqOn = (ds?.peqEnable ?? 0) === 1;
|
|
||||||
const dacVolDirect = (ds?.dacVolumeDirect ?? 0) === 1;
|
|
||||||
const dacArcIdx = ds?.dacArc ?? 0;
|
|
||||||
const dacGainIdx = ds?.dacGain ?? 0;
|
|
||||||
const dacImpIdx = ds?.dacImpedance ?? 0;
|
|
||||||
const analogGainIdx = ds?.analogGain ?? 0;
|
|
||||||
const xlrIdx = ds?.xlr ?? 0;
|
|
||||||
|
|
||||||
const goSelect = (title: string, options: string[], selected: number, key: string) => {
|
|
||||||
navigateToSelect(title, options, selected, "/dsp", key);
|
|
||||||
setLocation("/select");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-black">
|
|
||||||
<div className="page-header">
|
|
||||||
<button onClick={() => setLocation("/")} className="mr-4 text-white/60 active:text-white transition-colors">
|
|
||||||
<ChevronLeft size={24} />
|
|
||||||
</button>
|
|
||||||
<h1 className="flex-1 text-center text-[17px] font-semibold text-white">DSP & DAC</h1>
|
|
||||||
<div className="w-8" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="px-4 pb-32">
|
|
||||||
{/* ── DSP 引擎 ── */}
|
|
||||||
<SectionHeader title="DSP 引擎" />
|
|
||||||
<div className="ios-list-group">
|
|
||||||
<ListRow label="DSP 处理" description="主数字信号处理器"
|
|
||||||
toggle checked={dspOn}
|
|
||||||
onToggle={(v) => updateSetting({ dsp_enable: v ? 0 : 1 })} />
|
|
||||||
<ListRow label="参数均衡器" description="5 段参数均衡器"
|
|
||||||
toggle checked={peqOn}
|
|
||||||
onToggle={(v) => updateSetting({ peqEnable: v ? 1 : 0 })} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ── DAC 配置 ── */}
|
|
||||||
<SectionHeader title="DAC 配置" />
|
|
||||||
<div className="ios-list-group">
|
|
||||||
<ListRow label="ARC 模式" description="音频回传通道模式"
|
|
||||||
value={DAC_ARC_OPTIONS[dacArcIdx]}
|
|
||||||
onClick={() => goSelect("ARC 模式", DAC_ARC_OPTIONS, dacArcIdx, "dacArc")} />
|
|
||||||
<ListRow label="DAC 增益" description="数模转换器增益"
|
|
||||||
value={DAC_GAIN_OPTIONS[dacGainIdx]}
|
|
||||||
onClick={() => goSelect("DAC 增益", DAC_GAIN_OPTIONS, dacGainIdx, "dacGain")} />
|
|
||||||
<ListRow label="DAC 阻抗" description="输出阻抗设置"
|
|
||||||
value={DAC_IMP_OPTIONS[dacImpIdx]}
|
|
||||||
onClick={() => goSelect("DAC 阻抗", DAC_IMP_OPTIONS, dacImpIdx, "dacImpedance")} />
|
|
||||||
<ListRow label="直通音量控制" description="DAC 直通音量"
|
|
||||||
toggle checked={dacVolDirect}
|
|
||||||
onToggle={(v) => updateSetting({ dacVolumeDirect: v ? 1 : 0 })} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ── 模拟级 ── */}
|
|
||||||
<SectionHeader title="模拟级" />
|
|
||||||
<div className="ios-list-group">
|
|
||||||
<ListRow label="模拟增益" description="模拟放大器增益级"
|
|
||||||
value={ANALOG_GAIN_OPTIONS[analogGainIdx]}
|
|
||||||
onClick={() => goSelect("模拟增益", ANALOG_GAIN_OPTIONS, analogGainIdx, "analogGain")} />
|
|
||||||
<ListRow label="XLR 极性" description="XLR 输出相位极性"
|
|
||||||
value={XLR_OPTIONS[xlrIdx]}
|
|
||||||
onClick={() => goSelect("XLR 极性", XLR_OPTIONS, xlrIdx, "xlr")} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ── 音频格式 ── */}
|
|
||||||
<SectionHeader title="当前音频格式" />
|
|
||||||
<div className="ios-list-group p-4">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="w-2 h-2 rounded-full" style={{ background: "#00FFF6", boxShadow: "0 0 6px rgba(0,255,246,0.8)" }} />
|
|
||||||
<span className="text-[15px] font-semibold" style={{ color: "#00FFF6", fontFamily: "'JetBrains Mono', monospace" }}>
|
|
||||||
{ds?.audioFormat ?? "PCM 44.1kHz"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<BottomNav />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -37,8 +37,9 @@ import {
|
|||||||
} from "@/lib/luxsinApi";
|
} from "@/lib/luxsinApi";
|
||||||
import { getFilterType, getFilterShortName } from "@/lib/peqAudio";
|
import { getFilterType, getFilterShortName } from "@/lib/peqAudio";
|
||||||
import localeZh from "@/locales/data-zh.json";
|
import localeZh from "@/locales/data-zh.json";
|
||||||
import localeZhHK from "@/locales/data-zh-HK.json";
|
|
||||||
import localeEn from "@/locales/data-en.json";
|
import localeEn from "@/locales/data-en.json";
|
||||||
|
import { resolveLocalePack } from "@/locales/resolveLocale";
|
||||||
|
import { usePageTitleWithConnection } from "@/hooks/usePageTitleWithConnection";
|
||||||
import {
|
import {
|
||||||
FILTER_TYPES,
|
FILTER_TYPES,
|
||||||
BAND_FREQ_MIN,
|
BAND_FREQ_MIN,
|
||||||
@@ -106,18 +107,18 @@ export default function EQPage() {
|
|||||||
const eqOn = peqOn && !bypassOn;
|
const eqOn = peqOn && !bypassOn;
|
||||||
|
|
||||||
const eqUi = useMemo((): PeqEqUi => {
|
const eqUi = useMemo((): PeqEqUi => {
|
||||||
const lang = deviceState?.language;
|
const pack = resolveLocalePack(deviceState?.language);
|
||||||
const pack = lang === 0 ? localeEn : lang === 1 ? localeZhHK : localeZh;
|
|
||||||
const peq = pack.peq as typeof localeZh.peq;
|
const peq = pack.peq as typeof localeZh.peq;
|
||||||
return (peq.eqUi ?? (localeZh.peq as typeof localeZh.peq).eqUi) as PeqEqUi;
|
return (peq.eqUi ?? (localeEn.peq as typeof localeEn.peq).eqUi) as PeqEqUi;
|
||||||
}, [deviceState?.language]);
|
}, [deviceState?.language]);
|
||||||
|
|
||||||
const peqCardLabels = useMemo(() => {
|
const peqCardLabels = useMemo(() => {
|
||||||
const lang = deviceState?.language;
|
const pack = resolveLocalePack(deviceState?.language);
|
||||||
const pack = lang === 0 ? localeEn : lang === 1 ? localeZhHK : localeZh;
|
return pack.peq ?? localeEn.peq;
|
||||||
return pack.peq ?? localeZh.peq;
|
|
||||||
}, [deviceState?.language]);
|
}, [deviceState?.language]);
|
||||||
|
|
||||||
|
const eqPageTitle = usePageTitleWithConnection("HP-EQ");
|
||||||
|
|
||||||
const [bandsByMode, setBandsByMode] = useState<
|
const [bandsByMode, setBandsByMode] = useState<
|
||||||
Record<"A" | "B", typeof DEFAULT_BANDS>
|
Record<"A" | "B", typeof DEFAULT_BANDS>
|
||||||
>({
|
>({
|
||||||
@@ -1481,7 +1482,7 @@ export default function EQPage() {
|
|||||||
<ChevronLeft size={24} />
|
<ChevronLeft size={24} />
|
||||||
</button>
|
</button>
|
||||||
<h1 className="flex-1 text-center text-[17px] font-semibold text-white">
|
<h1 className="flex-1 text-center text-[17px] font-semibold text-white">
|
||||||
HP-EQ
|
{eqPageTitle}
|
||||||
</h1>
|
</h1>
|
||||||
<IOSToggle
|
<IOSToggle
|
||||||
checked={eqOn}
|
checked={eqOn}
|
||||||
|
|||||||
@@ -17,8 +17,9 @@ import { navigateToSelect } from "./SelectPage";
|
|||||||
import { parseFirmwareVersion, parseHearingData } from "@/lib/luxsinApi";
|
import { parseFirmwareVersion, parseHearingData } from "@/lib/luxsinApi";
|
||||||
import { isFirmwareFeatureAvailable } from "@/config/firmwareFeatures";
|
import { isFirmwareFeatureAvailable } from "@/config/firmwareFeatures";
|
||||||
import localeZh from "@/locales/data-zh.json";
|
import localeZh from "@/locales/data-zh.json";
|
||||||
import localeZhHK from "@/locales/data-zh-HK.json";
|
|
||||||
import localeEn from "@/locales/data-en.json";
|
import localeEn from "@/locales/data-en.json";
|
||||||
|
import { resolveDeviceLanguage, resolveLocalePack } from "@/locales/resolveLocale";
|
||||||
|
import { usePageTitleWithConnection } from "@/hooks/usePageTitleWithConnection";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
type EffectLocale = NonNullable<(typeof localeZh)["effect"]>;
|
type EffectLocale = NonNullable<(typeof localeZh)["effect"]>;
|
||||||
@@ -225,11 +226,12 @@ export default function EffectsPage() {
|
|||||||
const { deviceState, updateSetting } = useDevice();
|
const { deviceState, updateSetting } = useDevice();
|
||||||
|
|
||||||
const effectText = useMemo((): EffectLocale => {
|
const effectText = useMemo((): EffectLocale => {
|
||||||
const lang = deviceState?.language;
|
const pack = resolveLocalePack(deviceState?.language);
|
||||||
const pack = lang === 0 ? localeEn : lang === 1 ? localeZhHK : localeZh;
|
return (pack.effect ?? localeEn.effect) as EffectLocale;
|
||||||
return (pack.effect ?? localeZh.effect) as EffectLocale;
|
|
||||||
}, [deviceState?.language]);
|
}, [deviceState?.language]);
|
||||||
|
|
||||||
|
const pageTitle = usePageTitleWithConnection(effectText.pageTitle ?? "Effects");
|
||||||
|
|
||||||
const sceneLabels = useMemo(() => {
|
const sceneLabels = useMemo(() => {
|
||||||
const opts = effectText.style?.options ?? [];
|
const opts = effectText.style?.options ?? [];
|
||||||
return [...opts].sort((a, b) => a.index - b.index).map((o) => o.label);
|
return [...opts].sort((a, b) => a.index - b.index).map((o) => o.label);
|
||||||
@@ -346,7 +348,7 @@ export default function EffectsPage() {
|
|||||||
const [delayInputTarget, setDelayInputTarget] = useState<DelayInputTarget | null>(null);
|
const [delayInputTarget, setDelayInputTarget] = useState<DelayInputTarget | null>(null);
|
||||||
const [delayInputValue, setDelayInputValue] = useState("");
|
const [delayInputValue, setDelayInputValue] = useState("");
|
||||||
|
|
||||||
const delayInputEnglish = deviceState?.language === 0;
|
const delayInputEnglish = resolveDeviceLanguage(deviceState?.language) === 0;
|
||||||
const maxDelayMs = SUBWOOFER_DELAY_MAX / SUBWOOFER_DELAY_MS_DIVISOR;
|
const maxDelayMs = SUBWOOFER_DELAY_MAX / SUBWOOFER_DELAY_MS_DIVISOR;
|
||||||
|
|
||||||
const openDelayInput = (target: DelayInputTarget) => {
|
const openDelayInput = (target: DelayInputTarget) => {
|
||||||
@@ -423,7 +425,7 @@ export default function EffectsPage() {
|
|||||||
<button onClick={() => setLocation("/")} className="mr-4 text-white/60 active:text-white transition-colors">
|
<button onClick={() => setLocation("/")} className="mr-4 text-white/60 active:text-white transition-colors">
|
||||||
<ChevronLeft size={24} />
|
<ChevronLeft size={24} />
|
||||||
</button>
|
</button>
|
||||||
<h1 className="flex-1 text-center text-[17px] font-semibold text-white">{effectText.pageTitle}</h1>
|
<h1 className="flex-1 text-center text-[17px] font-semibold text-white">{pageTitle}</h1>
|
||||||
<IOSToggle
|
<IOSToggle
|
||||||
checked={effectOn}
|
checked={effectOn}
|
||||||
onChange={(v) => {
|
onChange={(v) => {
|
||||||
|
|||||||
@@ -12,8 +12,9 @@ import { ChevronLeft } from "lucide-react";
|
|||||||
import { useLocation } from "wouter";
|
import { useLocation } from "wouter";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import localeZh from "@/locales/data-zh.json";
|
import localeZh from "@/locales/data-zh.json";
|
||||||
import localeZhHK from "@/locales/data-zh-HK.json";
|
|
||||||
import localeEn from "@/locales/data-en.json";
|
import localeEn from "@/locales/data-en.json";
|
||||||
|
import { resolveLocalePack } from "@/locales/resolveLocale";
|
||||||
|
import { usePageTitleWithConnection } from "@/hooks/usePageTitleWithConnection";
|
||||||
|
|
||||||
type SourceLocale = NonNullable<(typeof localeZh)["source"]>;
|
type SourceLocale = NonNullable<(typeof localeZh)["source"]>;
|
||||||
|
|
||||||
@@ -212,8 +213,7 @@ export default function IOPage() {
|
|||||||
const { deviceState, setInput, setOutput } = useDevice();
|
const { deviceState, setInput, setOutput } = useDevice();
|
||||||
|
|
||||||
const sourceText = useMemo((): SourceLocale => {
|
const sourceText = useMemo((): SourceLocale => {
|
||||||
const lang = deviceState?.language;
|
const pack = resolveLocalePack(deviceState?.language);
|
||||||
const pack = lang === 0 ? localeEn : lang === 1 ? localeZhHK : localeZh;
|
|
||||||
return (pack.source ?? {}) as SourceLocale;
|
return (pack.source ?? {}) as SourceLocale;
|
||||||
}, [deviceState?.language]);
|
}, [deviceState?.language]);
|
||||||
|
|
||||||
@@ -227,7 +227,9 @@ export default function IOPage() {
|
|||||||
[sourceText.outputOptions],
|
[sourceText.outputOptions],
|
||||||
);
|
);
|
||||||
|
|
||||||
const ioPageTitle = sourceText.pageTitle ?? sourceText.label ?? "输入输出";
|
const ioPageTitle = usePageTitleWithConnection(
|
||||||
|
sourceText.pageTitle ?? sourceText.label ?? "I/O",
|
||||||
|
);
|
||||||
const inputSectionTitle = sourceText.inputSection ?? "输入";
|
const inputSectionTitle = sourceText.inputSection ?? "输入";
|
||||||
const outputSectionTitle = sourceText.outputSection ?? "输出";
|
const outputSectionTitle = sourceText.outputSection ?? "输出";
|
||||||
|
|
||||||
|
|||||||
@@ -53,10 +53,9 @@ import {
|
|||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
|
|
||||||
import localeZh from "@/locales/data-zh.json";
|
import localeZh from "@/locales/data-zh.json";
|
||||||
|
|
||||||
import localeZhHK from "@/locales/data-zh-HK.json";
|
|
||||||
|
|
||||||
import localeEn from "@/locales/data-en.json";
|
import localeEn from "@/locales/data-en.json";
|
||||||
|
import { resolveLocalePack } from "@/locales/resolveLocale";
|
||||||
|
import { usePageTitleWithConnection } from "@/hooks/usePageTitleWithConnection";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -92,18 +91,6 @@ type LocaleRoot = {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
const LOCALE_BY_LANG: Record<number, LocaleRoot> = {
|
|
||||||
|
|
||||||
0: localeEn as LocaleRoot,
|
|
||||||
|
|
||||||
1: localeZhHK as LocaleRoot,
|
|
||||||
|
|
||||||
2: localeZh as LocaleRoot,
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* ── Key → API field mapping ── */
|
/* ── Key → API field mapping ── */
|
||||||
|
|
||||||
const KEY_TO_SETTING: Record<string, string> = {
|
const KEY_TO_SETTING: Record<string, string> = {
|
||||||
@@ -232,11 +219,9 @@ export default function SelectPage() {
|
|||||||
|
|
||||||
const [passthroughConfirmIdx, setPassthroughConfirmIdx] = useState<number | null>(null);
|
const [passthroughConfirmIdx, setPassthroughConfirmIdx] = useState<number | null>(null);
|
||||||
|
|
||||||
|
const pageTitle = usePageTitleWithConnection(state?.title ?? "");
|
||||||
|
|
||||||
|
const dacVolumeDirectText = resolveLocalePack(deviceState?.language).dac?.dacVolumeDirect;
|
||||||
const langIdx = deviceState?.language ?? 2;
|
|
||||||
|
|
||||||
const dacVolumeDirectText = (LOCALE_BY_LANG[langIdx] ?? localeZh as LocaleRoot).dac?.dacVolumeDirect;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -390,7 +375,7 @@ export default function SelectPage() {
|
|||||||
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<h1 className="flex-1 text-center text-[17px] font-semibold text-white">{title}</h1>
|
<h1 className="flex-1 text-center text-[17px] font-semibold text-white">{pageTitle}</h1>
|
||||||
|
|
||||||
<div className="w-8" />
|
<div className="w-8" />
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ import { useEffect } from "react";
|
|||||||
import BottomNav from "@/components/BottomNav";
|
import BottomNav from "@/components/BottomNav";
|
||||||
import { formatFirmwareVersionDisplay } from "@/lib/luxsinApi";
|
import { formatFirmwareVersionDisplay } from "@/lib/luxsinApi";
|
||||||
import { navigateToSelect } from "./SelectPage";
|
import { navigateToSelect } from "./SelectPage";
|
||||||
import localeZh from "@/locales/data-zh.json";
|
|
||||||
import localeZhHK from "@/locales/data-zh-HK.json";
|
|
||||||
import localeEn from "@/locales/data-en.json";
|
import localeEn from "@/locales/data-en.json";
|
||||||
|
import { DEFAULT_DEVICE_LANGUAGE, resolveDeviceLanguage, resolveLocalePack } from "@/locales/resolveLocale";
|
||||||
|
import { usePageTitleWithConnection } from "@/hooks/usePageTitleWithConnection";
|
||||||
|
|
||||||
function IOSToggle({ checked, onChange }: { checked: boolean; onChange: (v: boolean) => void }) {
|
function IOSToggle({ checked, onChange }: { checked: boolean; onChange: (v: boolean) => void }) {
|
||||||
return (
|
return (
|
||||||
@@ -96,12 +96,6 @@ type LocaleSettings = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const LOCALE_BY_LANG: Record<number, LocaleSettings> = {
|
|
||||||
0: localeEn as LocaleSettings,
|
|
||||||
1: localeZhHK as LocaleSettings,
|
|
||||||
2: localeZh as LocaleSettings,
|
|
||||||
};
|
|
||||||
|
|
||||||
function optionLabels(items: Array<{ index: number; label: string }> | undefined, fallback: string[]) {
|
function optionLabels(items: Array<{ index: number; label: string }> | undefined, fallback: string[]) {
|
||||||
if (!items || items.length === 0) return fallback;
|
if (!items || items.length === 0) return fallback;
|
||||||
const sorted = [...items].sort((a, b) => a.index - b.index);
|
const sorted = [...items].sort((a, b) => a.index - b.index);
|
||||||
@@ -137,10 +131,10 @@ export default function SystemPage() {
|
|||||||
const screenOffIdx = ds?.screenOff ?? 2;
|
const screenOffIdx = ds?.screenOff ?? 2;
|
||||||
const sleepIdx = ds?.sleep ?? 0;
|
const sleepIdx = ds?.sleep ?? 0;
|
||||||
const autoHomeIdx = ds?.autoHome ?? 1;
|
const autoHomeIdx = ds?.autoHome ?? 1;
|
||||||
const rawLangIdx = ds?.language ?? 2;
|
const rawLangIdx = resolveDeviceLanguage(ds?.language);
|
||||||
const langIdx = SUPPORTED_LANGUAGE_INDICES.has(rawLangIdx) ? rawLangIdx : 2;
|
const langIdx = SUPPORTED_LANGUAGE_INDICES.has(rawLangIdx) ? rawLangIdx : DEFAULT_DEVICE_LANGUAGE;
|
||||||
const knobBreathLightOn = (ds?.knob_breathlight ?? 1) === 0;
|
const knobBreathLightOn = (ds?.knob_breathlight ?? 1) === 0;
|
||||||
const localeData = LOCALE_BY_LANG[langIdx] ?? localeZh;
|
const localeData = resolveLocalePack(ds?.language) as LocaleSettings;
|
||||||
const settingsText = localeData.settings;
|
const settingsText = localeData.settings;
|
||||||
const ui = settingsText.systemPage;
|
const ui = settingsText.systemPage;
|
||||||
const SCREEN_LIGHT_OPTIONS = optionLabels(settingsText.screenBrightness?.options, ["亮", "中等", "暗"]);
|
const SCREEN_LIGHT_OPTIONS = optionLabels(settingsText.screenBrightness?.options, ["亮", "中等", "暗"]);
|
||||||
@@ -160,6 +154,8 @@ export default function SystemPage() {
|
|||||||
(localeEn as LocaleSettings).settings.sleep?.label ??
|
(localeEn as LocaleSettings).settings.sleep?.label ??
|
||||||
"Sleep";
|
"Sleep";
|
||||||
|
|
||||||
|
const pageTitle = usePageTitleWithConnection(settingsText.vu);
|
||||||
|
|
||||||
const goSelect = (title: string, options: string[], selected: number, key: string) => {
|
const goSelect = (title: string, options: string[], selected: number, key: string) => {
|
||||||
navigateToSelect(title, options, selected, "/system", key);
|
navigateToSelect(title, options, selected, "/system", key);
|
||||||
setLocation("/select");
|
setLocation("/select");
|
||||||
@@ -171,7 +167,7 @@ export default function SystemPage() {
|
|||||||
<button onClick={() => setLocation("/")} className="mr-4 text-white/60 active:text-white transition-colors">
|
<button onClick={() => setLocation("/")} className="mr-4 text-white/60 active:text-white transition-colors">
|
||||||
<ChevronLeft size={24} />
|
<ChevronLeft size={24} />
|
||||||
</button>
|
</button>
|
||||||
<h1 className="flex-1 text-center text-[17px] font-semibold text-white">{settingsText.vu}</h1>
|
<h1 className="flex-1 text-center text-[17px] font-semibold text-white">{pageTitle}</h1>
|
||||||
<div className="w-8" />
|
<div className="w-8" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,9 @@ import { Check, ChevronLeft } from "lucide-react";
|
|||||||
import { useLocation } from "wouter";
|
import { useLocation } from "wouter";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import localeZh from "@/locales/data-zh.json";
|
import localeZh from "@/locales/data-zh.json";
|
||||||
import localeZhHK from "@/locales/data-zh-HK.json";
|
|
||||||
import localeEn from "@/locales/data-en.json";
|
import localeEn from "@/locales/data-en.json";
|
||||||
|
import { resolveLocalePack } from "@/locales/resolveLocale";
|
||||||
|
import { usePageTitleWithConnection } from "@/hooks/usePageTitleWithConnection";
|
||||||
|
|
||||||
type VUPageLocale = NonNullable<(typeof localeZh)["vuPage"]>;
|
type VUPageLocale = NonNullable<(typeof localeZh)["vuPage"]>;
|
||||||
|
|
||||||
@@ -35,12 +36,12 @@ export default function VUPage() {
|
|||||||
const currentVU = deviceState?.vu ?? 0;
|
const currentVU = deviceState?.vu ?? 0;
|
||||||
|
|
||||||
const vuPageText = useMemo((): VUPageLocale => {
|
const vuPageText = useMemo((): VUPageLocale => {
|
||||||
const lang = deviceState?.language;
|
const loc = resolveLocalePack(deviceState?.language);
|
||||||
const loc =
|
return (loc.vuPage ?? localeEn.vuPage ?? {}) as VUPageLocale;
|
||||||
lang === 0 ? localeEn : lang === 1 ? localeZhHK : localeZh;
|
|
||||||
return (loc.vuPage ?? {}) as VUPageLocale;
|
|
||||||
}, [deviceState?.language]);
|
}, [deviceState?.language]);
|
||||||
|
|
||||||
|
const pageTitle = usePageTitleWithConnection(vuPageText.title ?? "VU");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-black">
|
<div className="min-h-screen bg-black">
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
@@ -48,7 +49,7 @@ export default function VUPage() {
|
|||||||
<ChevronLeft size={24} />
|
<ChevronLeft size={24} />
|
||||||
</button>
|
</button>
|
||||||
<h1 className="flex-1 text-center text-[17px] font-semibold text-white">
|
<h1 className="flex-1 text-center text-[17px] font-semibold text-white">
|
||||||
{vuPageText.title ?? "VU 表库"}
|
{pageTitle}
|
||||||
</h1>
|
</h1>
|
||||||
<div className="w-8" />
|
<div className="w-8" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -81,6 +81,7 @@
|
|||||||
"add": "^2.0.6",
|
"add": "^2.0.6",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
|
"jiti": "^2.7.0",
|
||||||
"pnpm": "^10.15.1",
|
"pnpm": "^10.15.1",
|
||||||
"postcss": "^8.4.47",
|
"postcss": "^8.4.47",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
|
|||||||
Generated
+15
-12
@@ -167,13 +167,13 @@ importers:
|
|||||||
devDependencies:
|
devDependencies:
|
||||||
'@builder.io/vite-plugin-jsx-loc':
|
'@builder.io/vite-plugin-jsx-loc':
|
||||||
specifier: ^0.1.1
|
specifier: ^0.1.1
|
||||||
version: 0.1.1(vite@7.3.5(@types/node@24.13.1)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4))
|
version: 0.1.1(vite@7.3.5(@types/node@24.13.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4))
|
||||||
'@tailwindcss/typography':
|
'@tailwindcss/typography':
|
||||||
specifier: ^0.5.15
|
specifier: ^0.5.15
|
||||||
version: 0.5.20(tailwindcss@4.3.0)
|
version: 0.5.20(tailwindcss@4.3.0)
|
||||||
'@tailwindcss/vite':
|
'@tailwindcss/vite':
|
||||||
specifier: ^4.1.3
|
specifier: ^4.1.3
|
||||||
version: 4.3.0(vite@7.3.5(@types/node@24.13.1)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4))
|
version: 4.3.0(vite@7.3.5(@types/node@24.13.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4))
|
||||||
'@types/google.maps':
|
'@types/google.maps':
|
||||||
specifier: ^3.58.1
|
specifier: ^3.58.1
|
||||||
version: 3.65.1
|
version: 3.65.1
|
||||||
@@ -188,7 +188,7 @@ importers:
|
|||||||
version: 19.2.1(@types/react@19.2.17)
|
version: 19.2.1(@types/react@19.2.17)
|
||||||
'@vitejs/plugin-react':
|
'@vitejs/plugin-react':
|
||||||
specifier: ^5.0.4
|
specifier: ^5.0.4
|
||||||
version: 5.2.0(vite@7.3.5(@types/node@24.13.1)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4))
|
version: 5.2.0(vite@7.3.5(@types/node@24.13.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4))
|
||||||
add:
|
add:
|
||||||
specifier: ^2.0.6
|
specifier: ^2.0.6
|
||||||
version: 2.0.6
|
version: 2.0.6
|
||||||
@@ -198,6 +198,9 @@ importers:
|
|||||||
esbuild:
|
esbuild:
|
||||||
specifier: ^0.25.0
|
specifier: ^0.25.0
|
||||||
version: 0.25.10
|
version: 0.25.10
|
||||||
|
jiti:
|
||||||
|
specifier: ^2.7.0
|
||||||
|
version: 2.7.0
|
||||||
pnpm:
|
pnpm:
|
||||||
specifier: ^10.15.1
|
specifier: ^10.15.1
|
||||||
version: 10.34.1
|
version: 10.34.1
|
||||||
@@ -224,7 +227,7 @@ importers:
|
|||||||
version: 5.6.3
|
version: 5.6.3
|
||||||
vite:
|
vite:
|
||||||
specifier: ^7.1.7
|
specifier: ^7.1.7
|
||||||
version: 7.3.5(@types/node@24.13.1)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4)
|
version: 7.3.5(@types/node@24.13.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4)
|
||||||
vite-plugin-manus-runtime:
|
vite-plugin-manus-runtime:
|
||||||
specifier: ^0.0.57
|
specifier: ^0.0.57
|
||||||
version: 0.0.57
|
version: 0.0.57
|
||||||
@@ -4164,10 +4167,10 @@ snapshots:
|
|||||||
estree-walker: 2.0.2
|
estree-walker: 2.0.2
|
||||||
magic-string: 0.30.19
|
magic-string: 0.30.19
|
||||||
|
|
||||||
'@builder.io/vite-plugin-jsx-loc@0.1.1(vite@7.3.5(@types/node@24.13.1)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4))':
|
'@builder.io/vite-plugin-jsx-loc@0.1.1(vite@7.3.5(@types/node@24.13.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@builder.io/jsx-loc-internals': 0.0.1
|
'@builder.io/jsx-loc-internals': 0.0.1
|
||||||
vite: 7.3.5(@types/node@24.13.1)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4)
|
vite: 7.3.5(@types/node@24.13.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4)
|
||||||
|
|
||||||
'@chevrotain/cst-dts-gen@11.0.3':
|
'@chevrotain/cst-dts-gen@11.0.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -5427,12 +5430,12 @@ snapshots:
|
|||||||
postcss-selector-parser: 6.0.10
|
postcss-selector-parser: 6.0.10
|
||||||
tailwindcss: 4.3.0
|
tailwindcss: 4.3.0
|
||||||
|
|
||||||
'@tailwindcss/vite@4.3.0(vite@7.3.5(@types/node@24.13.1)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4))':
|
'@tailwindcss/vite@4.3.0(vite@7.3.5(@types/node@24.13.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tailwindcss/node': 4.3.0
|
'@tailwindcss/node': 4.3.0
|
||||||
'@tailwindcss/oxide': 4.3.0
|
'@tailwindcss/oxide': 4.3.0
|
||||||
tailwindcss: 4.3.0
|
tailwindcss: 4.3.0
|
||||||
vite: 7.3.5(@types/node@24.13.1)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4)
|
vite: 7.3.5(@types/node@24.13.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4)
|
||||||
|
|
||||||
'@types/babel__core@7.20.5':
|
'@types/babel__core@7.20.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -5623,7 +5626,7 @@ snapshots:
|
|||||||
|
|
||||||
'@ungap/structured-clone@1.3.1': {}
|
'@ungap/structured-clone@1.3.1': {}
|
||||||
|
|
||||||
'@vitejs/plugin-react@5.2.0(vite@7.3.5(@types/node@24.13.1)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4))':
|
'@vitejs/plugin-react@5.2.0(vite@7.3.5(@types/node@24.13.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.7
|
'@babel/core': 7.29.7
|
||||||
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.7)
|
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.7)
|
||||||
@@ -5631,7 +5634,7 @@ snapshots:
|
|||||||
'@rolldown/pluginutils': 1.0.0-rc.3
|
'@rolldown/pluginutils': 1.0.0-rc.3
|
||||||
'@types/babel__core': 7.20.5
|
'@types/babel__core': 7.20.5
|
||||||
react-refresh: 0.18.0
|
react-refresh: 0.18.0
|
||||||
vite: 7.3.5(@types/node@24.13.1)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4)
|
vite: 7.3.5(@types/node@24.13.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -7903,7 +7906,7 @@ snapshots:
|
|||||||
lightningcss: 1.32.0
|
lightningcss: 1.32.0
|
||||||
terser: 5.46.1
|
terser: 5.46.1
|
||||||
|
|
||||||
vite@7.3.5(@types/node@24.13.1)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4):
|
vite@7.3.5(@types/node@24.13.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.27.7
|
esbuild: 0.27.7
|
||||||
fdir: 6.5.0(picomatch@4.0.3)
|
fdir: 6.5.0(picomatch@4.0.3)
|
||||||
@@ -7914,7 +7917,7 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 24.13.1
|
'@types/node': 24.13.1
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
jiti: 1.21.7
|
jiti: 2.7.0
|
||||||
lightningcss: 1.32.0
|
lightningcss: 1.32.0
|
||||||
terser: 5.46.1
|
terser: 5.46.1
|
||||||
tsx: 4.22.4
|
tsx: 4.22.4
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ export default defineConfig({
|
|||||||
"@": path.resolve(import.meta.dirname, "client", "src"),
|
"@": path.resolve(import.meta.dirname, "client", "src"),
|
||||||
"@assets": path.resolve(import.meta.dirname, "attached_assets"),
|
"@assets": path.resolve(import.meta.dirname, "attached_assets"),
|
||||||
},
|
},
|
||||||
|
dedupe: ["react", "react-dom"],
|
||||||
},
|
},
|
||||||
envDir: path.resolve(import.meta.dirname),
|
envDir: path.resolve(import.meta.dirname),
|
||||||
root: path.resolve(import.meta.dirname, "client"),
|
root: path.resolve(import.meta.dirname, "client"),
|
||||||
|
|||||||
Reference in New Issue
Block a user