diff --git a/example/gm_menu.js b/example/gm_menu.js index 71f61eb..973eb55 100644 --- a/example/gm_menu.js +++ b/example/gm_menu.js @@ -9,8 +9,20 @@ // @grant GM_unregisterMenuCommand // ==/UserScript== - -const id = GM_registerMenuCommand("测试菜单", () => { +const id = GM_registerMenuCommand( + "测试菜单", + () => { console.log(id); GM_unregisterMenuCommand(id); -}, "h"); + }, + "h" +); + +const id2 = GM_registerMenuCommand( + "测试菜单2", + () => { + console.log(id2); + GM_unregisterMenuCommand(id2); + }, + "j" +); diff --git a/src/app/cache.ts b/src/app/cache.ts index 5f44f68..b36c949 100644 --- a/src/app/cache.ts +++ b/src/app/cache.ts @@ -143,11 +143,9 @@ export default class Cache { if (promise) { await promise; } - promise = this.get(key) .then((result) => set(result)) .then((value) => { - console.log("tx", key, value); if (value) { return this.set(key, value); } diff --git a/src/app/service/service_worker/client.ts b/src/app/service/service_worker/client.ts index db65ad2..18139fb 100644 --- a/src/app/service/service_worker/client.ts +++ b/src/app/service/service_worker/client.ts @@ -48,6 +48,10 @@ export class ScriptClient extends Client { getScriptRunResource(script: Script): Promise { return this.do("getScriptRunResource", script); } + + excludeUrl(uuid: string, url: string, remove: boolean) { + return this.do("excludeUrl", { uuid, url, remove }); + } } export class ResourceClient extends Client { diff --git a/src/app/service/service_worker/popup.ts b/src/app/service/service_worker/popup.ts index 95adc32..4aaa554 100644 --- a/src/app/service/service_worker/popup.ts +++ b/src/app/service/service_worker/popup.ts @@ -10,6 +10,7 @@ import { Script, ScriptDAO, SCRIPT_TYPE_NORMAL, + SCRIPT_RUN_STATUS_RUNNING, } from "@App/app/repo/scripts"; import { ScriptMenuRegisterCallbackValue, @@ -54,9 +55,11 @@ export class PopupService { ) {} genScriptMenuByTabMap(menu: ScriptMenu[]) { + let n = 0; menu.forEach((script) => { // 创建脚本菜单 if (script.menus.length) { + n += script.menus.length; chrome.contextMenus.create({ id: `scriptMenu_` + script.uuid, title: script.name, @@ -65,7 +68,6 @@ export class PopupService { }); script.menus.forEach((menu) => { // 创建菜单 - console.log("create menu", menu); chrome.contextMenus.create({ id: `scriptMenu_menu_${script.uuid}_${menu.id}`, title: menu.name, @@ -75,6 +77,7 @@ export class PopupService { }); } }); + return n; } // 生成chrome菜单 @@ -86,6 +89,7 @@ export class PopupService { if (!menu.length && !backgroundMenu.length) { return; } + let n = 0; // 创建根菜单 chrome.contextMenus.create({ id: "scriptMenu", @@ -93,18 +97,21 @@ export class PopupService { contexts: ["all"], }); if (menu) { - this.genScriptMenuByTabMap(menu); + n += this.genScriptMenuByTabMap(menu); } // 后台脚本的菜单 if (backgroundMenu) { - this.genScriptMenuByTabMap(backgroundMenu); + n += this.genScriptMenuByTabMap(backgroundMenu); + } + if (n === 0) { + // 如果没有菜单,删除菜单 + chrome.contextMenus.remove("scriptMenu"); } } async registerMenuCommand(message: ScriptMenuRegisterCallbackValue) { // 给脚本添加菜单 return this.txUpdateScriptMenu(message.tabId, async (data) => { - console.log("register menu", message, data); const script = data.find((item) => item.uuid === message.uuid); if (script) { const menu = script.menus.find((item) => item.id === message.id); @@ -165,7 +172,7 @@ export class PopupService { hasUserConfig: !!script.config, metadata: script.metadata, runStatus: script.runStatus, - runNum: 0, + runNum: script.type === SCRIPT_TYPE_NORMAL ? 0 : script.runStatus === SCRIPT_RUN_STATUS_RUNNING ? 1 : 0, runNumByIframe: 0, menus: [], customExclude: (script as ScriptMatchInfo).customizeExcludeMatches || [], @@ -175,21 +182,21 @@ export class PopupService { // 获取popup页面数据 async getPopupData(req: GetPopupDataReq): Promise { // 获取当前tabId - const scriptUuid = await this.runtime.getPageScriptByUrl(req.url); + const script = await this.runtime.getPageScriptByUrl(req.url, true); // 与运行时脚本进行合并 const runScript = await this.getScriptMenu(req.tabId); - // 筛选出未运行的脚本 - const notRunScript = scriptUuid.filter((script) => { - return !runScript.find((item) => item.uuid === script.uuid); - }); - // 将未运行的脚本转换为菜单 - const scriptList = notRunScript.map((script): ScriptMenu => { + // 合并数据 + const scriptMenu = script.map((script) => { + const run = runScript.find((item) => item.uuid === script.uuid); + if (run) { + // 如果脚本已经存在,则不添加,赋值状态 + run.enable = script.status === SCRIPT_STATUS_ENABLE; + return run; + } return this.scriptToMenu(script); }); - runScript.push(...scriptList); // 后台脚本只显示开启或者运行中的脚本 - - return { scriptList: runScript, backScriptList: await this.getScriptMenu(-1) }; + return { scriptList: scriptMenu, backScriptList: await this.getScriptMenu(-1) }; } async getScriptMenu(tabId: number) { @@ -263,17 +270,17 @@ export class PopupService { return; } return this.txUpdateScriptMenu(-1, async (menu) => { - const scriptMenu = menu.find((item) => item.uuid === uuid); + const index = menu.findIndex((item) => item.uuid === uuid); if (script.status === SCRIPT_STATUS_ENABLE) { // 加入菜单 - if (!scriptMenu) { + if (index === -1) { const item = this.scriptToMenu(script); menu.push(item); } } else { // 移出菜单 - if (scriptMenu) { - menu.splice(menu.indexOf(scriptMenu), 1); + if (index !== -1) { + menu.splice(index, 1); } } return menu; @@ -281,9 +288,9 @@ export class PopupService { }); subscribeScriptDelete(this.mq, async ({ uuid }) => { return this.txUpdateScriptMenu(-1, async (menu) => { - const scriptMenu = menu.find((item) => item.uuid === uuid); - if (scriptMenu) { - menu.splice(menu.indexOf(scriptMenu), 1); + const index = menu.findIndex((item) => item.uuid === uuid); + if (index !== -1) { + menu.splice(index, 1); return menu; } return null; @@ -293,6 +300,7 @@ export class PopupService { return this.txUpdateScriptMenu(-1, async (menu) => { const scriptMenu = menu.find((item) => item.uuid === uuid); if (scriptMenu) { + scriptMenu.runNum = 1; scriptMenu.runStatus = runStatus; return menu; } diff --git a/src/app/service/service_worker/runtime.ts b/src/app/service/service_worker/runtime.ts index 420e480..f50475d 100644 --- a/src/app/service/service_worker/runtime.ts +++ b/src/app/service/service_worker/runtime.ts @@ -67,7 +67,7 @@ export class RuntimeService { // 加载页面脚本 await this.loadPageScript(script); if (!data.enable) { - await this.unregistryPageScript(script); + await this.unregistryPageScript(script.uuid); } } }); @@ -82,15 +82,9 @@ export class RuntimeService { } }); // 监听脚本删除 - subscribeScriptDelete(this.mq, async (data) => { - const script = await this.scriptDAO.get(data.uuid); - if (!script) { - return; - } - if (script.type === SCRIPT_TYPE_NORMAL) { - await this.unregistryPageScript(script); - this.deleteScriptMatch(script.uuid); - } + subscribeScriptDelete(this.mq, async ({ uuid }) => { + await this.unregistryPageScript(uuid); + this.deleteScriptMatch(uuid); }); // 将开启的脚本发送一次enable消息 @@ -138,24 +132,29 @@ export class RuntimeService { return sendMessage(new ExtensionContentMessageSend(tabId, options), "content/runtime/" + action, data); } - async getPageScriptUuidByUrl(url: string) { + async getPageScriptUuidByUrl(url: string, includeCustomize?: boolean) { const match = await this.loadScriptMatchInfo(); // 匹配当前页面的脚本 const matchScriptUuid = match.match(url!); - // 排除自定义匹配 - const excludeScriptUuid = this.scriptCustomizeMatch.match(url!); - const excludeMatch = new Set(); - excludeScriptUuid.forEach((uuid) => { - excludeMatch.add(uuid); - }); - return matchScriptUuid.filter((value) => { - // 过滤掉自定义排除的脚本 - return !excludeMatch.has(value); - }); + // 包含自定义排除的脚本 + if (includeCustomize) { + const excludeScriptUuid = this.scriptCustomizeMatch.match(url!); + const match = new Set(); + excludeScriptUuid.forEach((uuid) => { + match.add(uuid); + }); + matchScriptUuid.forEach((uuid) => { + match.add(uuid); + }); + // 转化为数组 + console.log("matchScriptUuid", matchScriptUuid); + return Array.from(match); + } + return matchScriptUuid; } - async getPageScriptByUrl(url: string) { - const matchScriptUuid = await this.getPageScriptUuidByUrl(url); + async getPageScriptByUrl(url: string, includeCustomize?: boolean) { + const matchScriptUuid = await this.getPageScriptUuidByUrl(url, includeCustomize); return matchScriptUuid.map((uuid) => { return Object.assign({}, this.scriptMatchCache?.get(uuid)); }); @@ -266,15 +265,7 @@ export class RuntimeService { Object.keys(data).forEach((key) => { const item = data[key]; cache.set(item.uuid, item); - item.matches.forEach((match) => { - this.scriptMatch.add(match, item.uuid); - }); - item.excludeMatches.forEach((match) => { - this.scriptMatch.exclude(match, item.uuid); - }); - item.customizeExcludeMatches.forEach((match) => { - this.scriptCustomizeMatch.exclude(match, item.uuid); - }); + this.syncAddScriptMatch(item); }); } }); @@ -305,6 +296,11 @@ export class RuntimeService { await this.loadScriptMatchInfo(); } this.scriptMatchCache!.set(item.uuid, item); + this.syncAddScriptMatch(item); + this.saveScriptMatchInfo(); + } + + syncAddScriptMatch(item: ScriptMatchInfo) { // 清理一下老数据 this.scriptMatch.del(item.uuid); this.scriptCustomizeMatch.del(item.uuid); @@ -316,9 +312,8 @@ export class RuntimeService { this.scriptMatch.exclude(match, item.uuid); }); item.customizeExcludeMatches.forEach((match) => { - this.scriptCustomizeMatch.exclude(match, item.uuid); + this.scriptCustomizeMatch.add(match, item.uuid); }); - this.saveScriptMatchInfo(); } async updateScriptStatus(uuid: string, status: SCRIPT_STATUS) { @@ -329,11 +324,11 @@ export class RuntimeService { this.saveScriptMatchInfo(); } - deleteScriptMatch(uuid: string) { + async deleteScriptMatch(uuid: string) { if (!this.scriptMatchCache) { - return; + await this.loadScriptMatchInfo(); } - this.scriptMatchCache.delete(uuid); + this.scriptMatchCache!.delete(uuid); this.scriptMatch.del(uuid); this.scriptCustomizeMatch.del(uuid); this.saveScriptMatchInfo(); @@ -394,24 +389,28 @@ export class RuntimeService { if (script.metadata["run-at"]) { registerScript.runAt = getRunAt(script.metadata["run-at"]); } - await chrome.userScripts.register([registerScript]); + if (await Cache.getInstance().get("registryScript:" + script.uuid)) { + await chrome.userScripts.update([registerScript]); + } else { + await chrome.userScripts.register([registerScript]); + } await Cache.getInstance().set("registryScript:" + script.uuid, true); } } - async unregistryPageScript(script: Script) { - if (!(await Cache.getInstance().get("registryScript:" + script.uuid))) { + async unregistryPageScript(uuid: string) { + if (!(await Cache.getInstance().get("registryScript:" + uuid))) { return; } chrome.userScripts.unregister( { - ids: [script.uuid], + ids: [uuid], }, () => { // 删除缓存 - Cache.getInstance().del("registryScript:" + script.uuid); + Cache.getInstance().del("registryScript:" + uuid); // 修改脚本状态为disable - this.updateScriptStatus(script.uuid, SCRIPT_STATUS_DISABLE); + this.updateScriptStatus(uuid, SCRIPT_STATUS_DISABLE); } ); } diff --git a/src/app/service/service_worker/script.ts b/src/app/service/service_worker/script.ts index 38fa16b..7cdb4ca 100644 --- a/src/app/service/service_worker/script.ts +++ b/src/app/service/service_worker/script.ts @@ -198,7 +198,7 @@ export class ScriptService { .then(() => { logger.info("delete success"); this.mq.publish("deleteScript", { uuid }); - return {}; + return true; }) .catch((e) => { logger.error("delete error", Logger.E(e)); @@ -279,6 +279,32 @@ export class ScriptService { return Promise.resolve(ret); } + async excludeUrl({ uuid, url, remove }: { uuid: string; url: string; remove: boolean }) { + const script = await this.scriptDAO.get(uuid); + if (!script) { + throw new Error("script not found"); + } + script.selfMetadata = script.selfMetadata || {}; + let excludes = script.selfMetadata.exclude || script.metadata.exclude || []; + if (remove) { + excludes = excludes.filter((item) => item !== url); + } else { + excludes.push(url); + } + script.selfMetadata.exclude = excludes; + return this.scriptDAO + .update(uuid, script) + .then(() => { + // 广播一下 + this.mq.publish("installScript", { script, update: true }); + return true; + }) + .catch((e) => { + this.logger.error("exclude url error", Logger.E(e)); + throw e; + }); + } + init() { this.listenerScriptInstall(); @@ -290,5 +316,6 @@ export class ScriptService { this.group.on("updateRunStatus", this.updateRunStatus.bind(this)); this.group.on("getCode", this.getCode.bind(this)); this.group.on("getScriptRunResource", this.buildScriptRunResource.bind(this)); + this.group.on("excludeUrl", this.excludeUrl.bind(this)); } } diff --git a/src/locales/locales.ts b/src/locales/locales.ts index 24fcc95..2889b6b 100644 --- a/src/locales/locales.ts +++ b/src/locales/locales.ts @@ -46,13 +46,13 @@ dayjs.extend(relativeTime); export function i18nName(script: { name: string; metadata: Metadata }) { return script.metadata[`name:${i18n.language.toLowerCase()}`] - ? script.metadata[`name:${i18n.language.toLowerCase()}`][0] + ? script.metadata[`name:${i18n.language.toLowerCase()}`]![0] : script.name; } export function i18nDescription(script: { metadata: Metadata }) { return script.metadata[`description:${i18n.language.toLowerCase()}`] - ? script.metadata[`description:${i18n.language.toLowerCase()}`][0] + ? script.metadata[`description:${i18n.language.toLowerCase()}`]![0] : script.metadata.description; } diff --git a/src/manifest.json b/src/manifest.json index 302f0cf..b0e83b6 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -31,6 +31,7 @@ "webRequest", "userScripts", "contextMenus", + "unlimitedStorage", "declarativeNetRequest" ], "host_permissions": [ diff --git a/src/pages/components/ScriptMenuList/index.tsx b/src/pages/components/ScriptMenuList/index.tsx index 5ae1491..61a73d4 100644 --- a/src/pages/components/ScriptMenuList/index.tsx +++ b/src/pages/components/ScriptMenuList/index.tsx @@ -14,10 +14,12 @@ import { RiPlayFill, RiStopFill } from "react-icons/ri"; import { useTranslation } from "react-i18next"; import { ScriptIcons } from "@App/pages/options/routes/utils"; import { ScriptMenu, ScriptMenuItem } from "@App/app/service/service_worker/popup"; -import { MessageSender } from "@Packages/message/server"; import { selectMenuExpandNum } from "@App/pages/store/features/setting"; import { useAppSelector } from "@App/pages/store/hooks"; -import { popupClient } from "@App/pages/store/features/script"; +import { popupClient, runtimeClient, scriptClient } from "@App/pages/store/features/script"; +import { i18nName } from "@App/locales/locales"; +import { subscribeScriptRunStatus } from "@App/app/service/queue"; +import { messageQueue } from "@App/pages/store/global"; const CollapseItem = Collapse.Item; @@ -26,7 +28,7 @@ function isExclude(script: ScriptMenu, host: string) { return false; } for (let i = 0; i < script.customExclude.length; i += 1) { - if (script.customExclude[i] === `*://${host}*`) { + if (script.customExclude[i] === `*://${host}/*`) { return true; } } @@ -56,32 +58,28 @@ const ScriptMenuList: React.FC<{ setList(script); }, [script]); - // useEffect(() => { - // // 监听脚本运行状态 - // const channel = runtimeCtrl.watchRunStatus(); - // channel.setHandler(([id, status]: any) => { - // setList((prev) => { - // const newList = [...prev]; - // const index = newList.findIndex((item) => item.id === id); - // if (index !== -1) { - // newList[index].runStatus = status; - // } - // return newList; - // }); - // }); - // return () => { - // channel.disChannel(); - // }; - // }, []); + useEffect(() => { + // 监听脚本运行状态 + const unsub = subscribeScriptRunStatus(messageQueue, ({ uuid, runStatus }) => { + setList((prev) => { + const newList = [...prev]; + const index = newList.findIndex((item) => item.uuid === uuid); + if (index !== -1) { + newList[index].runStatus = runStatus; + } + return newList; + }); + }); + return () => { + unsub(); + }; + }, []); const sendMenuAction = (uuid: string, menu: ScriptMenuItem) => { popupClient.menuClick(uuid, menu).then(() => { window.close(); }); }; - // 监听菜单按键 - - // 菜单展开 return ( <> @@ -110,21 +108,15 @@ const ScriptMenuList: React.FC<{ size="small" checked={item.enable} onChange={(checked) => { - let p: Promise; - if (checked) { - p = scriptCtrl.enable(item.id).then(() => { - item.enable = true; + scriptClient + .enable(item.uuid, checked) + .then(() => { + item.enable = checked; + setList([...list]); + }) + .catch((err) => { + Message.error(err); }); - } else { - p = scriptCtrl.disable(item.id).then(() => { - item.enable = false; - }); - } - p.catch((err) => { - Message.error(err); - }).finally(() => { - setList([...list]); - }); }} /> - {item.name} + {i18nName(item)} @@ -154,9 +146,9 @@ const ScriptMenuList: React.FC<{ icon={item.runStatus !== SCRIPT_RUN_STATUS_RUNNING ? : } onClick={() => { if (item.runStatus !== SCRIPT_RUN_STATUS_RUNNING) { - runtimeCtrl.startScript(item.id); + runtimeClient.runScript(item.uuid); } else { - runtimeCtrl.stopScript(item.id); + runtimeClient.stopScript(item.uuid); } }} > @@ -168,7 +160,7 @@ const ScriptMenuList: React.FC<{ type="secondary" icon={} onClick={() => { - window.open(`/src/options.html#/script/editor/${item.id}`, "_blank"); + window.open(`/src/options.html#/script/editor/${item.uuid}`, "_blank"); window.close(); }} > @@ -181,7 +173,7 @@ const ScriptMenuList: React.FC<{ type="secondary" icon={} onClick={() => { - scriptCtrl.exclude(item.id, `*://${url.host}*`, isExclude(item, url.host)).finally(() => { + scriptClient.excludeUrl(item.uuid, `*://${url.host}/*`, isExclude(item, url.host)).finally(() => { window.close(); }); }} @@ -194,8 +186,8 @@ const ScriptMenuList: React.FC<{ title={t("confirm_delete_script")} icon={} onOk={() => { - setList(list.filter((i) => i.id !== item.id)); - scriptCtrl.delete(item.id).catch((e) => { + setList(list.filter((i) => i.uuid !== item.uuid)); + scriptClient.delete(item.uuid).catch((e) => { Message.error(`{t('delete_failed')}: ${e}`); }); }} @@ -259,7 +251,7 @@ const ScriptMenuList: React.FC<{ type="secondary" icon={} onClick={() => { - window.open(`/src/options.html#/?userConfig=${item.id}`, "_blank"); + window.open(`/src/options.html#/?userConfig=${item.uuid}`, "_blank"); window.close(); }} > diff --git a/src/pages/options/routes/ScriptList.tsx b/src/pages/options/routes/ScriptList.tsx index e30b52b..fe5800a 100644 --- a/src/pages/options/routes/ScriptList.tsx +++ b/src/pages/options/routes/ScriptList.tsx @@ -71,22 +71,16 @@ import { i18nName } from "@App/locales/locales"; import { getValues, ListHomeRender, ScriptIcons } from "./utils"; import { useAppDispatch, useAppSelector } from "@App/pages/store/hooks"; import { - deleteScript, requestEnableScript, fetchAndSortScriptList, requestDeleteScript, ScriptLoading, selectScripts, sortScript, - upsertScript, requestStopScript, requestRunScript, } from "@App/pages/store/features/script"; import { selectScriptListColumnWidth } from "@App/pages/store/features/setting"; -import { MessageQueue, Unsubscribe } from "@Packages/message/message_queue"; -import { subscribeScriptDelete, subscribeScriptInstall, subscribeScriptRunStatus } from "@App/app/service/queue"; -import { RuntimeClient } from "@App/app/service/service_worker/client"; -import { ExtensionMessage, ExtensionMessageSend } from "@Packages/message/extension_message"; type ListType = Script & { loading?: boolean }; diff --git a/src/pages/options/routes/script/ScriptEditor.tsx b/src/pages/options/routes/script/ScriptEditor.tsx index bae87fb..e22e7b5 100644 --- a/src/pages/options/routes/script/ScriptEditor.tsx +++ b/src/pages/options/routes/script/ScriptEditor.tsx @@ -17,6 +17,7 @@ import ScriptStorage from "@App/pages/components/ScriptStorage"; import ScriptResource from "@App/pages/components/ScriptResource"; import ScriptSetting from "@App/pages/components/ScriptSetting"; import { scriptClient } from "@App/pages/store/features/script"; +import { i18nName } from "@App/locales/locales"; import { useTranslation } from "react-i18next"; const { Row } = Grid; @@ -377,7 +378,7 @@ function ScriptEditor() { }); useEffect(() => { scriptDAO.all().then(async (scripts) => { - setScriptList(scripts); + setScriptList(scripts.sort((a, b) => a.sort - b.sort)); // 如果有id则打开对应的脚本 if (uuid) { for (let i = 0; i < scripts.length; i += 1) { @@ -734,7 +735,7 @@ function ScriptEditor() { } }} > - {script.name} + {i18nName(script)} ))} diff --git a/src/pages/popup/App.tsx b/src/pages/popup/App.tsx index 2658c9b..7cdb79a 100644 --- a/src/pages/popup/App.tsx +++ b/src/pages/popup/App.tsx @@ -65,6 +65,10 @@ function App() { const list = resp.scriptList; list.sort((a, b) => { if (a.enable === b.enable) { + // 根据菜单数排序 + if (a.menus.length !== b.menus.length) { + return b.menus.length - a.menus.length; + } if (a.runNum !== b.runNum) { return b.runNum - a.runNum; }