@@ -56,13 +56,16 @@ import {
import {
eqInterp ,
formatBandFreqDisplay ,
formatBandGainDisplay ,
formatBandParamForInput ,
formatBandQDisplay ,
freqLabel ,
normalizeFilterType ,
normalizeBandFreqHz ,
parseBandParamInput ,
} from "./eq/eqFormatters" ;
import { bandToPeqFilter , buildPeqCatalogSyncKey , cloneBands , getUniquePresetName } from "./eq/peqMappers" ;
import type { BandParamKind , PeqCatalogItem , PeqEqUi } from "./eq/types" ;
import type { BandParamKind , PeqCatalogItem , PeqEqUi , PeqPresetLocalCache } from "./eq/types" ;
/* ── Main Component ── */
export default function EQPage() {
@@ -126,6 +129,12 @@ export default function EQPage() {
const [ saveBPresetName , setSaveBPresetName ] = useState ( "" ) ;
const [ bandParamDialog , setBandParamDialog ] = useState < BandParamKind | null > ( null ) ;
const [ bandParamInput , setBandParamInput ] = useState ( "" ) ;
/** 滑块拖动时的实时显示(避免按钮文字滞后于 bands 状态) */
const [ bandSliderPreview , setBandSliderPreview ] = useState < {
freq? : number ;
gain? : number ;
q? : number ;
} | null > ( null ) ;
const [ isBatchEditDialogOpen , setIsBatchEditDialogOpen ] = useState ( false ) ;
const [ batchEditText , setBatchEditText ] = useState ( "" ) ;
type BrandDrawerTab = "brands" | "models" | "target" ;
@@ -155,8 +164,8 @@ export default function EQPage() {
const skipPeqAutoSyncRef = useRef ( false ) ;
/** A/B 切换已单独下发,避免 bands 变更触发的 effect 用错 peqChange/peqApply */
const skipBandsSyncFromAbToggleRef = useRef ( false ) ;
/** 本地编辑过的预设 filters(按名称 );syncPeq 拉取不会覆盖 */
const peqFiltersCacheRef = useRef < Record < string , PeqFilter [ ] > > ( { } ) ;
/** 本地编辑过的预设( filters / preamp / autoPre );syncPeq 拉取不会覆盖 */
const peqPresetCacheRef = useRef < Record < string , PeqPresetLocalCache > > ( { } ) ;
const headphoneMenuRef = useRef < HTMLDivElement | null > ( null ) ;
const filterMenuRef = useRef < HTMLDivElement | null > ( null ) ;
const bands = bandsByMode [ abMode ] ;
@@ -325,9 +334,10 @@ export default function EQPage() {
filters = peq . filters ;
}
return filters . map ( ( f : any ) = > {
const freq = f . fc || f . freq || f . frequency || 1000 ;
const rawFreq = f . fc ? ? f . freq ? ? f . frequency ? ? 1000 ;
const freq = normalizeBandFreqHz ( rawFreq ) ? ? 1000 ;
return {
freq : Number ( freq ) ,
freq : Math.round ( freq ) ,
gain : Number ( f . gain || 0 ) ,
q : Number ( f . q || 1 ) ,
type : normalizeFilterType ( f . type ) ,
@@ -445,7 +455,7 @@ export default function EQPage() {
preamp : parsed.preamp ,
filters ,
} ;
rememberPeqFiltersForPreset ( item . name , filters ) ;
rememberPeqPresetCache ( item . name , { filters , preamp : parsed.preamp , autoPre : 0 } ) ;
updatedPeq = merged ;
return merged ;
} )
@@ -460,18 +470,27 @@ export default function EQPage() {
}
} ;
const rememberPeqFiltersForPreset = useCallback (
( presetName : string | undefined , filters : PeqFilter [ ] ) = > {
if ( ! presetName || filters . length === 0 ) return ;
peqFiltersCacheRef . current [ presetName ] = filters ;
const rememberPeqPresetCache = useCallback (
( presetName : string | undefined , patch : PeqPresetLocalCache ) = > {
if ( ! presetName ) return ;
const prev = peqPresetCacheRef . current [ presetName ] ? ? { } ;
const next : PeqPresetLocalCache = { . . . prev , . . . patch } ;
if ( next . filters ? . length === 0 ) delete next . filters ;
peqPresetCacheRef . current [ presetName ] = next ;
} ,
[ ] ,
) ;
const mergeRemotePeqCatalog = useCallback ( ( items : PeqCatalogItem [ ] ) = > {
return items . map ( ( item ) = > {
const cached = peqFiltersCacheRef . current [ item . name ] ;
return cached ? . length ? { . . . item , filters : cached } : item ;
const cached = peqPresetCacheRef . current [ item . name ] ;
if ( ! cached ) return item ;
return {
. . . item ,
. . . ( cached . filters ? . length ? { filters : cached.filters } : { } ) ,
. . . ( cached . preamp !== undefined ? { preamp : cached.preamp } : { } ) ,
. . . ( cached . autoPre !== undefined ? { autoPre : cached.autoPre } : { } ) ,
} ;
} ) ;
} , [ ] ) ;
@@ -496,19 +515,33 @@ export default function EQPage() {
[ mergeRemotePeqCatalog ] ,
) ;
/** 将当前 A 曲线写回 peqItems 与本地缓存,切换耳机时才能恢复编辑结果 */
const persistFiltersToPeqItem = useCallback (
( catalogIdx : number , filtersSource : typeof DEFAULT_BANDS ) = > {
/** 将当前 A 曲线与总增益 写回 peqItems 与本地缓存,切换耳机时才能恢复编辑结果 */
const persistPresetEditsToPeqItem = useCallback (
(
catalogIdx : number ,
filtersSource : typeof DEFAULT_BANDS ,
meta ? : { preamp? : number ; autoPre? : number } ,
) = > {
const filters = filtersSource . map ( bandToPeqFilter ) ;
setPeqItems ( ( prev ) = >
prev . map ( ( item , i ) = > {
if ( i !== catalogIdx ) return item ;
rememberPeqFiltersForPreset ( item . name , filters ) ;
return { . . . item , filters } ;
const merged = {
. . . item ,
filters ,
. . . ( meta ? . preamp !== undefined ? { preamp : meta.preamp } : { } ) ,
. . . ( meta ? . autoPre !== undefined ? { autoPre : meta.autoPre } : { } ) ,
} ;
rememberPeqPresetCache ( item . name , {
filters ,
preamp : merged.preamp ,
autoPre : merged.autoPre ,
} ) ;
return merged ;
} ) ,
) ;
} ,
[ rememberPeqFiltersForPreset ] ,
[ rememberPeqPresetCache ] ,
) ;
const handleSelectHeadphone = useCallback (
@@ -519,8 +552,15 @@ export default function EQPage() {
}
const leaving = peqItems [ headphoneIdx ] ;
const leavingFilters = bandsByMode . A . map ( bandToPeqFilter ) ;
rememberPeqFiltersForPreset ( leaving ? . name , leavingFilters ) ;
persistFiltersToPeqItem ( headphoneIdx , bandsByMode . A ) ;
rememberPeqPresetCache ( leaving ? . name , {
filters : leavingFilters ,
preamp : leaving?.preamp ,
autoPre : leaving?.autoPre ,
} ) ;
persistPresetEditsToPeqItem ( headphoneIdx , bandsByMode . A , {
preamp : leaving?.preamp ,
autoPre : leaving?.autoPre ,
} ) ;
if ( peqSyncTimerRef . current !== null ) {
window . clearTimeout ( peqSyncTimerRef . current ) ;
peqSyncTimerRef . current = null ;
@@ -533,8 +573,8 @@ export default function EQPage() {
bandsByMode . A ,
headphoneIdx ,
peqItems ,
persistFiltersToPeqItem ,
rememberPeqFiltersForPreset ,
persistPresetEditsToPeqItem ,
rememberPeqPresetCache ,
updateSetting ,
] ,
) ;
@@ -580,13 +620,13 @@ export default function EQPage() {
try {
if ( isDemoMode || ! api ) {
const nextItems = peqItems . filter ( ( _ , idx ) = > idx !== headphoneIdx ) ;
delete peqFiltersCacheRef . current [ target . name ] ;
delete peqPresetCacheRef . current [ target . name ] ;
applyPeqStateToUI ( { peq : nextItems , peqSelect : Math.max ( 0 , headphoneIdx - 1 ) } ) ;
toast . success ( eqUi . toastDeleted ) ;
return ;
}
delete peqFiltersCacheRef . current [ target . name ] ;
delete peqPresetCacheRef . current [ target . name ] ;
await api . removePeq ( [ target . name ] ) ;
const latest = await api . getPeqState ( ) ;
applyPeqStateToUI ( latest ) ;
@@ -973,7 +1013,10 @@ export default function EQPage() {
const usePeqChange =
topLevel === "peqChange" || ( topLevel === "byMode" && mode === "A" ) ;
if ( usePeqChange ) {
persistFiltersToPeqItem ( idxAtSchedule , filtersSource ) ;
persistPresetEditsToPeqItem ( idxAtSchedule , filtersSource , {
preamp : peq.preamp ,
autoPre : peq.autoPre ,
} ) ;
}
const submit = usePeqChange
? ( ) = > upgradePeqChange ( { peqChange : body } satisfies PeqChangePayload )
@@ -983,7 +1026,7 @@ export default function EQPage() {
} ) ;
} , delay ) ;
} ,
[ api , headphoneIdx , isDemoMode , persistFiltersToPeqItem , upgradePeqApply , upgradePeqChange ] ,
[ api , headphoneIdx , isDemoMode , persistPresetEditsToPeqItem , upgradePeqApply , upgradePeqChange ] ,
) ;
/** 点击 A/B 切换试听:顶层固定 peqApply(A/B 数据结构相同,仅 filters 不同) */
@@ -1045,7 +1088,11 @@ export default function EQPage() {
} , [ ] ) ;
const chartRef = useRef < echarts.ECharts | null > ( null ) ;
const band = bands [ selectedBand ] ;
const band = bands [ selectedBand ] ? ? DEFAULT_BANDS [ selectedBand ] ? ? DEFAULT_BANDS [ 0 ] ;
useEffect ( ( ) = > {
setBandSliderPreview ( null ) ;
} , [ selectedBand , abMode , headphoneSwitchKey ] ) ;
// Render frequency response chart using ECharts
const renderCharts = ( peqFilters : typeof bands , raw : number [ ] | null , changeParam : boolean ) = > {
@@ -1146,7 +1193,11 @@ export default function EQPage() {
skipPeqAutoSyncRef . current = true ;
setBandsByMode ( ( prev ) = > ( { . . . prev , A : cloneBands ( bBands ) } ) ) ;
setSelectedBandByMode ( ( prev ) = > ( { . . . prev , A : prev.B } ) ) ;
rememberPeqFiltersForPreset ( peq . name , filters ) ;
rememberPeqPresetCache ( peq . name , {
filters ,
preamp : peq.preamp ,
autoPre : peq.autoPre ,
} ) ;
setPeqItems ( ( prev ) = >
prev . map ( ( item , i ) = > ( i !== headphoneIdx ? item : { . . . item , filters } ) ) ,
) ;
@@ -1177,10 +1228,12 @@ export default function EQPage() {
upgradePeqChange ,
] ) ;
const updateBand = ( idx : number , patch : Partial < typeof bands [ 0 ] > ) = > {
const nextBands = bands . map ( ( b , i ) = > ( i === idx ? { . . . b , . . . patch } : b ) ) ;
setBands ( nextBands ) ;
} ;
const updateBand = useCallback (
( idx : number , patch : Partial < typeof DEFAULT_BANDS [ 0 ] > ) = > {
setBands ( ( prevBands ) = > prevBands . map ( ( b , i ) = > ( i === idx ? { . . . b , . . . patch } : b ) ) ) ;
} ,
[ setBands ] ,
) ;
const bandParamDialogMeta = useMemo ( ( ) = > {
if ( ! bandParamDialog ) return null ;
@@ -1264,6 +1317,7 @@ export default function EQPage() {
if ( i !== headphoneIdx ) return item ;
const merged = { . . . item , . . . patch } ;
updatedPeq = merged ;
rememberPeqPresetCache ( item . name , patch ) ;
return merged ;
} ) ;
return next ;
@@ -1436,7 +1490,12 @@ export default function EQPage() {
background : "rgba(44,44,46,0.65)" ,
} }
onClick = { ( ) = > setSelectedBand ( i ) } >
< span className = "text-[12px] font-bold leading-tight lg:text-[11px]" > { freqLabel ( b . freq ) } < / span >
< span
className = "notranslate text-[12px] font-bold leading-tight tabular-nums lg:text-[11px]"
translate = "no"
>
{ freqLabel ( b . freq ) }
< / span >
< span className = "text-[9px] mt-0.5 opacity-70 leading-tight lg:text-[8px] lg:mt-0" > { getFilterShortName ( b . type ) } < / span >
< / button >
) ) }
@@ -1508,18 +1567,23 @@ export default function EQPage() {
< button
type = "button"
onClick = { ( ) = > openBandParamDialog ( "freq" ) }
className = "text-[13px] font-semibold px-2.5 py-1 rounded-[8px] text-white active:scale-95 transition-transform"
className = "notranslate text-[13px] font-semibold tabular-nums px-2.5 py-1 rounded-[8px] text-white active:scale-95 transition-transform"
style = { BAND_PARAM_VALUE_BOX_STYLE }
translate = "no"
>
{ formatBandFreqDisplay ( band . freq ) }
{ formatBandFreqDisplay ( bandSliderPreview ? . freq ? ? band . freq ) }
< / button >
< / div >
< CyanSlider
value = { Math . log10 ( band . freq ) }
value = { Math . log10 ( bandSliderPreview ? . freq ? ? band . freq ) }
min = { Math . log10 ( BAND_FREQ_MIN ) }
max = { Math . log10 ( BAND_FREQ_MAX ) }
step = { 0.005 }
onChange = { ( v ) = > updateBand ( selectedBand , { freq : Math.round ( Math . pow ( 10 , v ) ) } ) }
onChange = { ( v ) = > {
const freq = Math . round ( Math . pow ( 10 , v ) ) ;
setBandSliderPreview ( ( prev ) = > ( { . . . prev , freq } ) ) ;
updateBand ( selectedBand , { freq } ) ;
} }
/ >
< / div >
@@ -1533,15 +1597,18 @@ export default function EQPage() {
className = "text-[13px] font-semibold px-2.5 py-1 rounded-[8px] text-white active:scale-95 transition-transform"
style = { BAND_PARAM_VALUE_BOX_STYLE }
>
{ band . gain >= 0 ? ` + ${ band . gain . toFixed ( 1 ) } ` : band . gain . toFixed ( 1 ) } dB
{ formatBandGainDisplay ( bandSliderPreview ? . gain ? ? band . gain ) }
< / button >
< / div >
< CyanSlider
value = { band . gain }
value = { bandSliderPreview ? . gain ? ? band . gain }
min = { BAND_GAIN_MIN }
max = { BAND_GAIN_MAX }
step = { 0.1 }
onChange = { ( v ) = > updateBand ( selectedBand , { gain : v } ) }
onChange = { ( v ) = > {
setBandSliderPreview ( ( prev ) = > ( { . . . prev , gain : v } ) ) ;
updateBand ( selectedBand , { gain : v } ) ;
} }
/ >
< / div >
@@ -1555,15 +1622,18 @@ export default function EQPage() {
className = "text-[13px] font-semibold px-2.5 py-1 rounded-[8px] text-white active:scale-95 transition-transform"
style = { BAND_PARAM_VALUE_BOX_STYLE }
>
{ band . q . toFixed ( 2 ) }
{ formatBandQDisplay ( bandSliderPreview ? . q ? ? band . q ) }
< / button >
< / div >
< CyanSlider
value = { band . q }
value = { bandSliderPreview ? . q ? ? band . q }
min = { BAND_Q_MIN }
max = { BAND_Q_MAX }
step = { 0.01 }
onChange = { ( v ) = > updateBand ( selectedBand , { q : v } ) }
onChange = { ( v ) = > {
setBandSliderPreview ( ( prev ) = > ( { . . . prev , q : v } ) ) ;
updateBand ( selectedBand , { q : v } ) ;
} }
/ >
< / div >
< / div >