From 34e8098bb9578061f5287c045ef25d7740d4b047 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 1 Oct 2025 18:38:52 +0900 Subject: [PATCH 1/3] =?UTF-8?q?=E4=BA=8C=E7=BA=A7=E8=8F=9C=E5=8D=95&?= =?UTF-8?q?=E5=88=86=E9=9A=94=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/service_worker/popup.ts | 96 ++++++++++++------- src/pages/components/ScriptMenuList/index.tsx | 5 +- 2 files changed, 65 insertions(+), 36 deletions(-) diff --git a/src/app/service/service_worker/popup.ts b/src/app/service/service_worker/popup.ts index 944d5a77c..d3d770c81 100644 --- a/src/app/service/service_worker/popup.ts +++ b/src/app/service/service_worker/popup.ts @@ -40,32 +40,59 @@ export class PopupService { private systemConfig: SystemConfig ) {} - genScriptMenuByTabMap(menu: ScriptMenu[]) { - let n = 0; + genScriptMenuByTabMap(menuEntries: chrome.contextMenus.CreateProperties[], menu: ScriptMenu[]) { for (const { uuid, name, menus } of menu) { - // 如果是带输入框的菜单则不在页面内注册 - const nonInputMenus = menus.filter((item) => !item.options?.inputType); - // 创建脚本菜单 - if (nonInputMenus.length) { - n += nonInputMenus.length; - chrome.contextMenus.create({ - id: `scriptMenu_${uuid}`, - title: name, - contexts: ["all"], - parentId: "scriptMenu", - }); - nonInputMenus.forEach((menu) => { - // 创建菜单 - chrome.contextMenus.create({ - id: `scriptMenu_menu_${uuid}_${menu.id}`, - title: menu.name, + const subMenuEntries = [] as chrome.contextMenus.CreateProperties[]; + let withMenuItem = false; + // eslint-disable-next-line prefer-const + for (let { id, name, options } of menus) { + // 如果是带输入框的菜单则不在页面内注册 + if (options?.inputType) return; + let level = 3; + if (name[0] === "\xA7") { + // section sign (§) + level = 2; + name = name.substring(1); + // chrome.contextMenus的API限制:不支持一级菜单创建 (不支持 §§) + } + let createProperties: chrome.contextMenus.CreateProperties; + name = name.trim(); + if (!name.length) { + // 创建菜单分隔线 + createProperties = { + id: `scriptMenu_menu_${uuid}_${id}`, + type: "separator", contexts: ["all"], - parentId: `scriptMenu_${uuid}`, - }); - }); + }; + } else { + // 创建菜单项目 + createProperties = { + id: `scriptMenu_menu_${uuid}_${id}`, + title: name, + contexts: ["all"], + }; + withMenuItem = true; + } + if (level === 3) { + createProperties.parentId = `scriptMenu_${uuid}`; + } else if (level === 2) { + createProperties.parentId = `scriptMenu`; + } + subMenuEntries.push(createProperties); + } + if (withMenuItem) { + menuEntries.push( + { + // 创建脚本菜单 + id: `scriptMenu_${uuid}`, + title: name, + contexts: ["all"], + parentId: "scriptMenu", + }, + ...subMenuEntries + ); } } - return n; } // 生成chrome菜单 @@ -81,23 +108,24 @@ export class PopupService { if (!menu.length && !backgroundMenu.length) { return; } - let n = 0; - // 创建根菜单 - chrome.contextMenus.create({ - id: "scriptMenu", - title: "ScriptCat", - contexts: ["all"], - }); + const menuEntries = [] as chrome.contextMenus.CreateProperties[]; if (menu) { - n += this.genScriptMenuByTabMap(menu); + this.genScriptMenuByTabMap(menuEntries, menu); } // 后台脚本的菜单 if (backgroundMenu) { - n += this.genScriptMenuByTabMap(backgroundMenu); + this.genScriptMenuByTabMap(menuEntries, backgroundMenu); } - if (n === 0) { - // 如果没有菜单,删除菜单 - await chrome.contextMenus.remove("scriptMenu"); + if (menuEntries.length > 0) { + // 创建根菜单 + menuEntries.unshift({ + id: "scriptMenu", + title: "ScriptCat", + contexts: ["all"], + }); + for (const menuEntry of menuEntries) { + chrome.contextMenus.create(menuEntry); + } } } diff --git a/src/pages/components/ScriptMenuList/index.tsx b/src/pages/components/ScriptMenuList/index.tsx index bc646f9f2..133eec3ea 100644 --- a/src/pages/components/ScriptMenuList/index.tsx +++ b/src/pages/components/ScriptMenuList/index.tsx @@ -62,7 +62,8 @@ const MenuItem = React.memo(({ menu, uuid }: MenuItemProps) => { return null; } })(); - + const menuName = menu.name.replace(/^\xA7+/, "").trim(); + if (!menuName) return <>; return (
{ title={menu.options?.title} style={{ display: "block", width: "100%" }} > - {menu.name} + {menuName} {menu.options?.accessKey && `(${menu.options.accessKey.toUpperCase()})`} {InputMenu && ( From b80def7b57105ff94e12cdb4303e2185b868372e Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 1 Oct 2025 22:19:59 +0900 Subject: [PATCH 2/3] Update src/app/service/service_worker/popup.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/app/service/service_worker/popup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/service/service_worker/popup.ts b/src/app/service/service_worker/popup.ts index d3d770c81..6f84f81b5 100644 --- a/src/app/service/service_worker/popup.ts +++ b/src/app/service/service_worker/popup.ts @@ -47,7 +47,7 @@ export class PopupService { // eslint-disable-next-line prefer-const for (let { id, name, options } of menus) { // 如果是带输入框的菜单则不在页面内注册 - if (options?.inputType) return; + if (options?.inputType) continue; let level = 3; if (name[0] === "\xA7") { // section sign (§) From 3abd3f90b36dba9eb178f89b356955e6a3154a8e Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 1 Oct 2025 22:51:08 +0900 Subject: [PATCH 3/3] nested --- src/app/service/content/gm_api.ts | 52 +++++++++++++------------ src/app/service/service_worker/popup.ts | 1 + src/app/service/service_worker/types.ts | 25 ++++++------ 3 files changed, 43 insertions(+), 35 deletions(-) diff --git a/src/app/service/content/gm_api.ts b/src/app/service/content/gm_api.ts index 2f7211447..b81da79c5 100644 --- a/src/app/service/content/gm_api.ts +++ b/src/app/service/content/gm_api.ts @@ -1,6 +1,6 @@ import type { Message, MessageConnect } from "@Packages/message/types"; import type { CustomEventMessage } from "@Packages/message/custom_event_message"; -import type { NotificationMessageOption, ScriptMenuItem } from "../service_worker/types"; +import type { NotificationMessageOption, ScriptMenuItemOption } from "../service_worker/types"; import { base64ToBlob, strToBase64 } from "@App/pkg/utils/utils"; import LoggerCore from "@App/app/logger/core"; import EventEmitter from "eventemitter3"; @@ -446,39 +446,43 @@ export default class GMApi extends GM_Base { GM_registerMenuCommand( name: string, listener: (inputValue?: any) => void, - options_or_accessKey?: ScriptMenuItem["options"] | string + options_or_accessKey?: ScriptMenuItemOption | string ): number { if (!this.menuMap) { this.menuMap = new Map(); } - if (typeof options_or_accessKey === "object") { - const option: ScriptMenuItem["options"] = options_or_accessKey; - // 如果是对象,并且有id属性,则直接使用id - if (option.id && this.menuMap.has(option.id)) { - // 如果id存在,则直接使用 - this.EE.removeAllListeners("menuClick:" + option.id); - this.EE.addListener("menuClick:" + option.id, listener); - this.sendMessage("GM_registerMenuCommand", [option.id, name, option]); - return option.id; - } - } else { - options_or_accessKey = { accessKey: options_or_accessKey }; - let flag = 0; - this.menuMap.forEach((val, menuId) => { - if (val === name) { - flag = menuId; - } - }); - if (flag) { - return flag; + const options: ScriptMenuItemOption = + typeof options_or_accessKey === "string" + ? ({ accessKey: options_or_accessKey! } as ScriptMenuItemOption) + : options_or_accessKey! || ({} as ScriptMenuItemOption); + + if (options.autoClose === undefined) { + options.autoClose = true; + } + + if (options.nested === undefined) { + options.nested = true; + } + + // 如果是对象,并且有id属性,则直接使用id + if (options.id && this.menuMap.has(options.id)) { + // 如果id存在,则直接使用 + this.EE.removeAllListeners("menuClick:" + options.id); + this.EE.addListener("menuClick:" + options.id, listener); + this.sendMessage("GM_registerMenuCommand", [options.id, name, options]); + return options.id; + } + for (const [mappedId, mappedName] of this.menuMap) { + if (mappedName === name) { + return mappedId; } } this.eventId += 1; const id = this.eventId; - options_or_accessKey.id = id; + options.id = id; this.menuMap.set(id, name); this.EE.addListener("menuClick:" + id, listener); - this.sendMessage("GM_registerMenuCommand", [id, name, options_or_accessKey]); + this.sendMessage("GM_registerMenuCommand", [id, name, options]); return id; } diff --git a/src/app/service/service_worker/popup.ts b/src/app/service/service_worker/popup.ts index 6f84f81b5..4b44c74e3 100644 --- a/src/app/service/service_worker/popup.ts +++ b/src/app/service/service_worker/popup.ts @@ -49,6 +49,7 @@ export class PopupService { // 如果是带输入框的菜单则不在页面内注册 if (options?.inputType) continue; let level = 3; + if (options?.nested === false) level = 2; if (name[0] === "\xA7") { // section sign (§) level = 2; diff --git a/src/app/service/service_worker/types.ts b/src/app/service/service_worker/types.ts index 5e03ffa22..4a74e45ed 100644 --- a/src/app/service/service_worker/types.ts +++ b/src/app/service/service_worker/types.ts @@ -64,20 +64,23 @@ export type Api = (request: Request, con: IGetSender) => Promise; // popup +export type ScriptMenuItemOption = { + id?: number; + autoClose?: boolean; // default true + title?: string; + accessKey?: string; + nested?: boolean; // default true + // 可选输入框 + inputType?: "text" | "number" | "boolean"; + inputLabel?: string; + inputDefaultValue?: string | number | boolean; + inputPlaceholder?: string; +}; + export type ScriptMenuItem = { id: number; name: string; - options?: { - id?: number; - autoClose?: boolean; - title?: string; - accessKey?: string; - // 可选输入框 - inputType?: "text" | "number" | "boolean"; - inputLabel?: string; - inputDefaultValue?: string | number | boolean; - inputPlaceholder?: string; - }; + options?: ScriptMenuItemOption; tabId: number; //-1表示后台脚本 frameId?: number; documentId?: string;