@@ -66,6 +66,28 @@ let lastActiveTabId = 0;
6666// 串接中的更新承诺:序列化 genScriptMenu 执行,避免并行重建 contextMenu。
6767let contextMenuUpdatePromise = Promise . resolve ( ) ;
6868
69+ // 呼叫 API 设置 Badge
70+ const apiSetBadge = ( o : { text : string ; tabId : number ; backgroundColor ?: string ; textColor ?: string } ) => {
71+ const { text, tabId, backgroundColor, textColor } = o ;
72+ if ( ! text ) badgeShownSet . delete ( tabId ) ;
73+ chrome . action . setBadgeText ( {
74+ text : text ,
75+ tabId : tabId ,
76+ } ) ;
77+ if ( backgroundColor ) {
78+ chrome . action . setBadgeBackgroundColor ( {
79+ color : backgroundColor ,
80+ tabId : tabId ,
81+ } ) ;
82+ }
83+ if ( textColor ) {
84+ chrome . action . setBadgeTextColor ( {
85+ color : textColor ,
86+ tabId : tabId ,
87+ } ) ;
88+ }
89+ } ;
90+
6991// 处理popup页面的数据
7092export class PopupService {
7193 constructor (
@@ -128,59 +150,60 @@ export class PopupService {
128150 }
129151
130152 // 生成chrome菜单
131- async genScriptMenu ( ) {
153+ genScriptMenu ( ) {
132154 // 使用简单 Promise chain 避免同一个程序同时跑
133155 contextMenuUpdatePromise = contextMenuUpdatePromise
134156 . then ( async ( ) => {
135157 const tabId = lastActiveTabId ;
136- if ( tabId === 0 ) return ;
137- const menuEntries = [ ] as chrome . contextMenus . CreateProperties [ ] ;
138- const displayType = await this . systemConfig . getScriptMenuDisplayType ( ) ;
139- if ( displayType === "all" ) {
140- const [ menu , backgroundMenu ] = await Promise . all ( [ this . getScriptMenu ( tabId ) , this . getScriptMenu ( - 1 ) ] ) ;
141- if ( menu ?. length ) this . genScriptMenuByTabMap ( menuEntries , menu ) ;
142- if ( backgroundMenu ?. length ) this . genScriptMenuByTabMap ( menuEntries , backgroundMenu ) ; // 后台脚本的菜单
143- if ( menuEntries . length > 0 ) {
144- // 创建根菜单
145- // 若有子项才建立根节点「ScriptCat」,避免出现空的顶层菜单。
146- menuEntries . unshift ( {
147- id : "scriptMenu" ,
148- title : "ScriptCat" ,
149- contexts : [ "all" ] ,
150- } ) ;
151- }
152- }
153-
154- // 移除之前所有的菜单
155- await chrome . contextMenus . removeAll ( ) ;
156- contextMenuConvMap1 . clear ( ) ;
157- contextMenuConvMap2 . clear ( ) ;
158-
159- let i = 0 ;
160- for ( const menuEntry of menuEntries ) {
161- // 菜单项目用的共通 uuid. 不会随 tab 切换或换页换iframe载入等行为改变。稳定id
162- // 稳定显示 id:即使 removeAll 重建,显示 id 仍保持一致以规避 Chrome 的不稳定行为。
163- const menuDisplayId = `${ groupKeyNS } -${ 100000 + i } ` ;
164- // 把 SC管理用id 换成 menu显示用id
165- if ( menuEntry . id ) {
166- // 建立 SC id ↔ 显示 id 的双向映射:parentId/点击回推都依赖此映射。
167- contextMenuConvMap1 . set ( menuEntry . id ! , menuDisplayId ) ; // 用于parentId转换menuDisplayId
168- contextMenuConvMap2 . set ( menuDisplayId , menuEntry . id ! ) ; // 用于menuDisplayId转换成SC管理用id
169- menuEntry . id = menuDisplayId ;
170- }
171- if ( menuEntry . parentId ) {
172- menuEntry . parentId = contextMenuConvMap1 . get ( menuEntry . parentId ) || menuEntry . parentId ;
158+ if ( tabId > 0 ) {
159+ const menuEntries = [ ] as chrome . contextMenus . CreateProperties [ ] ;
160+ const displayType = await this . systemConfig . getScriptMenuDisplayType ( ) ;
161+ if ( displayType === "all" ) {
162+ const [ menu , backgroundMenu ] = await Promise . all ( [ this . getScriptMenu ( tabId ) , this . getScriptMenu ( - 1 ) ] ) ;
163+ if ( menu ?. length ) this . genScriptMenuByTabMap ( menuEntries , menu ) ;
164+ if ( backgroundMenu ?. length ) this . genScriptMenuByTabMap ( menuEntries , backgroundMenu ) ; // 后台脚本的菜单
165+ if ( menuEntries . length > 0 ) {
166+ // 创建根菜单
167+ // 若有子项才建立根节点「ScriptCat」,避免出现空的顶层菜单。
168+ menuEntries . unshift ( {
169+ id : "scriptMenu" ,
170+ title : "ScriptCat" ,
171+ contexts : [ "all" ] ,
172+ } ) ;
173+ }
173174 }
174175
175- i ++ ;
176- // 由于使用旧id,旧的内部context menu item应会被重用因此不会造成记忆体失控。
177- // (推论内部有cache机制,即使removeAll也是有残留)
178- chrome . contextMenus . create ( menuEntry , ( ) => {
179- const lastError = chrome . runtime . lastError ;
180- if ( lastError ) {
181- console . error ( "chrome.runtime.lastError in chrome.contextMenus.create:" , lastError . message ) ;
176+ // 移除之前所有的菜单
177+ await chrome . contextMenus . removeAll ( ) ;
178+ contextMenuConvMap1 . clear ( ) ;
179+ contextMenuConvMap2 . clear ( ) ;
180+
181+ let i = 0 ;
182+ for ( const menuEntry of menuEntries ) {
183+ // 菜单项目用的共通 uuid. 不会随 tab 切换或换页换iframe载入等行为改变。稳定id
184+ // 稳定显示 id:即使 removeAll 重建,显示 id 仍保持一致以规避 Chrome 的不稳定行为。
185+ const menuDisplayId = `${ groupKeyNS } -${ 100000 + i } ` ;
186+ // 把 SC管理用id 换成 menu显示用id
187+ if ( menuEntry . id ) {
188+ // 建立 SC id ↔ 显示 id 的双向映射:parentId/点击回推都依赖此映射。
189+ contextMenuConvMap1 . set ( menuEntry . id ! , menuDisplayId ) ; // 用于parentId转换menuDisplayId
190+ contextMenuConvMap2 . set ( menuDisplayId , menuEntry . id ! ) ; // 用于menuDisplayId转换成SC管理用id
191+ menuEntry . id = menuDisplayId ;
182192 }
183- } ) ;
193+ if ( menuEntry . parentId ) {
194+ menuEntry . parentId = contextMenuConvMap1 . get ( menuEntry . parentId ) || menuEntry . parentId ;
195+ }
196+
197+ i ++ ;
198+ // 由于使用旧id,旧的内部context menu item应会被重用因此不会造成记忆体失控。
199+ // (推论内部有cache机制,即使removeAll也是有残留)
200+ chrome . contextMenus . create ( menuEntry , ( ) => {
201+ const lastError = chrome . runtime . lastError ;
202+ if ( lastError ) {
203+ console . error ( "chrome.runtime.lastError in chrome.contextMenus.create:" , lastError . message ) ;
204+ }
205+ } ) ;
206+ }
184207 }
185208 } )
186209 . catch ( console . warn ) ;
@@ -221,7 +244,7 @@ export class PopupService {
221244 nested : undefined ,
222245 mSeparator : undefined ,
223246 } )
224- : `${ nameForKey } _${ options . mIndividualKey } ` ; // 一般菜單項目不需要 JSON.stringify
247+ : `${ nameForKey } _${ options . mIndividualKey } ` ; // 一般菜单项目不需要 JSON.stringify
225248 const groupKey = `${ uuidv5 ( popupGroup , groupKeyNS ) } ,${ options . nested ? 3 : 2 } ` ;
226249 const menu = menus . find ( ( item ) => item . key === key ) ;
227250 if ( ! menu ) {
@@ -297,18 +320,20 @@ export class PopupService {
297320
298321 // 更新脚本菜单
299322 async updateScriptMenu ( tabId : number ) {
300- if ( tabId !== lastActiveTabId ) return ; // 其他页面的指令,不理
301-
302- // 注意:不要使用 getCurrentTab()。
303- // 因为如果使用者切换到其他应用(如 Excel/Photoshop),网页仍可能触发 menu 的注册/解除操作。
304- // 若此时用 getCurrentTab(),就无法正确更新右键选单。
305-
306- // 检查一下 tab的有效性
307- // 仅针对目前 lastActiveTabId 进行检查与更新,避免误在非当前 tab 重建菜单。
308- const tab = await chrome . tabs . get ( lastActiveTabId ) ;
309- if ( tab && ! tab . frozen && tab . active && ! tab . discarded && tab . lastAccessed ) {
310- // 更新菜单 / 生成菜单
311- await this . genScriptMenu ( ) ;
323+ if ( tabId > 0 ) {
324+ if ( tabId !== lastActiveTabId ) return ; // 其他页面的指令,不理
325+
326+ // 注意:不要使用 getCurrentTab()。
327+ // 因为如果使用者切换到其他应用(如 Excel/Photoshop),网页仍可能触发 menu 的注册/解除操作。
328+ // 若此时用 getCurrentTab(),就无法正确更新右键选单。
329+
330+ // 检查一下 tab的有效性
331+ // 仅针对目前 lastActiveTabId 进行检查与更新,避免误在非当前 tab 重建菜单。
332+ const tab = await chrome . tabs . get ( lastActiveTabId ) ;
333+ if ( tab && ! tab . frozen && tab . active && ! tab . discarded && tab . lastAccessed ) {
334+ // 更新菜单 / 生成菜单
335+ this . genScriptMenu ( ) ;
336+ }
312337 }
313338 }
314339
@@ -378,8 +403,18 @@ export class PopupService {
378403 const { tabId, frameId, scriptmenus } = o ;
379404 // 设置数据
380405 await cacheInstance . tx ( `${ CACHE_KEY_TAB_SCRIPT } ${ tabId } ` , ( data : ScriptMenu [ ] | undefined , tx ) => {
406+ const isPrevDataEmpty = ! data ?. length ;
381407 // 特例:frameId 为 0/未提供时,重置当前 tab 的计数资料(视为页面重新载入)。
382408 data = ! frameId ? [ ] : data || [ ] ;
409+
410+ // 所有脚本都没有启动。更新适用于之前打开了现在关掉的情况,见 #978
411+ if ( scriptmenus . length === 0 && data . length === 0 ) {
412+ scriptCountMap . set ( tabId , "" ) ;
413+ runCountMap . set ( tabId , "" ) ;
414+ // 之前也是没数据的话,不用 tx.set (storage.session.set)
415+ if ( isPrevDataEmpty ) return ;
416+ }
417+
383418 // 设置脚本运行次数
384419 scriptmenus . forEach ( ( scriptmenu ) => {
385420 const scriptMenu = data . find ( ( item ) => item . uuid === scriptmenu . uuid ) ;
@@ -533,16 +568,16 @@ export class PopupService {
533568 } else {
534569 // 不显示数字
535570 if ( badgeShownSet . has ( tabId ) ) {
536- badgeShownSet . delete ( tabId ) ;
537- chrome . action . setBadgeText ( {
538- text : "" ,
539- tabId : tabId ,
540- } ) ;
571+ apiSetBadge ( { text : "" , tabId } ) ;
541572 }
542573 return ;
543574 }
544575 const text = map . get ( tabId ) ;
545576 if ( typeof text !== "string" ) return ;
577+ if ( ! text && ! badgeShownSet . has ( tabId ) ) {
578+ // 没有脚本不用显示 & 没有设置
579+ return ;
580+ }
546581 const backgroundColor = await this . systemConfig . getBadgeBackgroundColor ( ) ;
547582 const textColor = await this . systemConfig . getBadgeTextColor ( ) ;
548583 // 标记此 tab 的 badge 已设定,便于后续在「不显示」模式时进行清理。
@@ -551,18 +586,7 @@ export class PopupService {
551586 `${ cIdKey } -tabId#${ tabId } ` ,
552587 ( ) => {
553588 if ( ! badgeShownSet . has ( tabId ) ) return ;
554- chrome . action . setBadgeText ( {
555- text : text || "" ,
556- tabId : tabId ,
557- } ) ;
558- chrome . action . setBadgeBackgroundColor ( {
559- color : backgroundColor ,
560- tabId : tabId ,
561- } ) ;
562- chrome . action . setBadgeTextColor ( {
563- color : textColor ,
564- tabId : tabId ,
565- } ) ;
589+ apiSetBadge ( { text, tabId, backgroundColor, textColor } ) ;
566590 } ,
567591 50
568592 ) ;
@@ -614,28 +638,38 @@ export class PopupService {
614638 }
615639 clearData ( tabId ) ;
616640 } ) ;
617- // 监听页面切换加载菜单
618- // 进程启动时可能尚未触发 onActivated:补一次初始化以建立当前 tab 的菜单与 badge。
619- getCurrentTab ( ) . then ( ( tab ) => {
620- // 处理载入时未触发 chrome.tabs.onActivated 的情况
621- if ( ! lastActiveTabId && tab ?. id ) {
622- lastActiveTabId = tab . id ;
623- this . genScriptMenu ( ) ;
641+ /**
642+ * @param tabId 当前页面的 tabId。如 tabId 为 null, 则呼叫 getCurrentTab() 以API取当前页面的 tabId。
643+ */
644+ const doBadgeAndMenuUpdate = async ( tabId : number | undefined | null = null ) => {
645+ if ( tabId === null ) {
646+ tabId = await getCurrentTab ( ) . then ( ( tab ) => tab ?. id ) ;
647+ }
648+ tabId = tabId || 0 ;
649+ if ( tabId && tabId > 0 ) {
650+ // 若 tabId 有变化,则更新菜单。
651+ if ( lastActiveTabId !== tabId ) {
652+ lastActiveTabId = tabId ;
653+ this . genScriptMenu ( ) ;
654+ }
655+ // 更新Badge显示。
624656 this . updateBadgeIcon ( ) ;
625657 }
626- } ) ;
658+ } ;
659+ // 监听页面切换加载菜单
660+ // 进程启动时可能尚未触发 onActivated:补一次初始化以建立当前 tab 的菜单与 badge。
661+ doBadgeAndMenuUpdate ( null ) ;
627662 chrome . tabs . onActivated . addListener ( ( activeInfo ) => {
628663 const lastError = chrome . runtime . lastError ;
629664 if ( lastError ) {
630665 console . error ( "chrome.runtime.lastError in chrome.tabs.onActivated:" , lastError ) ;
631666 // 没有 tabId 资讯,无法加载菜单
632667 return ;
633668 }
634- lastActiveTabId = activeInfo . tabId ;
635669 // 目前设计:subframe 和 mainframe 的 contextMenu 是共用的。
636670 // 换句话说,subframe 的右键菜单可以执行 mainframe 的选项,反之亦然。
637- this . genScriptMenu ( ) ;
638- this . updateBadgeIcon ( ) ;
671+ lastActiveTabId = 0 ; // 强制呼叫 genScriptMenu()
672+ doBadgeAndMenuUpdate ( activeInfo . tabId ) ;
639673 } ) ;
640674
641675 chrome . webNavigation . onBeforeNavigate . addListener ( ( details ) => {
0 commit comments