-
-
-
+
+
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() {