diff --git a/client/public/images/player/music.png b/client/public/images/player/music.png deleted file mode 100644 index f88fe9e..0000000 Binary files a/client/public/images/player/music.png and /dev/null differ diff --git a/client/public/images/player/pause.png b/client/public/images/player/pause.png deleted file mode 100644 index 3e180f6..0000000 Binary files a/client/public/images/player/pause.png and /dev/null differ diff --git a/client/public/images/player/play.png b/client/public/images/player/play.png deleted file mode 100644 index a96b455..0000000 Binary files a/client/public/images/player/play.png and /dev/null differ diff --git a/client/public/images/player/skip-back.png b/client/public/images/player/skip-back.png deleted file mode 100644 index f2da8d8..0000000 Binary files a/client/public/images/player/skip-back.png and /dev/null differ diff --git a/client/public/images/player/skip-forward.png b/client/public/images/player/skip-forward.png deleted file mode 100644 index 823bcbb..0000000 Binary files a/client/public/images/player/skip-forward.png and /dev/null differ diff --git a/client/src/pages/EQPage.tsx b/client/src/pages/EQPage.tsx index 1924d3d..f0b6302 100644 --- a/client/src/pages/EQPage.tsx +++ b/client/src/pages/EQPage.tsx @@ -14,11 +14,12 @@ import { useDevice } from "@/contexts/DeviceContext"; import { ChevronLeft, ChevronDown, - Minus, - Plus, Edit3, Headphones, + Trash2, + Copy, Share2, + FilePenLine, } from "lucide-react"; import { useLocation } from "wouter"; import { useState, useMemo, useEffect, useRef, useCallback } from "react"; @@ -179,6 +180,10 @@ export default function EQPage() { onConfirm: () => void | Promise; onDismiss: () => void; } | null>(null); + const [deleteHeadphoneDialog, setDeleteHeadphoneDialog] = useState<{ + name: string; + idx: number; + } | null>(null); const { currentRawCurve, setCurrentRawCurve, @@ -549,22 +554,24 @@ export default function EQPage() { setSelectedBandByMode({ A: 0, B: 0 }); }; - const handleDeleteHeadphone = async () => { + const handleDeleteHeadphone = () => { const target = peqItems[headphoneIdx]; if (!target?.name) return; + setDeleteHeadphoneDialog({ name: target.name, idx: headphoneIdx }); + }; - const confirmed = window.confirm( - eqInterp(eqUi.deleteConfirm, { name: target.name }) - ); - if (!confirmed) return; + const confirmDeleteHeadphone = async () => { + const target = deleteHeadphoneDialog; + if (!target) return; + setDeleteHeadphoneDialog(null); try { if (isDemoMode || !api) { - const nextItems = peqItems.filter((_, idx) => idx !== headphoneIdx); + const nextItems = peqItems.filter((_, idx) => idx !== target.idx); forgetPeqPresetCache(target.name); applyPeqStateToUI({ peq: nextItems, - peqSelect: Math.max(0, headphoneIdx - 1), + peqSelect: Math.max(0, target.idx - 1), }); toast.success(eqUi.toastDeleted); return; @@ -1641,7 +1648,7 @@ export default function EQPage() { }} onClick={openPresetManageDialog} > - + @@ -2179,6 +2186,23 @@ export default function EQPage() { }} /> + { + void confirmDeleteHeadphone(); + }} + onCancel={() => setDeleteHeadphoneDialog(null)} + /> + updateSetting({ bt_next: 0 })} className="w-10 h-10 rounded-full flex items-center justify-center transition-all active:scale-95" style={{ background: "rgba(0,255,246,0.15)", border: "1px solid rgba(0,255,246,0.3)" }}> - 上一首 + @@ -933,36 +931,36 @@ export default function Home() {
} + icon={} label={homeText.input ?? "输入源"} value={inputLabel} onClick={() => setLocation("/io")} /> } + icon={} label={homeText.output ?? "输出端口"} value={outputLabel} onClick={() => setLocation("/io")} /> } + icon={} label={homeText.audioSet ?? "音频设置"} onClick={() => setLocation("/audio")} /> } + icon={} label={homeText.systemSet ?? "系统设置"} onClick={() => setLocation("/system")} /> } + icon={} label={homeText.vu ?? "VU表"} value={vuLabel} thumbnail={`${import.meta.env.BASE_URL}images/vu/vu${(ds.vu ?? 0) + 1}.png`} onClick={() => setLocation("/vu")} /> } + icon={} label={homeText.backToLegacy ?? "返回旧版"} onClick={() => { const target = ip.trim(); diff --git a/client/src/pages/eq/components/PeqOverwriteConfirmDialog.tsx b/client/src/pages/eq/components/PeqOverwriteConfirmDialog.tsx index 5a74c3e..d37bcd1 100644 --- a/client/src/pages/eq/components/PeqOverwriteConfirmDialog.tsx +++ b/client/src/pages/eq/components/PeqOverwriteConfirmDialog.tsx @@ -1,27 +1,24 @@ import { useRef } from "react"; -import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogOverlay, - AlertDialogPortal, - AlertDialogTitle, -} from "@/components/ui/alert-dialog"; import { cn } from "@/lib/utils"; /** Above BrandDrawer (z-110) and ShareDialog (z-120). */ const EQ_OVERWRITE_DIALOG_Z = "z-[130]"; +const PANEL_STYLE = { + background: + "linear-gradient(180deg, rgba(34,36,40,0.98) 0%, rgba(24,26,30,0.98) 100%)", + border: "1px solid rgba(255,255,255,0.12)", + boxShadow: "0 18px 48px rgba(0,0,0,0.55)", +} as const; + export function PeqOverwriteConfirmDialog({ open, title, description, cancelLabel, confirmLabel, + variant = "default", + zClassName = EQ_OVERWRITE_DIALOG_Z, onConfirm, onCancel, }: { @@ -30,56 +27,76 @@ export function PeqOverwriteConfirmDialog({ description: string; cancelLabel: string; confirmLabel: string; + variant?: "default" | "destructive"; + /** Override stacking order when nested inside another dialog. */ + zClassName?: string; onConfirm: () => void; onCancel: () => void; }) { const actionTakenRef = useRef(false); + if (!open) return null; + + const handleDismiss = () => { + if (!actionTakenRef.current) onCancel(); + actionTakenRef.current = false; + }; + return ( - { - if (!next) { - if (!actionTakenRef.current) onCancel(); - actionTakenRef.current = false; - } - }} +
- - - e.stopPropagation()} + role="alertdialog" + aria-modal="true" + aria-labelledby="peq-confirm-title" + aria-describedby="peq-confirm-desc" + > +

- - {title} - - {description} - - - - e.preventDefault()} - onClick={onCancel} - > - {cancelLabel} - - e.preventDefault()} - onClick={() => { - actionTakenRef.current = true; - onConfirm(); - }} - > - {confirmLabel} - - - - - + {title} +

+

+ {description} +

+ +
+ + +
+
+
); } diff --git a/client/src/pages/eq/components/PeqPresetManageDialog.tsx b/client/src/pages/eq/components/PeqPresetManageDialog.tsx index 61cf76a..c43dbab 100644 --- a/client/src/pages/eq/components/PeqPresetManageDialog.tsx +++ b/client/src/pages/eq/components/PeqPresetManageDialog.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { X, Trash2, Pencil, Check, Loader2 } from "lucide-react"; import { cn } from "@/lib/utils"; import { toast } from "sonner"; @@ -6,6 +6,8 @@ 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({ @@ -96,6 +98,9 @@ export function PeqPresetManageDialog({ const [deleting, setDeleting] = useState(false); const [renaming, setRenaming] = useState(false); + const [deleteConfirmIdxs, setDeleteConfirmIdxs] = useState( + null, + ); const checkedIdxs = useMemo( () => @@ -107,6 +112,10 @@ export function PeqPresetManageDialog({ const allChecked = items.length > 0 && checkedIdxs.length === items.length; + useEffect(() => { + if (!open) setDeleteConfirmIdxs(null); + }, [open]); + if (!open) return null; const toggleAll = () => { @@ -164,27 +173,27 @@ export function PeqPresetManageDialog({ } }; - const confirmAndDelete = async (idxs: number[]) => { + const requestDelete = (idxs: number[]) => { const uniq = Array.from(new Set(idxs)).filter( i => i >= 0 && i < items.length ); if (uniq.length === 0) return; + setDeleteConfirmIdxs(uniq); + }; - const names = uniq.map(i => items[i].name); + const executeDelete = async () => { + const idxs = deleteConfirmIdxs; + if (!idxs) return; + setDeleteConfirmIdxs(null); - // Confirmation dialog - const confirmMsg = - names.length === 1 - ? eqUi.deletePresetConfirm.replace("{{name}}", names[0]) - : eqUi.batchDeleteConfirm.replace("{{count}}", String(names.length)); - if (!window.confirm(confirmMsg)) return; + const names = idxs.map(i => items[i].name); setDeleting(true); try { const ok = await onDeletePresets(names); if (ok) { setChecked({}); - if (editingIdx !== null && uniq.includes(editingIdx)) { + if (editingIdx !== null && idxs.includes(editingIdx)) { setEditingIdx(null); setEditingName(""); } @@ -194,7 +203,19 @@ export function PeqPresetManageDialog({ } }; + const deleteConfirmDescription = + deleteConfirmIdxs === null + ? "" + : deleteConfirmIdxs.length === 1 + ? eqInterp(eqUi.deletePresetConfirm, { + name: items[deleteConfirmIdxs[0]]?.name ?? "", + }) + : eqInterp(eqUi.batchDeleteConfirm, { + count: deleteConfirmIdxs.length, + }); + return ( + <>
void confirmAndDelete(checkedIdxs)} + onClick={() => requestDelete(checkedIdxs)} > {deleting && } {eqUi.batchDelete} @@ -344,7 +365,7 @@ export function PeqPresetManageDialog({ border: "1px solid rgba(239,68,68,0.2)", }} title={eqUi.deletePreset} - onClick={() => void confirmAndDelete([idx])} + onClick={() => requestDelete([idx])} > @@ -357,5 +378,20 @@ export function PeqPresetManageDialog({
+ + { + void executeDelete(); + }} + onCancel={() => setDeleteConfirmIdxs(null)} + /> + ); } diff --git a/package.json b/package.json index 3e6f9b1..967794b 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "embla-carousel-react": "^8.6.0", "framer-motion": "^12.23.22", "input-otp": "^1.4.2", - "lucide-react": "^0.453.0", + "lucide-react": "^0.575.0", "nanoid": "^5.1.5", "next-themes": "^0.4.6", "react": "^19.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 154c87a..2350741 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -117,8 +117,8 @@ importers: specifier: ^1.4.2 version: 1.4.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) lucide-react: - specifier: ^0.453.0 - version: 0.453.0(react@19.2.7) + specifier: ^0.575.0 + version: 0.575.0(react@19.2.7) nanoid: specifier: ^5.1.5 version: 5.1.11 @@ -3028,16 +3028,16 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - lucide-react@0.453.0: - resolution: {integrity: sha512-kL+RGZCcJi9BvJtzg2kshO192Ddy9hv3ij+cPrVPWSRzgCWCVazoQJxOjAwgK53NomL07HB7GPHW120FimjNhQ==} - peerDependencies: - react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc - lucide-react@0.542.0: resolution: {integrity: sha512-w3hD8/SQB7+lzU2r4VdFyzzOzKnUjTZIF/MQJGSSvni7Llewni4vuViRppfRAa2guOsY5k4jZyxw/i9DQHv+dw==} peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + lucide-react@0.575.0: + resolution: {integrity: sha512-VuXgKZrk0uiDlWjGGXmKV6MSk9Yy4l10qgVvzGn2AWBx1Ylt0iBexKOAoA6I7JO3m+M9oeovJd3yYENfkUbOeg==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + magic-string@0.30.19: resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} @@ -6657,11 +6657,11 @@ snapshots: dependencies: yallist: 3.1.1 - lucide-react@0.453.0(react@19.2.7): + lucide-react@0.542.0(react@19.2.7): dependencies: react: 19.2.7 - lucide-react@0.542.0(react@19.2.7): + lucide-react@0.575.0(react@19.2.7): dependencies: react: 19.2.7