@@ -11,6 +11,11 @@ const VRM_POPUP_ANIMATION_DURATION_MS = 200;
1111 const style = document . createElement ( 'style' ) ;
1212 style . id = 'vrm-popup-styles' ;
1313 style . textContent = `
14+ :root {
15+ --neko-popup-selected-bg: rgba(68, 183, 254, 0.1);
16+ --neko-popup-selected-hover: rgba(68, 183, 254, 0.15);
17+ --neko-popup-hover-subtle: rgba(68, 183, 254, 0.08);
18+ }
1419 .vrm-popup {
1520 position: absolute;
1621 left: 100%;
@@ -270,7 +275,7 @@ VRMManager.prototype._createSettingsPopupContent = function (popup) {
270275 ] ;
271276
272277 settingsToggles . forEach ( toggle => {
273- const toggleItem = this . _createSettingsToggleItem ( toggle , popup ) ;
278+ const toggleItem = this . _createSettingsToggleItem ( toggle ) ;
274279 popup . appendChild ( toggleItem ) ;
275280
276281 if ( toggle . hasInterval ) {
@@ -396,7 +401,7 @@ VRMManager.prototype._createChatSettingsSidePanel = function (popup) {
396401 ] ;
397402
398403 chatToggles . forEach ( toggle => {
399- const toggleItem = this . _createSettingsToggleItem ( toggle , popup ) ;
404+ const toggleItem = this . _createSettingsToggleItem ( toggle ) ;
400405 container . appendChild ( toggleItem ) ;
401406 } ) ;
402407
@@ -982,27 +987,38 @@ VRMManager.prototype._createToggleItem = function (toggle, popup) {
982987} ;
983988
984989// 创建设置开关项
985- VRMManager . prototype . _createSettingsToggleItem = function ( toggle , popup ) {
990+ VRMManager . prototype . _createSettingsToggleItem = function ( toggle ) {
986991 const toggleItem = document . createElement ( 'div' ) ;
987992 toggleItem . className = 'vrm-toggle-item' ;
993+ toggleItem . id = `vrm-toggle-${ toggle . id } ` ;
988994 toggleItem . setAttribute ( 'role' , 'switch' ) ;
989995 toggleItem . setAttribute ( 'tabIndex' , '0' ) ;
990996 toggleItem . setAttribute ( 'aria-checked' , 'false' ) ;
991- toggleItem . style . padding = '8px 12px' ;
997+ toggleItem . setAttribute ( 'aria-label' , toggle . label ) ;
998+ Object . assign ( toggleItem . style , {
999+ padding : '8px 12px'
1000+ } ) ;
9921001
9931002 const checkbox = document . createElement ( 'input' ) ;
9941003 checkbox . type = 'checkbox' ;
9951004 checkbox . id = `vrm-${ toggle . id } ` ;
996- checkbox . style . position = 'absolute' ;
997- checkbox . style . opacity = '0' ;
998- checkbox . style . width = '1px' ;
999- checkbox . style . height = '1px' ;
1000- checkbox . style . overflow = 'hidden' ;
1005+ Object . assign ( checkbox . style , {
1006+ position : 'absolute' ,
1007+ width : '1px' ,
1008+ height : '1px' ,
1009+ padding : '0' ,
1010+ margin : '-1px' ,
1011+ overflow : 'hidden' ,
1012+ clip : 'rect(0, 0, 0, 0)' ,
1013+ whiteSpace : 'nowrap' ,
1014+ border : '0'
1015+ } ) ;
10011016 checkbox . setAttribute ( 'aria-hidden' , 'true' ) ;
10021017
1003- // 初始化状态
1004- if ( toggle . id === 'merge-messages' && typeof window . mergeMessagesEnabled !== 'undefined' ) {
1005- checkbox . checked = window . mergeMessagesEnabled ;
1018+ if ( toggle . id === 'merge-messages' ) {
1019+ if ( typeof window . mergeMessagesEnabled !== 'undefined' ) {
1020+ checkbox . checked = window . mergeMessagesEnabled ;
1021+ }
10061022 } else if ( toggle . id === 'focus-mode' && typeof window . focusModeEnabled !== 'undefined' ) {
10071023 checkbox . checked = toggle . inverted ? ! window . focusModeEnabled : window . focusModeEnabled ;
10081024 } else if ( toggle . id === 'proactive-chat' && typeof window . proactiveChatEnabled !== 'undefined' ) {
@@ -1018,97 +1034,163 @@ VRMManager.prototype._createSettingsToggleItem = function (toggle, popup) {
10181034
10191035 const checkmark = document . createElement ( 'div' ) ;
10201036 checkmark . className = 'vrm-toggle-checkmark' ;
1037+ checkmark . setAttribute ( 'aria-hidden' , 'true' ) ;
10211038 checkmark . innerHTML = '✓' ;
10221039 indicator . appendChild ( checkmark ) ;
10231040
1041+ const updateIndicatorStyle = ( checked ) => {
1042+ if ( checked ) {
1043+ indicator . style . backgroundColor = 'var(--neko-popup-active, #2a7bc4)' ;
1044+ indicator . style . borderColor = 'var(--neko-popup-active, #2a7bc4)' ;
1045+ checkmark . style . opacity = '1' ;
1046+ } else {
1047+ indicator . style . backgroundColor = 'transparent' ;
1048+ indicator . style . borderColor = 'var(--neko-popup-indicator-border, #ccc)' ;
1049+ checkmark . style . opacity = '0' ;
1050+ }
1051+ } ;
1052+
10241053 const label = document . createElement ( 'label' ) ;
1025- label . className = 'vrm-toggle-label' ;
10261054 label . innerText = toggle . label ;
1027- if ( toggle . labelKey ) label . setAttribute ( 'data-i18n' , toggle . labelKey ) ;
1028- label . htmlFor = `vrm-${ toggle . id } ` ;
1055+ if ( toggle . labelKey ) {
1056+ label . setAttribute ( 'data-i18n' , toggle . labelKey ) ;
1057+ }
1058+ label . style . cursor = 'pointer' ;
1059+ label . style . userSelect = 'none' ;
1060+ label . style . fontSize = '13px' ;
1061+ label . style . color = 'var(--neko-popup-text, #333)' ;
10291062 label . style . display = 'flex' ;
10301063 label . style . alignItems = 'center' ;
1064+ label . style . lineHeight = '1' ;
10311065 label . style . height = '20px' ;
1032- toggleItem . setAttribute ( 'aria-label' , toggle . label ) ;
1033-
1034- // 更新标签文本的函数
1035- const updateLabelText = ( ) => {
1036- if ( toggle . labelKey && window . t ) {
1037- label . innerText = window . t ( toggle . labelKey ) ;
1038- toggleItem . setAttribute ( 'aria-label' , window . t ( toggle . labelKey ) ) ;
1039- }
1040- } ;
1041- if ( toggle . labelKey ) {
1042- toggleItem . _updateLabelText = updateLabelText ;
1043- }
10441066
10451067 const updateStyle = ( ) => {
10461068 const isChecked = checkbox . checked ;
10471069 toggleItem . setAttribute ( 'aria-checked' , isChecked ? 'true' : 'false' ) ;
10481070 indicator . setAttribute ( 'aria-checked' , isChecked ? 'true' : 'false' ) ;
1049- if ( isChecked ) {
1050- toggleItem . style . background = 'var(--neko-popup-selected-bg, rgba(68, 183, 254, 0.1))' ;
1051- } else {
1052- toggleItem . style . background = 'transparent' ;
1053- }
1071+ updateIndicatorStyle ( isChecked ) ;
1072+ toggleItem . style . background = isChecked
1073+ ? 'var(--neko-popup-selected-bg, rgba(68,183,254,0.1))'
1074+ : 'transparent' ;
10541075 } ;
1076+
10551077 updateStyle ( ) ;
10561078
1057- toggleItem . appendChild ( checkbox ) ; toggleItem . appendChild ( indicator ) ; toggleItem . appendChild ( label ) ;
1079+ toggleItem . appendChild ( checkbox ) ;
1080+ toggleItem . appendChild ( indicator ) ;
1081+ toggleItem . appendChild ( label ) ;
10581082
10591083 toggleItem . addEventListener ( 'mouseenter' , ( ) => {
10601084 if ( checkbox . checked ) {
1061- toggleItem . style . background = 'var(--neko-popup-selected-hover, rgba(68, 183, 254, 0.15))' ;
1085+ toggleItem . style . background = 'var(--neko-popup-selected-hover, rgba(68,183,254,0.15))' ;
10621086 } else {
1063- toggleItem . style . background = 'var(--neko-popup-hover-subtle, rgba(68, 183, 254, 0.08))' ;
1087+ toggleItem . style . background = 'var(--neko-popup-hover-subtle, rgba(68,183,254,0.08))' ;
10641088 }
10651089 } ) ;
1066- toggleItem . addEventListener ( 'mouseleave' , updateStyle ) ;
1090+ toggleItem . addEventListener ( 'mouseleave' , ( ) => {
1091+ updateStyle ( ) ;
1092+ } ) ;
10671093
10681094 const handleToggleChange = ( isChecked ) => {
10691095 updateStyle ( ) ;
1070- if ( typeof window . saveNEKOSettings === 'function' ) {
1071- if ( toggle . id === 'merge-messages' ) {
1072- window . mergeMessagesEnabled = isChecked ;
1096+
1097+ if ( toggle . id === 'merge-messages' ) {
1098+ window . mergeMessagesEnabled = isChecked ;
1099+ if ( typeof window . saveNEKOSettings === 'function' ) {
10731100 window . saveNEKOSettings ( ) ;
1074- } else if ( toggle . id === 'focus-mode' ) {
1075- window . focusModeEnabled = toggle . inverted ? ! isChecked : isChecked ;
1101+ }
1102+ } else if ( toggle . id === 'focus-mode' ) {
1103+ const actualValue = toggle . inverted ? ! isChecked : isChecked ;
1104+ window . focusModeEnabled = actualValue ;
1105+ if ( typeof window . saveNEKOSettings === 'function' ) {
10761106 window . saveNEKOSettings ( ) ;
1077- } else if ( toggle . id === 'proactive-chat' ) {
1078- window . proactiveChatEnabled = isChecked ;
1107+ }
1108+ } else if ( toggle . id === 'proactive-chat' ) {
1109+ window . proactiveChatEnabled = isChecked ;
1110+ if ( typeof window . saveNEKOSettings === 'function' ) {
10791111 window . saveNEKOSettings ( ) ;
1080- if ( isChecked ) {
1081- window . resetProactiveChatBackoff && window . resetProactiveChatBackoff ( ) ;
1082- } else {
1083- if ( ! window . proactiveChatEnabled && ! window . proactiveVisionEnabled && window . stopProactiveChatSchedule ) window . stopProactiveChatSchedule ( ) ;
1084- }
1085- } else if ( toggle . id === 'proactive-vision' ) {
1086- window . proactiveVisionEnabled = isChecked ;
1112+ }
1113+ if ( isChecked && typeof window . resetProactiveChatBackoff === 'function' ) {
1114+ window . resetProactiveChatBackoff ( ) ;
1115+ } else if ( ! isChecked && typeof window . stopProactiveChatSchedule === 'function' ) {
1116+ window . stopProactiveChatSchedule ( ) ;
1117+ }
1118+ } else if ( toggle . id === 'proactive-vision' ) {
1119+ window . proactiveVisionEnabled = isChecked ;
1120+ if ( typeof window . saveNEKOSettings === 'function' ) {
10871121 window . saveNEKOSettings ( ) ;
1088- if ( isChecked ) {
1089- window . resetProactiveChatBackoff && window . resetProactiveChatBackoff ( ) ;
1090- if ( window . isRecording && window . startProactiveVisionDuringSpeech ) window . startProactiveVisionDuringSpeech ( ) ;
1091- } else {
1092- if ( ! window . proactiveChatEnabled && window . stopProactiveChatSchedule ) window . stopProactiveChatSchedule ( ) ;
1093- window . stopProactiveVisionDuringSpeech && window . stopProactiveVisionDuringSpeech ( ) ;
1122+ }
1123+ if ( isChecked ) {
1124+ if ( typeof window . resetProactiveChatBackoff === 'function' ) {
1125+ window . resetProactiveChatBackoff ( ) ;
1126+ }
1127+ if ( typeof window . isRecording !== 'undefined' && window . isRecording ) {
1128+ if ( typeof window . startProactiveVisionDuringSpeech === 'function' ) {
1129+ window . startProactiveVisionDuringSpeech ( ) ;
1130+ }
1131+ }
1132+ } else {
1133+ if ( typeof window . stopProactiveChatSchedule === 'function' ) {
1134+ if ( ! window . proactiveChatEnabled ) {
1135+ window . stopProactiveChatSchedule ( ) ;
1136+ }
1137+ }
1138+ if ( typeof window . stopProactiveVisionDuringSpeech === 'function' ) {
1139+ window . stopProactiveVisionDuringSpeech ( ) ;
10941140 }
10951141 }
10961142 }
10971143 } ;
10981144
1099- // 键盘支持
1145+ const performToggle = ( ) => {
1146+ if ( checkbox . _processing ) {
1147+ const elapsed = Date . now ( ) - ( checkbox . _processingTime || 0 ) ;
1148+ if ( elapsed < 500 ) {
1149+ return ;
1150+ }
1151+ }
1152+
1153+ checkbox . _processing = true ;
1154+ checkbox . _processingTime = Date . now ( ) ;
1155+
1156+ const newChecked = ! checkbox . checked ;
1157+ checkbox . checked = newChecked ;
1158+ handleToggleChange ( newChecked ) ;
1159+
1160+ setTimeout ( ( ) => {
1161+ checkbox . _processing = false ;
1162+ checkbox . _processingTime = null ;
1163+ } , 500 ) ;
1164+ } ;
1165+
11001166 toggleItem . addEventListener ( 'keydown' , ( e ) => {
11011167 if ( e . key === 'Enter' || e . key === ' ' ) {
11021168 e . preventDefault ( ) ;
1103- checkbox . checked = ! checkbox . checked ;
1104- handleToggleChange ( checkbox . checked ) ;
1169+ performToggle ( ) ;
11051170 }
11061171 } ) ;
11071172
1108- checkbox . addEventListener ( 'change' , ( e ) => { e . stopPropagation ( ) ; handleToggleChange ( checkbox . checked ) ; } ) ;
1109- [ toggleItem , indicator , label ] . forEach ( el => el . addEventListener ( 'click' , ( e ) => {
1110- if ( e . target !== checkbox ) { e . preventDefault ( ) ; e . stopPropagation ( ) ; checkbox . checked = ! checkbox . checked ; handleToggleChange ( checkbox . checked ) ; }
1111- } ) ) ;
1173+ toggleItem . addEventListener ( 'click' , ( e ) => {
1174+ if ( e . target !== checkbox ) {
1175+ e . preventDefault ( ) ;
1176+ e . stopPropagation ( ) ;
1177+ performToggle ( ) ;
1178+ }
1179+ } ) ;
1180+
1181+ indicator . addEventListener ( 'click' , ( e ) => {
1182+ e . preventDefault ( ) ;
1183+ e . stopPropagation ( ) ;
1184+ performToggle ( ) ;
1185+ } ) ;
1186+
1187+ label . addEventListener ( 'click' , ( e ) => {
1188+ e . preventDefault ( ) ;
1189+ e . stopPropagation ( ) ;
1190+ performToggle ( ) ;
1191+ } ) ;
1192+
1193+ checkbox . updateStyle = updateStyle ;
11121194
11131195 return toggleItem ;
11141196} ;
@@ -1374,49 +1456,37 @@ VRMManager.prototype.closeAllSettingsWindows = function (exceptUrl = null) {
13741456
13751457// 显示弹出框
13761458VRMManager . prototype . showPopup = function ( buttonId , popup ) {
1377- // 使用 display === 'flex' 判断弹窗是否可见(避免动画中误判)
13781459 const isVisible = popup . style . display === 'flex' ;
13791460
1380- // 如果是设置弹出框,每次显示时更新开关状态
13811461 if ( buttonId === 'settings' ) {
1382- const updateCheckboxStyle = ( checkbox ) => {
1462+ const syncCheckbox = ( checkbox , checked ) => {
13831463 if ( ! checkbox ) return ;
1384- const toggleItem = checkbox . parentElement ;
1385- // 使用 class 选择器查找元素,避免依赖 DOM 结构顺序
1386- const indicator = toggleItem ?. querySelector ( '.vrm-toggle-indicator' ) ;
1387- const checkmark = indicator ?. querySelector ( '.vrm-toggle-checkmark' ) ;
1388- if ( ! indicator || ! checkmark ) {
1389- console . warn ( '[VRM UI Popup] 无法找到 toggle indicator 或 checkmark 元素' ) ;
1390- return ;
1391- }
1392- if ( checkbox . checked ) {
1393- indicator . style . backgroundColor = 'var(--neko-popup-active, #44b7fe)' ; indicator . style . borderColor = 'var(--neko-popup-active, #44b7fe)' ; checkmark . style . opacity = '1' ; toggleItem . style . background = 'var(--neko-popup-selected-bg, rgba(68, 183, 254, 0.1))' ;
1394- } else {
1395- indicator . style . backgroundColor = 'transparent' ; indicator . style . borderColor = 'var(--neko-popup-indicator-border, #ccc)' ; checkmark . style . opacity = '0' ; toggleItem . style . background = 'transparent' ;
1464+ checkbox . checked = checked ;
1465+ if ( typeof checkbox . updateStyle === 'function' ) {
1466+ checkbox . updateStyle ( ) ;
13961467 }
13971468 } ;
13981469
13991470 const mergeCheckbox = document . querySelector ( '#vrm-merge-messages' ) ;
14001471 if ( mergeCheckbox && typeof window . mergeMessagesEnabled !== 'undefined' ) {
1401- mergeCheckbox . checked = window . mergeMessagesEnabled ; updateCheckboxStyle ( mergeCheckbox ) ;
1472+ syncCheckbox ( mergeCheckbox , window . mergeMessagesEnabled ) ;
14021473 }
14031474
14041475 const focusCheckbox = document . querySelector ( '#vrm-focus-mode' ) ;
14051476 if ( focusCheckbox && typeof window . focusModeEnabled !== 'undefined' ) {
1406- focusCheckbox . checked = ! window . focusModeEnabled ; updateCheckboxStyle ( focusCheckbox ) ;
1477+ syncCheckbox ( focusCheckbox , ! window . focusModeEnabled ) ;
14071478 }
14081479
14091480 const proactiveChatCheckbox = popup . querySelector ( '#vrm-proactive-chat' ) ;
14101481 if ( proactiveChatCheckbox && typeof window . proactiveChatEnabled !== 'undefined' ) {
1411- proactiveChatCheckbox . checked = window . proactiveChatEnabled ; updateCheckboxStyle ( proactiveChatCheckbox ) ;
1482+ syncCheckbox ( proactiveChatCheckbox , window . proactiveChatEnabled ) ;
14121483 }
14131484
14141485 const proactiveVisionCheckbox = popup . querySelector ( '#vrm-proactive-vision' ) ;
14151486 if ( proactiveVisionCheckbox && typeof window . proactiveVisionEnabled !== 'undefined' ) {
1416- proactiveVisionCheckbox . checked = window . proactiveVisionEnabled ; updateCheckboxStyle ( proactiveVisionCheckbox ) ;
1487+ syncCheckbox ( proactiveVisionCheckbox , window . proactiveVisionEnabled ) ;
14171488 }
14181489
1419- // 同步搭话方式选项状态
14201490 if ( window . CHAT_MODE_CONFIG ) {
14211491 window . CHAT_MODE_CONFIG . forEach ( config => {
14221492 const checkbox = document . querySelector ( `#vrm-proactive-${ config . mode } -chat` ) ;
0 commit comments