diff --git a/src/app/service/content/gm_api.ts b/src/app/service/content/gm_api.ts index 99c2dafbd..9adea3ec8 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 b2943b0d4..365012dde 100644 --- a/src/app/service/service_worker/popup.ts +++ b/src/app/service/service_worker/popup.ts @@ -41,32 +41,60 @@ 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) continue; + let level = 3; + if (options?.nested === false) level = 2; + 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菜单 @@ -82,23 +110,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/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; diff --git a/src/pages/components/ScriptMenuList/index.tsx b/src/pages/components/ScriptMenuList/index.tsx index a732c596c..8781565ce 100644 --- a/src/pages/components/ScriptMenuList/index.tsx +++ b/src/pages/components/ScriptMenuList/index.tsx @@ -63,7 +63,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 && (