From cf970a6a7fa2a656a7292d583643b7b3bafb170f Mon Sep 17 00:00:00 2001 From: yangy Date: Tue, 21 Apr 2026 14:31:31 +0800 Subject: [PATCH] Refactor ConnectScreen to use logo image instead of SVG, enhance EQPage with batch editing functionality for PEQ settings, and implement power off confirmation dialog in Home component. Update localization files for new power off features. --- client/public/favicon.ico | Bin 0 -> 67646 bytes client/public/logo.png | Bin 0 -> 817 bytes client/src/components/ConnectScreen.tsx | 22 +-- client/src/lib/luxsinApi.ts | 2 + client/src/locales/data-en.json | 12 +- client/src/locales/data-zh-HK.json | 14 +- client/src/locales/data-zh.json | 14 +- client/src/pages/EQPage.tsx | 190 +++++++++++++++++++++++- client/src/pages/Home.tsx | 78 +++++++++- 9 files changed, 300 insertions(+), 32 deletions(-) create mode 100644 client/public/favicon.ico create mode 100644 client/public/logo.png diff --git a/client/public/favicon.ico b/client/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..bdb6388f4b5d5222b9f7ee27677a1f240c91d4e1 GIT binary patch literal 67646 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 literal 0 HcmV?d00001 diff --git a/client/public/logo.png b/client/public/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..121484ad28e011494950a411314b1467812d7a76 GIT binary patch literal 817 zcmV-11J3-3P)=7VS!{s(xR}JZA5Sp zEqZLCodoTo2qT0DqKGmm0;7V!0!wHSMf8A#K!qMqRFGO57v-Y(;=q66G~D}juUOb0 zhI{7BoO|X!bIzH23#C$N8E_n^1l|H2Oh0r1L%_uxD{BGP8=oSu0a&Mjx@kzV8)%k1 z3f#_MV+2nDKde*K3H~qmlw+O7s}M+L#UL;r7zb)2n~!=v1eO5R>R$#rCErk_7#;xo zbV_T>_%}ZnB!2;3xv@jQ9$=RG-+^a9E6^*Q`RmL>vLeHrO6B)i_4jLY%vqyb%n^}p zEc31a%fl+kFMum4wngb4Y2$hW**qk(!d|>Xd1T>o5}4_14*(y4dZ)VsJkId`*+4@? za6->;qDxh%e$|#Nr!yJs2HrX61xKbg!~6Px^C@EoooqgqtMxejZz(Vtk~1>SYg1?j zjEj80O()x_A9n4DP9uwtfDY%c+D|f)BN++!uxN4m?v(jEqjZ=T+1a8_>slChEubA( zTxLBe>^;6FO{!4%lpKMy9lYn{GeEyuv(w+zvq442Bjl%Fc&Bo+)-_Lb^B+iNLI~F( zH@RK0KI~dZ$LF@@8SAyAQl{eGA8xlKY~? zw7SIiW8UU9Fcw_}3xLlFb~a~)2;S1#nlu(y;IQ^V-Iu8({{@B^&BPrwdFSG`@ literal 0 HcmV?d00001 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。保存后将覆盖当前耳机参数。 +

+