Skip to content

Commit e67138e

Browse files
authored
🐛 修复徽章计数显示的问题 (#986)
* fix: Fixed Badge Count Display for making all scripts inactive * ` // 之前也是没数据的话,不用 tx.set (storage.session.set)` * 次序调整,效能优化 * .
1 parent 9b17387 commit e67138e

1 file changed

Lines changed: 121 additions & 87 deletions

File tree

src/app/service/service_worker/popup.ts

Lines changed: 121 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,28 @@ let lastActiveTabId = 0;
6666
// 串接中的更新承诺:序列化 genScriptMenu 执行,避免并行重建 contextMenu。
6767
let 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页面的数据
7092
export 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

Comments
 (0)