diff --git a/client/public/favicon.ico b/client/public/favicon.ico new file mode 100644 index 0000000..bdb6388 Binary files /dev/null and b/client/public/favicon.ico differ diff --git a/client/public/logo.png b/client/public/logo.png new file mode 100644 index 0000000..121484a Binary files /dev/null and b/client/public/logo.png differ diff --git a/client/src/components/ConnectScreen.tsx b/client/src/components/ConnectScreen.tsx index 81ce723..5e855d4 100644 --- a/client/src/components/ConnectScreen.tsx +++ b/client/src/components/ConnectScreen.tsx @@ -24,22 +24,12 @@ export default function ConnectScreen() { return (
{/* Logo / Brand */} -
-
- - - - - - - - -
+
+ Luxsin

Luxsin X8

Controller

diff --git a/client/src/lib/luxsinApi.ts b/client/src/lib/luxsinApi.ts index c24ddc3..0262fdb 100644 --- a/client/src/lib/luxsinApi.ts +++ b/client/src/lib/luxsinApi.ts @@ -262,6 +262,8 @@ export class LuxsinAPI { setXlrPolarity(reverse: boolean) { return this.setSetting({ xlr: reverse ? 1 : 0 }); } setDacArc(earc: boolean) { return this.setSetting({ dacArc: earc ? 1 : 0 }); } setBootSound(on: boolean) { return this.setSetting({ bootSound: on ? 1 : 0 }); } + /** Device power off — `GET .../dev/info.cgi?action=setting&power=0` */ + powerOff() { return this.setSetting({ power: 0 }); } } // ============================================================ diff --git a/client/src/locales/data-en.json b/client/src/locales/data-en.json index 90c3201..aa38987 100644 --- a/client/src/locales/data-en.json +++ b/client/src/locales/data-en.json @@ -1,6 +1,16 @@ { "home": { - "audioSet": "Audio setting","power": "power", "input": "input", "output": "output", "vu":"VU meter" + "audioSet": "Audio setting", + "power": "Power", + "input": "Input", + "output": "Output", + "vu": "VU meter", + "powerOffConfirmTitle": "Turn off the device?", + "powerOffConfirmDesc": "The device will shut down and disconnect from this app. Do you want to continue?", + "powerOffConfirmOk": "Turn off", + "powerOffConfirmCancel": "Cancel", + "powerOffToastDemo": "Demo mode: disconnected from the device.", + "powerOffToastSent": "Shutdown command sent." }, "source": { "label": "Input/Output", diff --git a/client/src/locales/data-zh-HK.json b/client/src/locales/data-zh-HK.json index 05367f1..961b4f0 100644 --- a/client/src/locales/data-zh-HK.json +++ b/client/src/locales/data-zh-HK.json @@ -1,5 +1,17 @@ { - "home": { "audioSet": "音訊設定","power": "電源", "input": "輸入源", "output": "輸出端口", "vu":"VU表"}, + "home": { + "audioSet": "音訊設定", + "power": "電源", + "input": "輸入源", + "output": "輸出端口", + "vu": "VU表", + "powerOffConfirmTitle": "確認關閉電源?", + "powerOffConfirmDesc": "裝置將關機並中斷與本應用程式的連線,是否繼續?", + "powerOffConfirmOk": "關閉電源", + "powerOffConfirmCancel": "取消", + "powerOffToastDemo": "演示模式:已中斷與裝置的連線", + "powerOffToastSent": "已傳送關機指令" + }, "source": { "label": "輸入/輸出", "input": { "coaxial": "同軸", "optical": "光纖", "bluetooth": "藍牙", "rca": "類比RCA" }, diff --git a/client/src/locales/data-zh.json b/client/src/locales/data-zh.json index ecfa5df..fef4fb9 100644 --- a/client/src/locales/data-zh.json +++ b/client/src/locales/data-zh.json @@ -1,5 +1,17 @@ { - "home": { "audioSet": "音频设置", "power": "电源", "input": "输入源", "output": "输出端口", "vu":"VU表" }, + "home": { + "audioSet": "音频设置", + "power": "电源", + "input": "输入源", + "output": "输出端口", + "vu": "VU表", + "powerOffConfirmTitle": "确认关闭电源?", + "powerOffConfirmDesc": "设备将关机并断开与本应用的连接,是否继续?", + "powerOffConfirmOk": "关闭电源", + "powerOffConfirmCancel": "取消", + "powerOffToastDemo": "演示模式:已断开与设备的连接", + "powerOffToastSent": "已发送关机指令" + }, "source": { "label": "输入输出", "input": { "coaxial": "同轴", "optical": "光钎", "bluetooth": "蓝牙", "rca": "模拟RCA" }, diff --git a/client/src/pages/EQPage.tsx b/client/src/pages/EQPage.tsx index 62ac563..2ac6097 100644 --- a/client/src/pages/EQPage.tsx +++ b/client/src/pages/EQPage.tsx @@ -458,6 +458,8 @@ export default function EQPage() { const [addPresetMode, setAddPresetMode] = useState<"copy" | "flat">("copy"); const [copyPresetName, setCopyPresetName] = useState(""); const [flatPresetName, setFlatPresetName] = useState(""); + const [isBatchEditDialogOpen, setIsBatchEditDialogOpen] = useState(false); + const [batchEditText, setBatchEditText] = useState(""); type BrandDrawerTab = "brands" | "models" | "target"; const [isBrandDrawerOpen, setIsBrandDrawerOpen] = useState(false); const [brandDrawerTab, setBrandDrawerTab] = useState("brands"); @@ -624,6 +626,124 @@ export default function EQPage() { }); }; + const toCompactNumber = (value: number) => Number(value.toFixed(3)).toString(); + + const buildBatchEditText = useCallback(() => { + const preamp = Number(currentPeq?.preamp ?? 0); + const source = bands.length > 0 ? bands : DEFAULT_BANDS; + const lines = [`Preamp:${toCompactNumber(preamp)}dB`]; + for (let i = 0; i < 10; i++) { + const band = source[i] ?? DEFAULT_BANDS[i] ?? DEFAULT_BANDS[0]; + lines.push( + `Filter ${i + 1}: ${band.enabled ? "ON" : "OFF"} ${normalizeFilterType(band.type)} Fc ${toCompactNumber(band.freq)} Hz Gain ${toCompactNumber(band.gain)} dB Q ${toCompactNumber(band.q)}` + ); + } + return lines.join("\n"); + }, [bands, currentPeq?.preamp]); + + const openBatchEditDialog = () => { + setBatchEditText(buildBatchEditText()); + setIsBatchEditDialogOpen(true); + }; + + const parseBatchEditText = (raw: string) => { + const lines = raw + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean); + if (lines.length < 11) { + throw new Error("格式错误:至少需要 11 行(Preamp + 10 个滤波器)"); + } + + const preampMatch = lines[0].match(/^Preamp\s*:\s*([+-]?\d+(?:\.\d+)?)\s*dB$/i); + if (!preampMatch) { + throw new Error("格式错误:第 1 行应为 Preamp:-5.96dB"); + } + const preamp = Number(preampMatch[1]); + if (!Number.isFinite(preamp)) { + throw new Error("格式错误:Preamp 数值无效"); + } + + const parsedFilters = new Array<{ + enabled: boolean; + type: string; + freq: number; + gain: number; + q: number; + }>(10); + + for (let i = 1; i <= 10; i++) { + const line = lines[i]; + const match = line.match( + /^Filter\s+(\d+)\s*:\s*(ON|OFF)\s+([A-Za-z]+)\s+Fc\s+([+-]?\d+(?:\.\d+)?)\s+Hz\s+Gain\s+([+-]?\d+(?:\.\d+)?)\s+dB\s+Q\s+([+-]?\d+(?:\.\d+)?)$/i + ); + if (!match) { + throw new Error(`格式错误:第 ${i + 1} 行不符合 Filter 格式`); + } + + const filterNo = Number(match[1]); + if (filterNo < 1 || filterNo > 10) { + throw new Error(`格式错误:Filter 编号 ${filterNo} 超出范围`); + } + + const type = normalizeFilterType(match[3].toUpperCase()); + if (!FILTER_TYPES.includes(type)) { + throw new Error(`格式错误:Filter ${filterNo} 的类型无效(${match[3]})`); + } + + const freq = Number(match[4]); + const gain = Number(match[5]); + const q = Number(match[6]); + if (![freq, gain, q].every(Number.isFinite)) { + throw new Error(`格式错误:Filter ${filterNo} 的数值无效`); + } + + parsedFilters[filterNo - 1] = { + enabled: match[2].toUpperCase() === "ON", + type, + freq: Number(freq.toFixed(2)), + gain: Number(gain.toFixed(2)), + q: Number(q.toFixed(2)), + }; + } + + if (parsedFilters.some((item) => !item)) { + throw new Error("格式错误:Filter 1-10 必须全部提供"); + } + + return { + preamp: Number(preamp.toFixed(2)), + filters: parsedFilters as typeof bands, + }; + }; + + const handleSaveBatchEdit = () => { + try { + const parsed = parseBatchEditText(batchEditText); + let updatedPeq: { name: string; autoPre?: number; preamp?: number; canDel?: number } | undefined; + setPeqItems((prev) => + prev.map((item, i) => { + if (i !== headphoneIdx) return item; + const merged = { + ...item, + autoPre: 0, + preamp: parsed.preamp, + filters: parsed.filters.map(bandToPeqFilter), + }; + updatedPeq = merged; + return merged; + }) + ); + setBands(parsed.filters); + schedulePeqSync(updatedPeq, parsed.filters, 0); + setSelectedBand(0); + setIsBatchEditDialogOpen(false); + toast.success("批量编辑已应用"); + } catch (error) { + toast.error(error instanceof Error ? error.message : "批量编辑格式错误"); + } + }; + const applyPeqStateToUI = ( peqState: { peq?: Array<{ name: string; filters?: any[] | string }>; peqSelect?: number }, ) => { @@ -1069,7 +1189,7 @@ export default function EQPage() {
+
+

+ 共 11 行:第 1 行 Preamp,第 2-11 行 Filter 1-10。保存后将覆盖当前耳机参数。 +

+