import { useEffect, useMemo, useRef, useState } from "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 { toast } from "sonner"; import type { PeqEqUi } from "../constants"; import { PEQ_PRESET_NAME_MAX_LENGTH } from "../constants"; import { clampPeqPresetName, isPeqPresetNameTooLong } from "../peqPresetName"; import { PeqPresetNameLengthHint } from "./PeqPresetNameLengthHint"; import { PeqOverwriteConfirmDialog } from "./PeqOverwriteConfirmDialog"; import { eqInterp } from "../utils"; /* ── Custom checkbox ── */ function Checkbox({ checked, onChange, className, }: { checked: boolean; onChange: () => void; className?: string; }) { return ( ); } type PeqItem = { name: string; filters?: any[] | string; autoPre?: number; preamp?: number; canDel?: number; brand?: string; model?: string; target?: string; form?: string; }; /** * Preset management dialog. * * Behavior: * - Delete (single / batch): calls onDeletePresets callback with preset names, * which should call info.cgi with { peqRemove: names } then sync. * - Rename: calls onRenamePreset(oldName, newName, item) callback. * Firmware >= 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; /** * Called with preset names to delete. Should call api.removePeq(names) then * fetchEqSyncPeq + applyPeqStateToUI. Return true on success, false on failure. */ onDeletePresets: (names: string[]) => Promise; /** * 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) .filter(([, v]) => v) .map(([k]) => Number(k)), [checked] ); 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) { 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; const toggleAll = () => { if (allChecked) { setChecked({}); return; } const next: Record = {}; for (let i = 0; i < displayItems.length; i++) next[i] = true; setChecked(next); }; const toggleOne = (idx: number) => { setChecked(prev => ({ ...prev, [idx]: !prev[idx] })); }; const beginRename = (idx: number) => { 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(); if (!nextName) { toast.error(eqUi.renameEmptyName); return; } if (isPeqPresetNameTooLong(nextName)) { toast.error(eqUi.addPresetNameTooLong); return; } const oldItem = displayItems[editingIdx]; if (!oldItem) return; if (nextName === oldItem.name) { setEditingIdx(null); setEditingName(""); return; } if (displayItems.some((it, i) => i !== editingIdx && it.name === nextName)) { toast.error(eqUi.renameNameExists); return; } setRenaming(true); 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(""); } } finally { setRenaming(false); } }; const requestDelete = (idxs: number[]) => { const uniq = Array.from(new Set(idxs)).filter( i => i >= 0 && i < displayItems.length ); if (uniq.length === 0) return; setDeleteConfirmIdxs(uniq); }; const executeDelete = async () => { const idxs = deleteConfirmIdxs; if (!idxs) return; setDeleteConfirmIdxs(null); 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(""); } } } finally { setDeleting(false); } }; 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: 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 ( <>
e.stopPropagation()} >

{eqUi.managePresetTitle}

{reorderEnabled && ( <> {eqUi.moveToFirst} {eqUi.moveToLast} )}
{displayItems.length === 0 ? (
{eqUi.noPresets}
) : ( displayItems.map((item, idx) => { const isEditing = idx === editingIdx; const isSelected = idx === displaySelectedIdx; return (
toggleOne(idx)} />
{isEditing ? (
setEditingName(clampPeqPresetName(e.target.value))} maxLength={PEQ_PRESET_NAME_MAX_LENGTH} className="h-9 min-w-0 flex-1 rounded-[10px] border border-white/12 bg-[#15171b] px-3 text-[14px] text-white/90 outline-none placeholder:text-white/30 focus:border-[#00FFF6]/40" />
) : (
{item.name}
)}
{!isEditing && (
{reorderEnabled && displayItems.length > 1 && ( <> )}
)}
); }) )}
{ void executeDelete(); }} onCancel={() => setDeleteConfirmIdxs(null)} /> ); }