version>=29 使用了新的重命名接口、EQ 排序功能
This commit is contained in:
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* Firmware version gates for UI features.
|
||||||
|
*
|
||||||
|
* Versions are compared using `parseFirmwareVersion` (e.g. 29 → 1.0.0.29).
|
||||||
|
* Adjust thresholds here instead of hardcoding in page components or API helpers.
|
||||||
|
*/
|
||||||
|
export const FIRMWARE_FEATURE_MIN_VERSION = {
|
||||||
|
/** Boot sound: indices 7–10 (-35…-50 dB); legacy firmware caps at index 6. */
|
||||||
|
bootSoundExtendedSteps: 26,
|
||||||
|
/** Ambient LED block on System page. */
|
||||||
|
ambientLed: 28,
|
||||||
|
/** PEQ preset rename (peqRename) and reorder (peqSoft) in manage dialog. */
|
||||||
|
peqPresetRenameAndSort: 29,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type FirmwareFeatureKey = keyof typeof FIRMWARE_FEATURE_MIN_VERSION;
|
||||||
|
|
||||||
|
export function isFirmwareFeatureAvailable(
|
||||||
|
feature: FirmwareFeatureKey,
|
||||||
|
firmwareVersion: number,
|
||||||
|
): boolean {
|
||||||
|
return firmwareVersion >= FIRMWARE_FEATURE_MIN_VERSION[feature];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Max `bootSound` select index (0–6 legacy, 0–10 when extended steps are supported). */
|
||||||
|
export function getBootSoundMaxIndex(firmwareVersion: number): number {
|
||||||
|
return isFirmwareFeatureAvailable("bootSoundExtendedSteps", firmwareVersion) ? 10 : 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function supportsAmbientLed(firmwareVersion: number): boolean {
|
||||||
|
return isFirmwareFeatureAvailable("ambientLed", firmwareVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function supportsPeqPresetRenameAndSort(firmwareVersion: number): boolean {
|
||||||
|
return isFirmwareFeatureAvailable("peqPresetRenameAndSort", firmwareVersion);
|
||||||
|
}
|
||||||
|
|
||||||
+41
-13
@@ -207,6 +207,20 @@ export interface PeqApplyPayload {
|
|||||||
peqApply: PeqPresetBody;
|
peqApply: PeqPresetBody;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** POST body for `/dev/info.cgi` — rename an existing PEQ preset (firmware build 29+). */
|
||||||
|
export interface PeqRenamePayload {
|
||||||
|
peqRename: {
|
||||||
|
oldName: string;
|
||||||
|
newName: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** POST body for `/dev/info.cgi` — reorder PEQ presets (firmware build 29+). */
|
||||||
|
export interface PeqSoftPayload {
|
||||||
|
peqSoft: string[];
|
||||||
|
peqSelect: number;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Input/Output Labels
|
// Input/Output Labels
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -333,6 +347,32 @@ export class LuxsinAPI {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Rename a headphone PEQ profile (firmware build 29+). */
|
||||||
|
async renamePeq(oldName: string, newName: string): Promise<void> {
|
||||||
|
const payload: PeqRenamePayload = { peqRename: { oldName, newName } };
|
||||||
|
const encoded = encodeCustomBase64(JSON.stringify(payload));
|
||||||
|
const form = new URLSearchParams();
|
||||||
|
form.set("json", encoded);
|
||||||
|
await fetch(`${this.baseUrl}/dev/info.cgi`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" },
|
||||||
|
body: form.toString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reorder headphone PEQ profiles (firmware build 29+). */
|
||||||
|
async sortPeq(names: string[], peqSelect: number): Promise<void> {
|
||||||
|
const payload: PeqSoftPayload = { peqSoft: names, peqSelect };
|
||||||
|
const encoded = encodeCustomBase64(JSON.stringify(payload));
|
||||||
|
const form = new URLSearchParams();
|
||||||
|
form.set("json", encoded);
|
||||||
|
await fetch(`${this.baseUrl}/dev/info.cgi`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" },
|
||||||
|
body: form.toString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setVolume(volume: number) { return this.setSetting({ volume }); }
|
setVolume(volume: number) { return this.setSetting({ volume }); }
|
||||||
setInput(input: number) { return this.setSetting({ input }); }
|
setInput(input: number) { return this.setSetting({ input }); }
|
||||||
setOutput(output: number) { return this.setSetting({ output }); }
|
setOutput(output: number) { return this.setSetting({ output }); }
|
||||||
@@ -390,18 +430,6 @@ export function formatFirmwareVersionDisplay(version: unknown): string {
|
|||||||
return `1.0.0.${raw}`;
|
return `1.0.0.${raw}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Max `bootSound` index available for the current firmware. */
|
|
||||||
export function getBootSoundMaxIndex(firmwareVersion: number): number {
|
|
||||||
return firmwareVersion >= 26 ? 10 : 6;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Ambient LED block on System page (firmware build 28+). */
|
|
||||||
export const AMBIENT_LED_MIN_FIRMWARE_VERSION = 28;
|
|
||||||
|
|
||||||
export function supportsAmbientLed(firmwareVersion: number): boolean {
|
|
||||||
return firmwareVersion >= AMBIENT_LED_MIN_FIRMWARE_VERSION;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clampLedChannel(value: unknown): number {
|
export function clampLedChannel(value: unknown): number {
|
||||||
const n = typeof value === "number" ? value : Number(value);
|
const n = typeof value === "number" ? value : Number(value);
|
||||||
if (!Number.isFinite(n)) return 0;
|
if (!Number.isFinite(n)) return 0;
|
||||||
@@ -417,7 +445,7 @@ export function normalizeBootSound(value: unknown): number {
|
|||||||
|
|
||||||
export function readBootSoundFromState(state: DeviceState | null | undefined): number {
|
export function readBootSoundFromState(state: DeviceState | null | undefined): number {
|
||||||
if (!state) return 0;
|
if (!state) return 0;
|
||||||
const raw = state.bootSound ?? (state as Record<string, unknown>).bootsound;
|
const raw = state.bootSound ?? (state as unknown as Record<string, unknown>).bootsound;
|
||||||
return normalizeBootSound(raw);
|
return normalizeBootSound(raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -316,6 +316,14 @@
|
|||||||
"batchDelete": "Auswahl löschen",
|
"batchDelete": "Auswahl löschen",
|
||||||
"noPresets": "Keine Voreinstellungen",
|
"noPresets": "Keine Voreinstellungen",
|
||||||
"renamePreset": "Umbenennen",
|
"renamePreset": "Umbenennen",
|
||||||
|
"movePresetUp": "Nach oben",
|
||||||
|
"movePresetDown": "Nach unten",
|
||||||
|
"confirmOrder": "Reihenfolge bestätigen",
|
||||||
|
"moveToFirst": "Ganz nach oben",
|
||||||
|
"moveToLast": "Ganz nach unten",
|
||||||
|
"currentPreset": "Aktuell",
|
||||||
|
"toastOrderSaved": "Reihenfolge gespeichert",
|
||||||
|
"toastOrderFail": "Reihenfolge konnte nicht gespeichert werden",
|
||||||
"deletePreset": "Löschen",
|
"deletePreset": "Löschen",
|
||||||
"toastRenamed": "Umbenannt",
|
"toastRenamed": "Umbenannt",
|
||||||
"toastRenameFail": "Umbenennung fehlgeschlagen",
|
"toastRenameFail": "Umbenennung fehlgeschlagen",
|
||||||
|
|||||||
@@ -316,6 +316,14 @@
|
|||||||
"batchDelete": "Delete selected",
|
"batchDelete": "Delete selected",
|
||||||
"noPresets": "No presets",
|
"noPresets": "No presets",
|
||||||
"renamePreset": "Rename",
|
"renamePreset": "Rename",
|
||||||
|
"movePresetUp": "Move up",
|
||||||
|
"movePresetDown": "Move down",
|
||||||
|
"confirmOrder": "Apply Order",
|
||||||
|
"moveToFirst": "Move Up To First",
|
||||||
|
"moveToLast": "Move Down To Last",
|
||||||
|
"currentPreset": "Current",
|
||||||
|
"toastOrderSaved": "Order saved",
|
||||||
|
"toastOrderFail": "Failed to save order",
|
||||||
"deletePreset": "Delete",
|
"deletePreset": "Delete",
|
||||||
"toastRenamed": "Renamed",
|
"toastRenamed": "Renamed",
|
||||||
"toastRenameFail": "Rename failed",
|
"toastRenameFail": "Rename failed",
|
||||||
|
|||||||
@@ -316,6 +316,14 @@
|
|||||||
"batchDelete": "Eliminar selección",
|
"batchDelete": "Eliminar selección",
|
||||||
"noPresets": "Sin presets",
|
"noPresets": "Sin presets",
|
||||||
"renamePreset": "Renombrar",
|
"renamePreset": "Renombrar",
|
||||||
|
"movePresetUp": "Subir",
|
||||||
|
"movePresetDown": "Bajar",
|
||||||
|
"confirmOrder": "Confirmar orden",
|
||||||
|
"moveToFirst": "Mover al inicio",
|
||||||
|
"moveToLast": "Mover al final",
|
||||||
|
"currentPreset": "Actual",
|
||||||
|
"toastOrderSaved": "Orden guardado",
|
||||||
|
"toastOrderFail": "No se pudo guardar el orden",
|
||||||
"deletePreset": "Eliminar",
|
"deletePreset": "Eliminar",
|
||||||
"toastRenamed": "Renombrado",
|
"toastRenamed": "Renombrado",
|
||||||
"toastRenameFail": "Error al renombrar",
|
"toastRenameFail": "Error al renombrar",
|
||||||
|
|||||||
@@ -316,6 +316,14 @@
|
|||||||
"batchDelete": "Supprimer la sélection",
|
"batchDelete": "Supprimer la sélection",
|
||||||
"noPresets": "Aucun préréglage",
|
"noPresets": "Aucun préréglage",
|
||||||
"renamePreset": "Renommer",
|
"renamePreset": "Renommer",
|
||||||
|
"movePresetUp": "Monter",
|
||||||
|
"movePresetDown": "Descendre",
|
||||||
|
"confirmOrder": "Confirmer l'ordre",
|
||||||
|
"moveToFirst": "Déplacer en premier",
|
||||||
|
"moveToLast": "Déplacer en dernier",
|
||||||
|
"currentPreset": "Actuel",
|
||||||
|
"toastOrderSaved": "Ordre enregistré",
|
||||||
|
"toastOrderFail": "Échec de l'enregistrement de l'ordre",
|
||||||
"deletePreset": "Supprimer",
|
"deletePreset": "Supprimer",
|
||||||
"toastRenamed": "Renommé",
|
"toastRenamed": "Renommé",
|
||||||
"toastRenameFail": "Échec du renommage",
|
"toastRenameFail": "Échec du renommage",
|
||||||
|
|||||||
@@ -316,6 +316,14 @@
|
|||||||
"batchDelete": "批量刪除",
|
"batchDelete": "批量刪除",
|
||||||
"noPresets": "暫無預設",
|
"noPresets": "暫無預設",
|
||||||
"renamePreset": "重新命名",
|
"renamePreset": "重新命名",
|
||||||
|
"movePresetUp": "上移",
|
||||||
|
"movePresetDown": "下移",
|
||||||
|
"confirmOrder": "確認順序",
|
||||||
|
"moveToFirst": "置頂",
|
||||||
|
"moveToLast": "置底",
|
||||||
|
"currentPreset": "目前選中",
|
||||||
|
"toastOrderSaved": "順序已儲存",
|
||||||
|
"toastOrderFail": "儲存順序失敗",
|
||||||
"deletePreset": "刪除",
|
"deletePreset": "刪除",
|
||||||
"toastRenamed": "已重新命名",
|
"toastRenamed": "已重新命名",
|
||||||
"toastRenameFail": "重新命名失敗",
|
"toastRenameFail": "重新命名失敗",
|
||||||
|
|||||||
@@ -316,6 +316,14 @@
|
|||||||
"batchDelete": "批量删除",
|
"batchDelete": "批量删除",
|
||||||
"noPresets": "暂无预设",
|
"noPresets": "暂无预设",
|
||||||
"renamePreset": "重命名",
|
"renamePreset": "重命名",
|
||||||
|
"movePresetUp": "上移",
|
||||||
|
"movePresetDown": "下移",
|
||||||
|
"confirmOrder": "确认顺序",
|
||||||
|
"moveToFirst": "置顶",
|
||||||
|
"moveToLast": "置底",
|
||||||
|
"currentPreset": "当前选中",
|
||||||
|
"toastOrderSaved": "顺序已保存",
|
||||||
|
"toastOrderFail": "保存顺序失败",
|
||||||
"deletePreset": "删除",
|
"deletePreset": "删除",
|
||||||
"toastRenamed": "已重命名",
|
"toastRenamed": "已重命名",
|
||||||
"toastRenameFail": "重命名失败",
|
"toastRenameFail": "重命名失败",
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ import { useLocation } from "wouter";
|
|||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import BottomNav from "@/components/BottomNav";
|
import BottomNav from "@/components/BottomNav";
|
||||||
import { navigateToSelect } from "./SelectPage";
|
import { navigateToSelect } from "./SelectPage";
|
||||||
|
import { getBootSoundMaxIndex } from "@/config/firmwareFeatures";
|
||||||
import {
|
import {
|
||||||
getBootSoundMaxIndex,
|
|
||||||
normalizeBootSound,
|
normalizeBootSound,
|
||||||
parseFirmwareVersion,
|
parseFirmwareVersion,
|
||||||
readBootSoundFromState,
|
readBootSoundFromState,
|
||||||
|
|||||||
+63
-21
@@ -21,12 +21,14 @@ import { toast } from "sonner";
|
|||||||
import {
|
import {
|
||||||
fetchLuxsinAudioCurve,
|
fetchLuxsinAudioCurve,
|
||||||
normalizePeqFiltersForSubmit,
|
normalizePeqFiltersForSubmit,
|
||||||
|
parseFirmwareVersion,
|
||||||
type PeqFilter,
|
type PeqFilter,
|
||||||
type PeqApplyPayload,
|
type PeqApplyPayload,
|
||||||
type PeqChangePayload,
|
type PeqChangePayload,
|
||||||
type PeqState,
|
type PeqState,
|
||||||
buildPeqPresetBody,
|
buildPeqPresetBody,
|
||||||
} from "@/lib/luxsinApi";
|
} from "@/lib/luxsinApi";
|
||||||
|
import { supportsPeqPresetRenameAndSort } from "@/config/firmwareFeatures";
|
||||||
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 localeEn from "@/locales/data-en.json";
|
import localeEn from "@/locales/data-en.json";
|
||||||
@@ -101,6 +103,12 @@ export default function EQPage() {
|
|||||||
return (peq.eqUi ?? (localeEn.peq as typeof localeEn.peq).eqUi) as PeqEqUi;
|
return (peq.eqUi ?? (localeEn.peq as typeof localeEn.peq).eqUi) as PeqEqUi;
|
||||||
}, [deviceState?.language]);
|
}, [deviceState?.language]);
|
||||||
|
|
||||||
|
const firmwareVersion = useMemo(
|
||||||
|
() => parseFirmwareVersion(deviceState?.version),
|
||||||
|
[deviceState?.version],
|
||||||
|
);
|
||||||
|
const peqReorderEnabled = supportsPeqPresetRenameAndSort(firmwareVersion);
|
||||||
|
|
||||||
const peqCardLabels = useMemo(() => {
|
const peqCardLabels = useMemo(() => {
|
||||||
const pack = resolveLocalePack(deviceState?.language);
|
const pack = resolveLocalePack(deviceState?.language);
|
||||||
return pack.peq ?? localeEn.peq;
|
return pack.peq ?? localeEn.peq;
|
||||||
@@ -526,15 +534,18 @@ export default function EQPage() {
|
|||||||
const applyPeqStateToUI = (
|
const applyPeqStateToUI = (
|
||||||
remote: { peq?: Array<{ name: string; filters?: any[] | string }>; peqSelect?: number; filters?: PeqFilter[] },
|
remote: { peq?: Array<{ name: string; filters?: any[] | string }>; peqSelect?: number; filters?: PeqFilter[] },
|
||||||
) => {
|
) => {
|
||||||
|
const resolvedPeqSelect =
|
||||||
|
remote.peqSelect ?? deviceState?.peqSelect ?? peqState?.peqSelect ?? 0;
|
||||||
|
|
||||||
if (remote.peq !== undefined) {
|
if (remote.peq !== undefined) {
|
||||||
syncPeqCatalog({
|
syncPeqCatalog({
|
||||||
filters: remote.filters ?? peqState?.filters ?? [],
|
filters: remote.filters ?? peqState?.filters ?? [],
|
||||||
peq: remote.peq,
|
peq: remote.peq,
|
||||||
peqSelect: remote.peqSelect ?? deviceState?.peqSelect ?? peqState?.peqSelect,
|
peqSelect: resolvedPeqSelect,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
lastPeqCatalogSyncKeyRef.current = buildPeqCatalogSyncKey(remote, deviceState?.peqSelect);
|
lastPeqCatalogSyncKeyRef.current = buildPeqCatalogSyncKey(remote, resolvedPeqSelect);
|
||||||
syncPeqCatalogFromState(remote, deviceState?.peqSelect);
|
syncPeqCatalogFromState(remote, resolvedPeqSelect);
|
||||||
|
|
||||||
const items = mergeRemotePeqCatalog((remote.peq ?? []) as PeqCatalogItem[]);
|
const items = mergeRemotePeqCatalog((remote.peq ?? []) as PeqCatalogItem[]);
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
@@ -544,7 +555,7 @@ export default function EQPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nextIdx = Math.min(
|
const nextIdx = Math.min(
|
||||||
Math.max(deviceState?.peqSelect ?? remote.peqSelect ?? 0, 0),
|
Math.max(resolvedPeqSelect, 0),
|
||||||
items.length - 1,
|
items.length - 1,
|
||||||
);
|
);
|
||||||
const nextBands = normalizeFiltersFromPeq(items[nextIdx] as { filters?: any[] | string });
|
const nextBands = normalizeFiltersFromPeq(items[nextIdx] as { filters?: any[] | string });
|
||||||
@@ -866,10 +877,11 @@ export default function EQPage() {
|
|||||||
// msgCount 轮询更新 peqState 后,同步耳机列表与当前选中项
|
// msgCount 轮询更新 peqState 后,同步耳机列表与当前选中项
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!peqState?.peq) return;
|
if (!peqState?.peq) return;
|
||||||
const key = buildPeqCatalogSyncKey(peqState, deviceState?.peqSelect);
|
const resolvedPeqSelect = peqState.peqSelect ?? deviceState?.peqSelect;
|
||||||
|
const key = buildPeqCatalogSyncKey(peqState, resolvedPeqSelect);
|
||||||
if (!key || key === lastPeqCatalogSyncKeyRef.current) return;
|
if (!key || key === lastPeqCatalogSyncKeyRef.current) return;
|
||||||
lastPeqCatalogSyncKeyRef.current = key;
|
lastPeqCatalogSyncKeyRef.current = key;
|
||||||
syncPeqCatalogFromState(peqState, deviceState?.peqSelect);
|
syncPeqCatalogFromState(peqState, resolvedPeqSelect);
|
||||||
}, [peqState, deviceState?.peqSelect, syncPeqCatalogFromState]);
|
}, [peqState, deviceState?.peqSelect, syncPeqCatalogFromState]);
|
||||||
|
|
||||||
// 切换耳机时默认回到 A 组对比
|
// 切换耳机时默认回到 A 组对比
|
||||||
@@ -1872,6 +1884,7 @@ export default function EQPage() {
|
|||||||
open={isPresetManageDialogOpen}
|
open={isPresetManageDialogOpen}
|
||||||
items={peqItems}
|
items={peqItems}
|
||||||
selectedIdx={headphoneIdx}
|
selectedIdx={headphoneIdx}
|
||||||
|
reorderEnabled={peqReorderEnabled}
|
||||||
onClose={() => setIsPresetManageDialogOpen(false)}
|
onClose={() => setIsPresetManageDialogOpen(false)}
|
||||||
eqUi={eqUi}
|
eqUi={eqUi}
|
||||||
onDeletePresets={async (names) => {
|
onDeletePresets={async (names) => {
|
||||||
@@ -1896,6 +1909,26 @@ export default function EQPage() {
|
|||||||
}}
|
}}
|
||||||
onRenamePreset={async (oldName, newName, item) => {
|
onRenamePreset={async (oldName, newName, item) => {
|
||||||
try {
|
try {
|
||||||
|
if (isDemoMode || !api) {
|
||||||
|
const nextItems = peqItems.map(it =>
|
||||||
|
it.name === oldName ? { ...it, name: newName } : it
|
||||||
|
);
|
||||||
|
delete peqPresetCacheRef.current[oldName];
|
||||||
|
applyPeqStateToUI({ peq: nextItems, peqSelect: headphoneIdx });
|
||||||
|
toast.success(eqUi.toastRenamed);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firmwareVersion = parseFirmwareVersion(deviceState?.version);
|
||||||
|
if (supportsPeqPresetRenameAndSort(firmwareVersion)) {
|
||||||
|
await api.renamePeq(oldName, newName);
|
||||||
|
delete peqPresetCacheRef.current[oldName];
|
||||||
|
const latest = await fetchEqSyncPeq(api, "renamePreset");
|
||||||
|
applyPeqStateToUI(latest);
|
||||||
|
toast.success(eqUi.toastRenamed);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const rawFilters = normalizePeqFiltersForSubmit(item.filters);
|
const rawFilters = normalizePeqFiltersForSubmit(item.filters);
|
||||||
const filters = rawFilters.map(f => ({
|
const filters = rawFilters.map(f => ({
|
||||||
...f,
|
...f,
|
||||||
@@ -1915,26 +1948,12 @@ export default function EQPage() {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isDemoMode || !api) {
|
// Legacy (< 29): peqChange then peqRemove
|
||||||
const nextItems = peqItems.map(it =>
|
|
||||||
it.name === oldName ? { ...it, name: newName } : it
|
|
||||||
);
|
|
||||||
delete peqPresetCacheRef.current[oldName];
|
|
||||||
applyPeqStateToUI({ peq: nextItems, peqSelect: headphoneIdx });
|
|
||||||
toast.success(eqUi.toastRenamed);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 1: peqChange — create new preset with new name + same EQ data
|
|
||||||
// Use api.upgradePeqChange directly (NOT the DeviceContext wrapper)
|
|
||||||
// to avoid applyPeqFiltersToState side-effect that overwrites current UI bands.
|
|
||||||
await api.upgradePeqChange(payload);
|
await api.upgradePeqChange(payload);
|
||||||
await new Promise(r => setTimeout(r, 300));
|
await new Promise(r => setTimeout(r, 300));
|
||||||
// Step 2: peqRemove — delete the old preset
|
|
||||||
delete peqPresetCacheRef.current[oldName];
|
delete peqPresetCacheRef.current[oldName];
|
||||||
await api.removePeq([oldName]);
|
await api.removePeq([oldName]);
|
||||||
await new Promise(r => setTimeout(r, 300));
|
await new Promise(r => setTimeout(r, 300));
|
||||||
// Step 3: sync latest state from device
|
|
||||||
const latest = await fetchEqSyncPeq(api, "renamePreset");
|
const latest = await fetchEqSyncPeq(api, "renamePreset");
|
||||||
applyPeqStateToUI(latest);
|
applyPeqStateToUI(latest);
|
||||||
toast.success(eqUi.toastRenamed);
|
toast.success(eqUi.toastRenamed);
|
||||||
@@ -1944,6 +1963,29 @@ export default function EQPage() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onConfirmOrder={async (names, peqSelect) => {
|
||||||
|
try {
|
||||||
|
if (isDemoMode || !api) {
|
||||||
|
const byName = new Map(peqItems.map(it => [it.name, it]));
|
||||||
|
const nextItems = names
|
||||||
|
.map(name => byName.get(name))
|
||||||
|
.filter((it): it is NonNullable<typeof it> => !!it);
|
||||||
|
applyPeqStateToUI({ peq: nextItems, peqSelect });
|
||||||
|
toast.success(eqUi.toastOrderSaved);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
await api.sortPeq(names, peqSelect);
|
||||||
|
const latest = await fetchEqSyncPeq(api, "sortPresets");
|
||||||
|
const nextSelect = latest.peqSelect ?? peqSelect;
|
||||||
|
applyPeqStateToUI({ ...latest, peqSelect: nextSelect });
|
||||||
|
updateSetting({ peqSelect: nextSelect });
|
||||||
|
toast.success(eqUi.toastOrderSaved);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
toast.error(eqUi.toastOrderFail);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ShareDialog
|
<ShareDialog
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ import { useLocation } from "wouter";
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import BottomNav from "@/components/BottomNav";
|
import BottomNav from "@/components/BottomNav";
|
||||||
import { navigateToSelect } from "./SelectPage";
|
import { navigateToSelect } from "./SelectPage";
|
||||||
|
import { supportsAmbientLed } from "@/config/firmwareFeatures";
|
||||||
import {
|
import {
|
||||||
clampLedChannel,
|
clampLedChannel,
|
||||||
formatFirmwareVersionDisplay,
|
formatFirmwareVersionDisplay,
|
||||||
parseFirmwareVersion,
|
parseFirmwareVersion,
|
||||||
supportsAmbientLed,
|
|
||||||
} from "@/lib/luxsinApi";
|
} from "@/lib/luxsinApi";
|
||||||
import localeEn from "@/locales/data-en.json";
|
import localeEn from "@/locales/data-en.json";
|
||||||
import { DEFAULT_DEVICE_LANGUAGE, resolveDeviceLanguage, resolveLocalePack } from "@/locales/resolveLocale";
|
import { DEFAULT_DEVICE_LANGUAGE, resolveDeviceLanguage, resolveLocalePack } from "@/locales/resolveLocale";
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { X, Trash2, Pencil, Check, Loader2 } from "lucide-react";
|
import { X, Trash2, Pencil, Check, Loader2, ArrowBigDown, ArrowBigUp, ArrowBigUpDash, ArrowBigDownDash } from "lucide-react";
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import type { PeqEqUi } from "../types";
|
import type { PeqEqUi } from "../types";
|
||||||
@@ -61,21 +62,26 @@ type PeqItem = {
|
|||||||
* Behavior:
|
* Behavior:
|
||||||
* - Delete (single / batch): calls onDeletePresets callback with preset names,
|
* - Delete (single / batch): calls onDeletePresets callback with preset names,
|
||||||
* which should call info.cgi with { peqRemove: names } then sync.
|
* which should call info.cgi with { peqRemove: names } then sync.
|
||||||
* - Rename: calls onRenamePreset(oldName, newName, item) callback which
|
* - Rename: calls onRenamePreset(oldName, newName, item) callback.
|
||||||
* should: 1) peqChange with new name + same EQ data 2) peqRemove old name.
|
* Firmware >= 29: peqRename. Firmware < 29: peqChange then peqRemove.
|
||||||
|
* - Reorder (firmware >= 29): up/down buttons + confirm order → peqSoft.
|
||||||
*/
|
*/
|
||||||
export function PeqPresetManageDialog({
|
export function PeqPresetManageDialog({
|
||||||
open,
|
open,
|
||||||
items,
|
items,
|
||||||
selectedIdx,
|
selectedIdx,
|
||||||
|
reorderEnabled,
|
||||||
onClose,
|
onClose,
|
||||||
eqUi,
|
eqUi,
|
||||||
onDeletePresets,
|
onDeletePresets,
|
||||||
onRenamePreset,
|
onRenamePreset,
|
||||||
|
onConfirmOrder,
|
||||||
}: {
|
}: {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
items: PeqItem[];
|
items: PeqItem[];
|
||||||
selectedIdx: number;
|
selectedIdx: number;
|
||||||
|
/** Firmware >= 29: show reorder controls and confirm-order button. */
|
||||||
|
reorderEnabled: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
eqUi: PeqEqUi;
|
eqUi: PeqEqUi;
|
||||||
/**
|
/**
|
||||||
@@ -84,24 +90,36 @@ export function PeqPresetManageDialog({
|
|||||||
*/
|
*/
|
||||||
onDeletePresets: (names: string[]) => Promise<boolean>;
|
onDeletePresets: (names: string[]) => Promise<boolean>;
|
||||||
/**
|
/**
|
||||||
* Called to rename a preset. Implementation should:
|
* Called to rename a preset. Implementation is version-dependent:
|
||||||
* 1) upgradePeqChange({ peqChange: { name: newName, filters, ...rest } })
|
* - firmware >= 29: api.renamePeq(oldName, newName) then sync
|
||||||
* 2) api.removePeq([oldName])
|
* - firmware < 29: peqChange + peqRemove then sync
|
||||||
* 3) fetchEqSyncPeq + applyPeqStateToUI
|
|
||||||
* Return true on success, false on failure.
|
* Return true on success, false on failure.
|
||||||
*/
|
*/
|
||||||
onRenamePreset: (oldName: string, newName: string, item: PeqItem) => Promise<boolean>;
|
onRenamePreset: (oldName: string, newName: string, item: PeqItem) => Promise<boolean>;
|
||||||
|
/**
|
||||||
|
* Called to persist a new preset order (firmware >= 29).
|
||||||
|
* `names` is the full ordered list; `peqSelect` is the index of the currently selected preset.
|
||||||
|
*/
|
||||||
|
onConfirmOrder: (names: string[], peqSelect: number) => Promise<boolean>;
|
||||||
}) {
|
}) {
|
||||||
const [checked, setChecked] = useState<Record<number, boolean>>({});
|
const [checked, setChecked] = useState<Record<number, boolean>>({});
|
||||||
const [editingIdx, setEditingIdx] = useState<number | null>(null);
|
const [editingIdx, setEditingIdx] = useState<number | null>(null);
|
||||||
const [editingName, setEditingName] = useState("");
|
const [editingName, setEditingName] = useState("");
|
||||||
|
const [draftItems, setDraftItems] = useState<PeqItem[]>(items);
|
||||||
|
const [draftSelectedIdx, setDraftSelectedIdx] = useState(selectedIdx);
|
||||||
|
|
||||||
const [deleting, setDeleting] = useState(false);
|
const [deleting, setDeleting] = useState(false);
|
||||||
const [renaming, setRenaming] = useState(false);
|
const [renaming, setRenaming] = useState(false);
|
||||||
|
const [confirmingOrder, setConfirmingOrder] = useState(false);
|
||||||
const [deleteConfirmIdxs, setDeleteConfirmIdxs] = useState<number[] | null>(
|
const [deleteConfirmIdxs, setDeleteConfirmIdxs] = useState<number[] | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const prevOpenRef = useRef(false);
|
||||||
|
|
||||||
|
const displayItems = reorderEnabled ? draftItems : items;
|
||||||
|
const displaySelectedIdx = reorderEnabled ? draftSelectedIdx : selectedIdx;
|
||||||
|
|
||||||
const checkedIdxs = useMemo(
|
const checkedIdxs = useMemo(
|
||||||
() =>
|
() =>
|
||||||
Object.entries(checked)
|
Object.entries(checked)
|
||||||
@@ -110,11 +128,31 @@ export function PeqPresetManageDialog({
|
|||||||
[checked]
|
[checked]
|
||||||
);
|
);
|
||||||
|
|
||||||
const allChecked = items.length > 0 && checkedIdxs.length === items.length;
|
const singleCheckedIdx = checkedIdxs.length === 1 ? checkedIdxs[0] : null;
|
||||||
|
|
||||||
|
const allChecked = displayItems.length > 0 && checkedIdxs.length === displayItems.length;
|
||||||
|
|
||||||
|
const orderChanged = useMemo(() => {
|
||||||
|
if (!reorderEnabled) return false;
|
||||||
|
if (draftItems.length !== items.length) return true;
|
||||||
|
return draftItems.some((it, i) => it.name !== items[i]?.name);
|
||||||
|
}, [reorderEnabled, draftItems, items]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) setDeleteConfirmIdxs(null);
|
if (!open) {
|
||||||
}, [open]);
|
prevOpenRef.current = false;
|
||||||
|
setDeleteConfirmIdxs(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!prevOpenRef.current) {
|
||||||
|
setDraftItems(items);
|
||||||
|
setDraftSelectedIdx(selectedIdx);
|
||||||
|
setChecked({});
|
||||||
|
setEditingIdx(null);
|
||||||
|
setEditingName("");
|
||||||
|
}
|
||||||
|
prevOpenRef.current = true;
|
||||||
|
}, [open, items, selectedIdx]);
|
||||||
|
|
||||||
if (!open) return null;
|
if (!open) return null;
|
||||||
|
|
||||||
@@ -124,7 +162,7 @@ export function PeqPresetManageDialog({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const next: Record<number, boolean> = {};
|
const next: Record<number, boolean> = {};
|
||||||
for (let i = 0; i < items.length; i++) next[i] = true;
|
for (let i = 0; i < displayItems.length; i++) next[i] = true;
|
||||||
setChecked(next);
|
setChecked(next);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -133,11 +171,49 @@ export function PeqPresetManageDialog({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const beginRename = (idx: number) => {
|
const beginRename = (idx: number) => {
|
||||||
const name = items[idx]?.name ?? "";
|
const name = displayItems[idx]?.name ?? "";
|
||||||
setEditingIdx(idx);
|
setEditingIdx(idx);
|
||||||
setEditingName(name);
|
setEditingName(name);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const movePreset = (idx: number, direction: "up" | "down") => {
|
||||||
|
const targetIdx = direction === "up" ? idx - 1 : idx + 1;
|
||||||
|
if (targetIdx < 0 || targetIdx >= draftItems.length) return;
|
||||||
|
|
||||||
|
setDraftItems(prev => {
|
||||||
|
const next = [...prev];
|
||||||
|
[next[idx], next[targetIdx]] = [next[targetIdx], next[idx]];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
setDraftSelectedIdx(prev => {
|
||||||
|
if (prev === idx) return targetIdx;
|
||||||
|
if (prev === targetIdx) return idx;
|
||||||
|
return prev;
|
||||||
|
});
|
||||||
|
setChecked({});
|
||||||
|
setEditingIdx(null);
|
||||||
|
setEditingName("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const movePresetToEdge = (idx: number, edge: "first" | "last") => {
|
||||||
|
if (idx < 0 || idx >= draftItems.length) return;
|
||||||
|
const selectedName = draftItems[draftSelectedIdx]?.name ?? null;
|
||||||
|
const next = [...draftItems];
|
||||||
|
const [item] = next.splice(idx, 1);
|
||||||
|
if (edge === "first") next.unshift(item);
|
||||||
|
else next.push(item);
|
||||||
|
|
||||||
|
const newSelectedIdx = selectedName
|
||||||
|
? next.findIndex(it => it.name === selectedName)
|
||||||
|
: draftSelectedIdx;
|
||||||
|
|
||||||
|
setDraftItems(next);
|
||||||
|
setDraftSelectedIdx(newSelectedIdx >= 0 ? newSelectedIdx : 0);
|
||||||
|
setChecked({});
|
||||||
|
setEditingIdx(null);
|
||||||
|
setEditingName("");
|
||||||
|
};
|
||||||
|
|
||||||
const commitRename = async () => {
|
const commitRename = async () => {
|
||||||
if (editingIdx === null) return;
|
if (editingIdx === null) return;
|
||||||
const nextName = editingName.trim();
|
const nextName = editingName.trim();
|
||||||
@@ -149,14 +225,14 @@ export function PeqPresetManageDialog({
|
|||||||
toast.error(eqUi.addPresetNameTooLong);
|
toast.error(eqUi.addPresetNameTooLong);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const oldItem = items[editingIdx];
|
const oldItem = displayItems[editingIdx];
|
||||||
if (!oldItem) return;
|
if (!oldItem) return;
|
||||||
if (nextName === oldItem.name) {
|
if (nextName === oldItem.name) {
|
||||||
setEditingIdx(null);
|
setEditingIdx(null);
|
||||||
setEditingName("");
|
setEditingName("");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (items.some((it, i) => i !== editingIdx && it.name === nextName)) {
|
if (displayItems.some((it, i) => i !== editingIdx && it.name === nextName)) {
|
||||||
toast.error(eqUi.renameNameExists);
|
toast.error(eqUi.renameNameExists);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -165,6 +241,11 @@ export function PeqPresetManageDialog({
|
|||||||
try {
|
try {
|
||||||
const ok = await onRenamePreset(oldItem.name, nextName, oldItem);
|
const ok = await onRenamePreset(oldItem.name, nextName, oldItem);
|
||||||
if (ok) {
|
if (ok) {
|
||||||
|
if (reorderEnabled) {
|
||||||
|
setDraftItems(prev =>
|
||||||
|
prev.map(it => (it.name === oldItem.name ? { ...it, name: nextName } : it))
|
||||||
|
);
|
||||||
|
}
|
||||||
setEditingIdx(null);
|
setEditingIdx(null);
|
||||||
setEditingName("");
|
setEditingName("");
|
||||||
}
|
}
|
||||||
@@ -175,7 +256,7 @@ export function PeqPresetManageDialog({
|
|||||||
|
|
||||||
const requestDelete = (idxs: number[]) => {
|
const requestDelete = (idxs: number[]) => {
|
||||||
const uniq = Array.from(new Set(idxs)).filter(
|
const uniq = Array.from(new Set(idxs)).filter(
|
||||||
i => i >= 0 && i < items.length
|
i => i >= 0 && i < displayItems.length
|
||||||
);
|
);
|
||||||
if (uniq.length === 0) return;
|
if (uniq.length === 0) return;
|
||||||
setDeleteConfirmIdxs(uniq);
|
setDeleteConfirmIdxs(uniq);
|
||||||
@@ -186,13 +267,27 @@ export function PeqPresetManageDialog({
|
|||||||
if (!idxs) return;
|
if (!idxs) return;
|
||||||
setDeleteConfirmIdxs(null);
|
setDeleteConfirmIdxs(null);
|
||||||
|
|
||||||
const names = idxs.map(i => items[i].name);
|
const names = idxs.map(i => displayItems[i].name);
|
||||||
|
|
||||||
setDeleting(true);
|
setDeleting(true);
|
||||||
try {
|
try {
|
||||||
const ok = await onDeletePresets(names);
|
const ok = await onDeletePresets(names);
|
||||||
if (ok) {
|
if (ok) {
|
||||||
setChecked({});
|
setChecked({});
|
||||||
|
if (reorderEnabled) {
|
||||||
|
setDraftItems(prev => {
|
||||||
|
const next = prev.filter(it => !names.includes(it.name));
|
||||||
|
setDraftSelectedIdx(sel => {
|
||||||
|
let removedBefore = 0;
|
||||||
|
for (const i of idxs) {
|
||||||
|
if (i < sel) removedBefore++;
|
||||||
|
}
|
||||||
|
const adjusted = sel - removedBefore;
|
||||||
|
return Math.max(0, Math.min(adjusted, next.length - 1));
|
||||||
|
});
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
if (editingIdx !== null && idxs.includes(editingIdx)) {
|
if (editingIdx !== null && idxs.includes(editingIdx)) {
|
||||||
setEditingIdx(null);
|
setEditingIdx(null);
|
||||||
setEditingName("");
|
setEditingName("");
|
||||||
@@ -203,22 +298,63 @@ export function PeqPresetManageDialog({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const executeConfirmOrder = async () => {
|
||||||
|
if (!orderChanged || confirmingOrder) return;
|
||||||
|
const names = draftItems.map(it => it.name);
|
||||||
|
setConfirmingOrder(true);
|
||||||
|
try {
|
||||||
|
const ok = await onConfirmOrder(names, draftSelectedIdx);
|
||||||
|
if (ok) {
|
||||||
|
setChecked({});
|
||||||
|
setEditingIdx(null);
|
||||||
|
setEditingName("");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setConfirmingOrder(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const deleteConfirmDescription =
|
const deleteConfirmDescription =
|
||||||
deleteConfirmIdxs === null
|
deleteConfirmIdxs === null
|
||||||
? ""
|
? ""
|
||||||
: deleteConfirmIdxs.length === 1
|
: deleteConfirmIdxs.length === 1
|
||||||
? eqInterp(eqUi.deletePresetConfirm, {
|
? eqInterp(eqUi.deletePresetConfirm, {
|
||||||
name: items[deleteConfirmIdxs[0]]?.name ?? "",
|
name: displayItems[deleteConfirmIdxs[0]]?.name ?? "",
|
||||||
})
|
})
|
||||||
: eqInterp(eqUi.batchDeleteConfirm, {
|
: eqInterp(eqUi.batchDeleteConfirm, {
|
||||||
count: deleteConfirmIdxs.length,
|
count: deleteConfirmIdxs.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const orderBusy = confirmingOrder || renaming || deleting;
|
||||||
|
const canMoveToFirst =
|
||||||
|
reorderEnabled &&
|
||||||
|
singleCheckedIdx !== null &&
|
||||||
|
singleCheckedIdx > 0 &&
|
||||||
|
!orderBusy;
|
||||||
|
const canMoveToLast =
|
||||||
|
reorderEnabled &&
|
||||||
|
singleCheckedIdx !== null &&
|
||||||
|
singleCheckedIdx < displayItems.length - 1 &&
|
||||||
|
!orderBusy;
|
||||||
|
|
||||||
|
const edgeMoveButtonClass = (enabled: boolean) =>
|
||||||
|
cn(
|
||||||
|
"w-8 h-8 rounded-[8px] flex items-center justify-center active:scale-95 transition-transform",
|
||||||
|
enabled
|
||||||
|
? "text-white/75 hover:bg-white/[0.08]"
|
||||||
|
: "opacity-30 cursor-not-allowed",
|
||||||
|
);
|
||||||
|
|
||||||
|
const edgeMoveButtonStyle = {
|
||||||
|
background: "rgba(255,255,255,0.06)",
|
||||||
|
border: "1px solid rgba(255,255,255,0.1)",
|
||||||
|
} as const;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="fixed inset-0 z-[140] flex items-center justify-center bg-black/65 px-4">
|
<div className="fixed inset-0 z-[140] flex items-center justify-center bg-black/65 px-4">
|
||||||
<div
|
<div
|
||||||
className="w-full max-w-[520px] rounded-[14px] p-5"
|
className="w-full max-w-[600px] rounded-[14px] p-5"
|
||||||
style={{
|
style={{
|
||||||
background:
|
background:
|
||||||
"linear-gradient(180deg, rgba(34,36,40,0.98) 0%, rgba(24,26,30,0.98) 100%)",
|
"linear-gradient(180deg, rgba(34,36,40,0.98) 0%, rgba(24,26,30,0.98) 100%)",
|
||||||
@@ -247,20 +383,78 @@ export function PeqPresetManageDialog({
|
|||||||
{eqUi.selectAll}
|
{eqUi.selectAll}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<button
|
<div className="flex items-center gap-2">
|
||||||
type="button"
|
{reorderEnabled && (
|
||||||
disabled={checkedIdxs.length === 0 || deleting}
|
<>
|
||||||
className={cn(
|
<Tooltip>
|
||||||
"rounded-full px-3 py-1.5 text-[13px] font-semibold transition-all active:scale-[0.98] flex items-center gap-1.5",
|
<TooltipTrigger asChild>
|
||||||
checkedIdxs.length === 0 || deleting
|
<button
|
||||||
? "bg-white/10 text-white/30 cursor-not-allowed"
|
type="button"
|
||||||
: "bg-red-500/15 text-red-300 hover:bg-red-500/20"
|
aria-disabled={!canMoveToFirst}
|
||||||
|
className={edgeMoveButtonClass(canMoveToFirst)}
|
||||||
|
style={edgeMoveButtonStyle}
|
||||||
|
onClick={() => {
|
||||||
|
if (!canMoveToFirst || singleCheckedIdx === null) return;
|
||||||
|
movePresetToEdge(singleCheckedIdx, "first");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ArrowBigUpDash size={14} />
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent sideOffset={6} className="z-[200]">
|
||||||
|
{eqUi.moveToFirst}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-disabled={!canMoveToLast}
|
||||||
|
className={edgeMoveButtonClass(canMoveToLast)}
|
||||||
|
style={edgeMoveButtonStyle}
|
||||||
|
onClick={() => {
|
||||||
|
if (!canMoveToLast || singleCheckedIdx === null) return;
|
||||||
|
movePresetToEdge(singleCheckedIdx, "last");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ArrowBigDownDash size={14} />
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent sideOffset={6} className="z-[200]">
|
||||||
|
{eqUi.moveToLast}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={!orderChanged || orderBusy}
|
||||||
|
className={cn(
|
||||||
|
"rounded-full px-3 py-1.5 text-[13px] font-semibold transition-all active:scale-[0.98] flex items-center gap-1.5",
|
||||||
|
!orderChanged || orderBusy
|
||||||
|
? "bg-[#00FFF6]/15 text-[#00FFF6]/35 cursor-not-allowed"
|
||||||
|
: "bg-[#00FFF6]/15 text-[#00FFF6] hover:bg-[#00FFF6]/25"
|
||||||
|
)}
|
||||||
|
onClick={() => void executeConfirmOrder()}
|
||||||
|
>
|
||||||
|
{confirmingOrder && <Loader2 size={13} className="animate-spin" />}
|
||||||
|
{eqUi.confirmOrder}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
onClick={() => requestDelete(checkedIdxs)}
|
<button
|
||||||
>
|
type="button"
|
||||||
{deleting && <Loader2 size={13} className="animate-spin" />}
|
disabled={checkedIdxs.length === 0 || deleting}
|
||||||
{eqUi.batchDelete}
|
className={cn(
|
||||||
</button>
|
"rounded-full px-3 py-1.5 text-[13px] font-semibold transition-all active:scale-[0.98] flex items-center gap-1.5",
|
||||||
|
checkedIdxs.length === 0 || deleting
|
||||||
|
? "bg-white/10 text-white/30 cursor-not-allowed"
|
||||||
|
: "bg-red-500/15 text-red-300 hover:bg-red-500/20"
|
||||||
|
)}
|
||||||
|
onClick={() => requestDelete(checkedIdxs)}
|
||||||
|
>
|
||||||
|
{deleting && <Loader2 size={13} className="animate-spin" />}
|
||||||
|
{eqUi.batchDelete}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -271,21 +465,18 @@ export function PeqPresetManageDialog({
|
|||||||
boxShadow: "0 8px 20px rgba(0,0,0,0.35)",
|
boxShadow: "0 8px 20px rgba(0,0,0,0.35)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{items.length === 0 ? (
|
{displayItems.length === 0 ? (
|
||||||
<div className="py-10 text-center text-[13px] text-white/30">
|
<div className="py-10 text-center text-[13px] text-white/30">
|
||||||
{eqUi.noPresets}
|
{eqUi.noPresets}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
items.map((item, idx) => {
|
displayItems.map((item, idx) => {
|
||||||
const active = idx === selectedIdx;
|
|
||||||
const isEditing = idx === editingIdx;
|
const isEditing = idx === editingIdx;
|
||||||
|
const isSelected = idx === displaySelectedIdx;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={`${item.name}-${idx}`}
|
key={`${item.name}-${idx}`}
|
||||||
className={cn(
|
className="flex items-center gap-2 pl-3 pr-2 py-2.5 border-b border-white/[0.06] last:border-b-0"
|
||||||
"flex items-center gap-2 px-3 py-2.5 border-b border-white/[0.06] last:border-b-0",
|
|
||||||
active ? "bg-white/[0.06]" : ""
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={!!checked[idx]}
|
checked={!!checked[idx]}
|
||||||
@@ -335,7 +526,10 @@ export function PeqPresetManageDialog({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className="truncate text-[13px] text-white/85"
|
className={cn(
|
||||||
|
"truncate text-[13px]",
|
||||||
|
isSelected ? "text-[#00FFF6]" : "text-white/85",
|
||||||
|
)}
|
||||||
title={item.name}
|
title={item.name}
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
@@ -344,7 +538,49 @@ export function PeqPresetManageDialog({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isEditing && (
|
{!isEditing && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="ml-2 flex shrink-0 items-center gap-1.5">
|
||||||
|
{reorderEnabled && displayItems.length > 1 && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={idx === 0 || orderBusy}
|
||||||
|
className={cn(
|
||||||
|
"w-8 h-8 rounded-[8px] flex items-center justify-center active:scale-95 transition-transform",
|
||||||
|
idx === 0 || orderBusy
|
||||||
|
? "opacity-30 cursor-not-allowed"
|
||||||
|
: "text-white/75 hover:bg-white/[0.08]",
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
background: "rgba(255,255,255,0.06)",
|
||||||
|
border: "1px solid rgba(255,255,255,0.1)",
|
||||||
|
}}
|
||||||
|
title={eqUi.movePresetUp}
|
||||||
|
aria-label={eqUi.movePresetUp}
|
||||||
|
onClick={() => movePreset(idx, "up")}
|
||||||
|
>
|
||||||
|
<ArrowBigUp size={14} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={idx === displayItems.length - 1 || orderBusy}
|
||||||
|
className={cn(
|
||||||
|
"w-8 h-8 rounded-[8px] flex items-center justify-center active:scale-95 transition-transform",
|
||||||
|
idx === displayItems.length - 1 || orderBusy
|
||||||
|
? "opacity-30 cursor-not-allowed"
|
||||||
|
: "text-white/75 hover:bg-white/[0.08]",
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
background: "rgba(255,255,255,0.06)",
|
||||||
|
border: "1px solid rgba(255,255,255,0.1)",
|
||||||
|
}}
|
||||||
|
title={eqUi.movePresetDown}
|
||||||
|
aria-label={eqUi.movePresetDown}
|
||||||
|
onClick={() => movePreset(idx, "down")}
|
||||||
|
>
|
||||||
|
<ArrowBigDown size={14} />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="w-8 h-8 rounded-[8px] flex items-center justify-center text-white/70 active:scale-95 transition-transform"
|
className="w-8 h-8 rounded-[8px] flex items-center justify-center text-white/70 active:scale-95 transition-transform"
|
||||||
|
|||||||
Reference in New Issue
Block a user