version>=2008 使用了新的重命名接口、EQ 排序功能
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { X, Trash2, Pencil, Check, Loader2 } from "lucide-react";
|
||||
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";
|
||||
@@ -61,21 +62,26 @@ type PeqItem = {
|
||||
* 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 which
|
||||
* should: 1) peqChange with new name + same EQ data 2) peqRemove old name.
|
||||
* - 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;
|
||||
/**
|
||||
@@ -84,24 +90,36 @@ export function PeqPresetManageDialog({
|
||||
*/
|
||||
onDeletePresets: (names: string[]) => Promise<boolean>;
|
||||
/**
|
||||
* 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<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 [editingIdx, setEditingIdx] = useState<number | null>(null);
|
||||
const [editingName, setEditingName] = useState("");
|
||||
const [draftItems, setDraftItems] = useState<PeqItem[]>(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<number[] | null>(
|
||||
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<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);
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<div className="fixed inset-0 z-[140] flex items-center justify-center bg-black/65 px-4">
|
||||
<div
|
||||
className="w-full max-w-[520px] rounded-[14px] p-5"
|
||||
className="w-full max-w-[600px] rounded-[14px] p-5"
|
||||
style={{
|
||||
background:
|
||||
"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}
|
||||
</label>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
disabled={checkedIdxs.length === 0 || deleting}
|
||||
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",
|
||||
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"
|
||||
<div className="flex items-center gap-2">
|
||||
{reorderEnabled && (
|
||||
<>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
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)}
|
||||
>
|
||||
{deleting && <Loader2 size={13} className="animate-spin" />}
|
||||
{eqUi.batchDelete}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={checkedIdxs.length === 0 || deleting}
|
||||
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",
|
||||
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
|
||||
@@ -271,21 +465,18 @@ export function PeqPresetManageDialog({
|
||||
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">
|
||||
{eqUi.noPresets}
|
||||
</div>
|
||||
) : (
|
||||
items.map((item, idx) => {
|
||||
const active = idx === selectedIdx;
|
||||
displayItems.map((item, idx) => {
|
||||
const isEditing = idx === editingIdx;
|
||||
const isSelected = idx === displaySelectedIdx;
|
||||
return (
|
||||
<div
|
||||
key={`${item.name}-${idx}`}
|
||||
className={cn(
|
||||
"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]" : ""
|
||||
)}
|
||||
className="flex items-center gap-2 pl-3 pr-2 py-2.5 border-b border-white/[0.06] last:border-b-0"
|
||||
>
|
||||
<Checkbox
|
||||
checked={!!checked[idx]}
|
||||
@@ -335,7 +526,10 @@ export function PeqPresetManageDialog({
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="truncate text-[13px] text-white/85"
|
||||
className={cn(
|
||||
"truncate text-[13px]",
|
||||
isSelected ? "text-[#00FFF6]" : "text-white/85",
|
||||
)}
|
||||
title={item.name}
|
||||
>
|
||||
{item.name}
|
||||
@@ -344,7 +538,49 @@ export function PeqPresetManageDialog({
|
||||
</div>
|
||||
|
||||
{!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
|
||||
type="button"
|
||||
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