From bfc4b45ca3f9ec86ea437407ff34e1742bf53945 Mon Sep 17 00:00:00 2001 From: eafonyang Date: Tue, 7 Jul 2026 17:15:01 +0800 Subject: [PATCH] =?UTF-8?q?version>=3D2008=20=E4=BD=BF=E7=94=A8=E4=BA=86?= =?UTF-8?q?=E6=96=B0=E7=9A=84=E9=87=8D=E5=91=BD=E5=90=8D=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E3=80=81EQ=20=E6=8E=92=E5=BA=8F=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/config/firmwareFeatures.ts | 6 + client/src/lib/luxsinApi.ts | 46 +++ client/src/locales/data-de.json | 7 + client/src/locales/data-en.json | 7 + client/src/locales/data-es.json | 7 + client/src/locales/data-fr.json | 7 + client/src/locales/data-zh-HK.json | 7 + client/src/locales/data-zh.json | 7 + client/src/pages/EQPage.tsx | 87 +++-- .../eq/components/PeqPresetManageDialog.tsx | 318 +++++++++++++++--- 10 files changed, 432 insertions(+), 67 deletions(-) diff --git a/client/src/config/firmwareFeatures.ts b/client/src/config/firmwareFeatures.ts index 11a663b..8594bb3 100644 --- a/client/src/config/firmwareFeatures.ts +++ b/client/src/config/firmwareFeatures.ts @@ -11,6 +11,8 @@ export const FIRMWARE_FEATURE_MIN_VERSION = { subwooferFullRange: 2001, /** Hearing compensation section on the Effects page. */ hearingCompensation: 2007, + /** PEQ preset rename (peqRename) and reorder (peqSoft) in manage dialog. */ + peqPresetRenameAndSort: 2008, } as const; export type FirmwareFeatureKey = keyof typeof FIRMWARE_FEATURE_MIN_VERSION; @@ -26,3 +28,7 @@ export function isFirmwareFeatureAvailable( export function getBootSoundMaxIndex(firmwareVersion: number): number { return isFirmwareFeatureAvailable("bootSoundExtendedSteps", firmwareVersion) ? 10 : 6; } + +export function supportsPeqPresetRenameAndSort(firmwareVersion: number): boolean { + return isFirmwareFeatureAvailable("peqPresetRenameAndSort", firmwareVersion); +} diff --git a/client/src/lib/luxsinApi.ts b/client/src/lib/luxsinApi.ts index 185c384..7ae4ad5 100644 --- a/client/src/lib/luxsinApi.ts +++ b/client/src/lib/luxsinApi.ts @@ -240,6 +240,20 @@ export interface PeqApplyPayload { peqApply: PeqPresetBody; } +/** POST body for `/dev/info.cgi` — rename an existing PEQ preset. */ +export interface PeqRenamePayload { + peqRename: { + oldName: string; + newName: string; + }; +} + +/** POST body for `/dev/info.cgi` — reorder PEQ presets. */ +export interface PeqSoftPayload { + peqSoft: string[]; + peqSelect: number; +} + // ============================================================ // Input/Output Labels // ============================================================ @@ -383,6 +397,38 @@ export class LuxsinAPI { } } + /** Rename a headphone PEQ profile. */ + async renamePeq(oldName: string, newName: string): Promise { + const payload: PeqRenamePayload = { peqRename: { oldName, newName } }; + const encoded = encodeCustomBase64(JSON.stringify(payload)); + const form = new URLSearchParams(); + form.set("json", encoded); + const response = await fetch(`${this.baseUrl}/dev/info.cgi`, { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" }, + body: form.toString(), + }); + if (!response.ok) { + throw new Error(`PEQ rename failed: ${response.status}`); + } + } + + /** Reorder headphone PEQ profiles. */ + async sortPeq(names: string[], peqSelect: number): Promise { + const payload: PeqSoftPayload = { peqSoft: names, peqSelect }; + const encoded = encodeCustomBase64(JSON.stringify(payload)); + const form = new URLSearchParams(); + form.set("json", encoded); + const response = await fetch(`${this.baseUrl}/dev/info.cgi`, { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" }, + body: form.toString(), + }); + if (!response.ok) { + throw new Error(`PEQ sort failed: ${response.status}`); + } + } + setVolume(volume: number) { return this.setSetting({ volume }); } setInput(input: number) { return this.setSetting({ input }); } setOutput(output: number) { return this.setSetting({ output }); } diff --git a/client/src/locales/data-de.json b/client/src/locales/data-de.json index 9d7fec7..f5f3080 100644 --- a/client/src/locales/data-de.json +++ b/client/src/locales/data-de.json @@ -281,6 +281,13 @@ "batchDelete": "Auswahl löschen", "noPresets": "Keine Voreinstellungen", "renamePreset": "Umbenennen", + "movePresetUp": "Nach oben", + "movePresetDown": "Nach unten", + "confirmOrder": "Reihenfolge bestätigen", + "moveToFirst": "Ganz nach oben", + "moveToLast": "Ganz nach unten", + "toastOrderSaved": "Reihenfolge gespeichert", + "toastOrderFail": "Reihenfolge konnte nicht gespeichert werden", "deletePreset": "Löschen", "toastRenamed": "Umbenannt", "toastRenameFail": "Umbenennung fehlgeschlagen", diff --git a/client/src/locales/data-en.json b/client/src/locales/data-en.json index 5bdb221..d8bd479 100644 --- a/client/src/locales/data-en.json +++ b/client/src/locales/data-en.json @@ -281,6 +281,13 @@ "batchDelete": "Delete selected", "noPresets": "No presets", "renamePreset": "Rename", + "movePresetUp": "Move up", + "movePresetDown": "Move down", + "confirmOrder": "Apply Order", + "moveToFirst": "Move Up To First", + "moveToLast": "Move Down To Last", + "toastOrderSaved": "Order saved", + "toastOrderFail": "Failed to save order", "deletePreset": "Delete", "toastRenamed": "Renamed", "toastRenameFail": "Rename failed", diff --git a/client/src/locales/data-es.json b/client/src/locales/data-es.json index ac0e1d8..693eb18 100644 --- a/client/src/locales/data-es.json +++ b/client/src/locales/data-es.json @@ -281,6 +281,13 @@ "batchDelete": "Eliminar selección", "noPresets": "Sin presets", "renamePreset": "Renombrar", + "movePresetUp": "Subir", + "movePresetDown": "Bajar", + "confirmOrder": "Confirmar orden", + "moveToFirst": "Mover al inicio", + "moveToLast": "Mover al final", + "toastOrderSaved": "Orden guardado", + "toastOrderFail": "No se pudo guardar el orden", "deletePreset": "Eliminar", "toastRenamed": "Renombrado", "toastRenameFail": "Error al renombrar", diff --git a/client/src/locales/data-fr.json b/client/src/locales/data-fr.json index 4ace26d..b1f3e70 100644 --- a/client/src/locales/data-fr.json +++ b/client/src/locales/data-fr.json @@ -281,6 +281,13 @@ "batchDelete": "Supprimer la sélection", "noPresets": "Aucun préréglage", "renamePreset": "Renommer", + "movePresetUp": "Monter", + "movePresetDown": "Descendre", + "confirmOrder": "Confirmer l'ordre", + "moveToFirst": "Déplacer en premier", + "moveToLast": "Déplacer en dernier", + "toastOrderSaved": "Ordre enregistré", + "toastOrderFail": "Échec de l'enregistrement de l'ordre", "deletePreset": "Supprimer", "toastRenamed": "Renommé", "toastRenameFail": "Échec du renommage", diff --git a/client/src/locales/data-zh-HK.json b/client/src/locales/data-zh-HK.json index 3607f50..fc48ae5 100644 --- a/client/src/locales/data-zh-HK.json +++ b/client/src/locales/data-zh-HK.json @@ -274,6 +274,13 @@ "batchDelete": "批量刪除", "noPresets": "暫無預設", "renamePreset": "重新命名", + "movePresetUp": "上移", + "movePresetDown": "下移", + "confirmOrder": "確認順序", + "moveToFirst": "置頂", + "moveToLast": "置底", + "toastOrderSaved": "順序已儲存", + "toastOrderFail": "儲存順序失敗", "deletePreset": "刪除", "toastRenamed": "已重新命名", "toastRenameFail": "重新命名失敗", diff --git a/client/src/locales/data-zh.json b/client/src/locales/data-zh.json index 9157482..b2690c0 100644 --- a/client/src/locales/data-zh.json +++ b/client/src/locales/data-zh.json @@ -274,6 +274,13 @@ "batchDelete": "批量删除", "noPresets": "暂无预设", "renamePreset": "重命名", + "movePresetUp": "上移", + "movePresetDown": "下移", + "confirmOrder": "确认顺序", + "moveToFirst": "置顶", + "moveToLast": "置底", + "toastOrderSaved": "顺序已保存", + "toastOrderFail": "保存顺序失败", "deletePreset": "删除", "toastRenamed": "已重命名", "toastRenameFail": "重命名失败", diff --git a/client/src/pages/EQPage.tsx b/client/src/pages/EQPage.tsx index 11f2890..3226bb9 100644 --- a/client/src/pages/EQPage.tsx +++ b/client/src/pages/EQPage.tsx @@ -30,12 +30,14 @@ import { toast } from "sonner"; import { fetchLuxsinAudioCurve, normalizePeqFiltersForSubmit, + parseFirmwareVersion, type PeqFilter, type PeqApplyPayload, type PeqChangePayload, type PeqState, buildPeqPresetBody, } from "@/lib/luxsinApi"; +import { supportsPeqPresetRenameAndSort } from "@/config/firmwareFeatures"; import { getFilterType, getFilterShortName } from "@/lib/peqAudio"; import localeZh from "@/locales/data-zh.json"; import localeEn from "@/locales/data-en.json"; @@ -113,6 +115,12 @@ export default function EQPage() { return (peq.eqUi ?? (localeEn.peq as typeof localeEn.peq).eqUi) as PeqEqUi; }, [deviceState?.language]); + const firmwareVersion = useMemo( + () => parseFirmwareVersion(deviceState?.version), + [deviceState?.version], + ); + const peqReorderEnabled = supportsPeqPresetRenameAndSort(firmwareVersion); + const peqCardLabels = useMemo(() => { const pack = resolveLocalePack(deviceState?.language); return pack.peq ?? localeEn.peq; @@ -520,19 +528,21 @@ export default function EQPage() { peqSelect?: number; filters?: PeqFilter[]; }) => { + const resolvedPeqSelect = + remote.peqSelect ?? deviceState?.peqSelect ?? peqState?.peqSelect ?? 0; + if (remote.peq !== undefined) { syncPeqCatalog({ filters: remote.filters ?? peqState?.filters ?? [], peq: remote.peq, - peqSelect: - remote.peqSelect ?? deviceState?.peqSelect ?? peqState?.peqSelect, + peqSelect: resolvedPeqSelect, }); } lastPeqCatalogSyncKeyRef.current = buildPeqCatalogSyncKey( remote, - deviceState?.peqSelect + resolvedPeqSelect ); - syncPeqCatalogFromState(remote, deviceState?.peqSelect); + syncPeqCatalogFromState(remote, resolvedPeqSelect); const items = remote.peq ?? []; if (items.length === 0) { @@ -542,7 +552,7 @@ export default function EQPage() { } const nextIdx = Math.min( - Math.max(deviceState?.peqSelect ?? remote.peqSelect ?? 0, 0), + Math.max(resolvedPeqSelect, 0), items.length - 1 ); const nextBands = normalizeFiltersFromPeq( @@ -901,10 +911,11 @@ export default function EQPage() { // msgCount 轮询更新 peqState 后,同步耳机列表与当前选中项 useEffect(() => { 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; lastPeqCatalogSyncKeyRef.current = key; - syncPeqCatalogFromState(peqState, deviceState?.peqSelect); + syncPeqCatalogFromState(peqState, resolvedPeqSelect); }, [peqState, deviceState?.peqSelect, syncPeqCatalogFromState]); // 切换耳机时默认回到 A 组对比 @@ -1968,6 +1979,7 @@ export default function EQPage() { open={isPresetManageDialogOpen} items={peqItems} selectedIdx={headphoneIdx} + reorderEnabled={peqReorderEnabled} onClose={() => setIsPresetManageDialogOpen(false)} eqUi={eqUi} onDeletePresets={async (names) => { @@ -1995,9 +2007,25 @@ export default function EQPage() { }} onRenamePreset={async (oldName, newName, item) => { try { - // normalizePeqFiltersForSubmit returns filters with string type values - // (e.g. "LOW_SHELF", "PEAKING") — but the device expects numeric type. - // Must convert with getFilterType() before submitting to info.cgi. + if (isDemoMode || !api) { + const nextItems = peqItems.map(it => + it.name === oldName ? { ...it, name: newName } : it + ); + forgetPeqPresetCache(oldName); + applyPeqStateToUI({ peq: nextItems, peqSelect: headphoneIdx }); + toast.success(eqUi.toastRenamed); + return true; + } + + if (supportsPeqPresetRenameAndSort(firmwareVersion)) { + await api.renamePeq(oldName, newName); + forgetPeqPresetCache(oldName); + const latest = await fetchEqSyncPeq(api, "renamePreset"); + applyPeqStateToUI(latest); + toast.success(eqUi.toastRenamed); + return true; + } + const rawFilters = normalizePeqFiltersForSubmit(item.filters); const filters = rawFilters.map(f => ({ ...f, @@ -2017,28 +2045,12 @@ export default function EQPage() { }, }; - if (isDemoMode || !api) { - const nextItems = peqItems.map(it => - it.name === oldName ? { ...it, name: newName } : it - ); - forgetPeqPresetCache(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 new Promise(r => setTimeout(r, 300)); - // Step 2: peqRemove — delete the old preset forgetPeqPresetCache(oldName); await api.removePeq([oldName]); await new Promise(r => setTimeout(r, 300)); - // Step 3: sync latest state from device const latest = await fetchEqSyncPeq(api, "renamePreset"); - applyPeqStateToUI(latest); toast.success(eqUi.toastRenamed); return true; @@ -2047,6 +2059,29 @@ export default function EQPage() { 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 => !!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; + } + }} /> = threshold: peqRename. Below threshold: peqChange then peqRemove. + * - Reorder (when reorderEnabled): up/down buttons + apply order → peqSoft. */ export function PeqPresetManageDialog({ open, items, selectedIdx, + reorderEnabled, onClose, eqUi, onDeletePresets, onRenamePreset, + onConfirmOrder, }: { open: boolean; items: PeqItem[]; selectedIdx: number; + /** When true: show reorder controls and apply-order button (firmware-gated in parent). */ + reorderEnabled: boolean; onClose: () => void; eqUi: PeqEqUi; /** @@ -84,24 +90,36 @@ export function PeqPresetManageDialog({ */ onDeletePresets: (names: string[]) => Promise; /** - * Called to rename a preset. Implementation should: - * 1) upgradePeqChange({ peqChange: { name: newName, filters, ...rest } }) - * 2) api.removePeq([oldName]) - * 3) fetchEqSyncPeq + applyPeqStateToUI + * Called to rename a preset. Implementation is version-dependent: + * - firmware >= 29: api.renamePeq(oldName, newName) then sync + * - firmware < 29: peqChange + peqRemove then sync * Return true on success, false on failure. */ onRenamePreset: (oldName: string, newName: string, item: PeqItem) => Promise; + /** + * 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; }) { const [checked, setChecked] = useState>({}); const [editingIdx, setEditingIdx] = useState(null); const [editingName, setEditingName] = useState(""); + const [draftItems, setDraftItems] = useState(items); + const [draftSelectedIdx, setDraftSelectedIdx] = useState(selectedIdx); const [deleting, setDeleting] = useState(false); const [renaming, setRenaming] = useState(false); + const [confirmingOrder, setConfirmingOrder] = useState(false); const [deleteConfirmIdxs, setDeleteConfirmIdxs] = useState( null, ); + const prevOpenRef = useRef(false); + + const displayItems = reorderEnabled ? draftItems : items; + const displaySelectedIdx = reorderEnabled ? draftSelectedIdx : selectedIdx; + const checkedIdxs = useMemo( () => Object.entries(checked) @@ -110,11 +128,31 @@ export function PeqPresetManageDialog({ [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(() => { - if (!open) setDeleteConfirmIdxs(null); - }, [open]); + if (!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; @@ -124,7 +162,7 @@ export function PeqPresetManageDialog({ return; } const next: Record = {}; - for (let i = 0; i < items.length; i++) next[i] = true; + for (let i = 0; i < displayItems.length; i++) next[i] = true; setChecked(next); }; @@ -133,11 +171,49 @@ export function PeqPresetManageDialog({ }; const beginRename = (idx: number) => { - const name = items[idx]?.name ?? ""; + const name = displayItems[idx]?.name ?? ""; setEditingIdx(idx); 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 () => { if (editingIdx === null) return; const nextName = editingName.trim(); @@ -149,14 +225,14 @@ export function PeqPresetManageDialog({ toast.error(eqUi.addPresetNameTooLong); return; } - const oldItem = items[editingIdx]; + const oldItem = displayItems[editingIdx]; if (!oldItem) return; if (nextName === oldItem.name) { setEditingIdx(null); setEditingName(""); 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); return; } @@ -165,6 +241,11 @@ export function PeqPresetManageDialog({ try { const ok = await onRenamePreset(oldItem.name, nextName, oldItem); if (ok) { + if (reorderEnabled) { + setDraftItems(prev => + prev.map(it => (it.name === oldItem.name ? { ...it, name: nextName } : it)) + ); + } setEditingIdx(null); setEditingName(""); } @@ -175,7 +256,7 @@ export function PeqPresetManageDialog({ const requestDelete = (idxs: number[]) => { 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; setDeleteConfirmIdxs(uniq); @@ -186,13 +267,27 @@ export function PeqPresetManageDialog({ if (!idxs) return; setDeleteConfirmIdxs(null); - const names = idxs.map(i => items[i].name); + const names = idxs.map(i => displayItems[i].name); setDeleting(true); try { const ok = await onDeletePresets(names); if (ok) { 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)) { setEditingIdx(null); 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 = deleteConfirmIdxs === null ? "" : deleteConfirmIdxs.length === 1 ? eqInterp(eqUi.deletePresetConfirm, { - name: items[deleteConfirmIdxs[0]]?.name ?? "", + name: displayItems[deleteConfirmIdxs[0]]?.name ?? "", }) : eqInterp(eqUi.batchDeleteConfirm, { 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 ( <>
- + + + {eqUi.moveToFirst} + + + + + + + + {eqUi.moveToLast} + + + + )} - onClick={() => requestDelete(checkedIdxs)} - > - {deleting && } - {eqUi.batchDelete} - + +
- {items.length === 0 ? ( + {displayItems.length === 0 ? (
{eqUi.noPresets}
) : ( - items.map((item, idx) => { - const active = idx === selectedIdx; + displayItems.map((item, idx) => { const isEditing = idx === editingIdx; + const isSelected = idx === displaySelectedIdx; return (
) : (
{item.name} @@ -344,7 +538,49 @@ export function PeqPresetManageDialog({
{!isEditing && ( -
+
+ {reorderEnabled && displayItems.length > 1 && ( + <> + + + + )}