2025-04-11 17:40:25 +08:00

416 lines
12 KiB
TypeScript

import { MessageQueue } from "@Packages/message/message_queue";
import { ExtMessageSender, Group } from "@Packages/message/server";
import { RuntimeService, ScriptMatchInfo } from "./runtime";
import Cache from "@App/app/cache";
import { GetPopupDataReq, GetPopupDataRes } from "./client";
import {
SCRIPT_RUN_STATUS,
Metadata,
SCRIPT_STATUS_ENABLE,
Script,
ScriptDAO,
SCRIPT_TYPE_NORMAL,
SCRIPT_RUN_STATUS_RUNNING,
} from "@App/app/repo/scripts";
import {
ScriptMenuRegisterCallbackValue,
subscribeScriptDelete,
subscribeScriptEnable,
subscribeScriptInstall,
subscribeScriptMenuRegister,
subscribeScriptRunStatus,
} from "../queue";
import { getStorageName } from "@App/pkg/utils/utils";
export type ScriptMenuItem = {
id: number;
name: string;
accessKey?: string;
tabId: number; //-1表示后台脚本
frameId?: number;
documentId?: string;
};
export type ScriptMenu = {
uuid: string; // 脚本uuid
name: string; // 脚本名称
storageName: string; // 脚本存储名称
enable: boolean; // 脚本是否启用
updatetime: number; // 脚本更新时间
hasUserConfig: boolean; // 是否有用户配置
metadata: Metadata; // 脚本元数据
runStatus?: SCRIPT_RUN_STATUS; // 脚本运行状态
runNum: number; // 脚本运行次数
runNumByIframe: number; // iframe运行次数
menus: ScriptMenuItem[]; // 脚本菜单
customExclude: string[]; // 自定义排除
};
// 处理popup页面的数据
export class PopupService {
scriptDAO = new ScriptDAO();
constructor(
private group: Group,
private mq: MessageQueue,
private runtime: RuntimeService
) {}
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,
contexts: ["all"],
parentId: "scriptMenu",
});
script.menus.forEach((menu) => {
// 创建菜单
chrome.contextMenus.create({
id: `scriptMenu_menu_${script.uuid}_${menu.id}`,
title: menu.name,
contexts: ["all"],
parentId: `scriptMenu_${script.uuid}`,
});
});
}
});
return n;
}
// 生成chrome菜单
async genScriptMenu(tabId: number) {
// 移除之前所有的菜单
chrome.contextMenus.removeAll();
const [menu, backgroundMenu] = await Promise.all([this.getScriptMenu(tabId), this.getScriptMenu(-1)]);
if (!menu.length && !backgroundMenu.length) {
return;
}
let n = 0;
// 创建根菜单
chrome.contextMenus.create({
id: "scriptMenu",
title: "ScriptCat",
contexts: ["all"],
});
if (menu) {
n += this.genScriptMenuByTabMap(menu);
}
// 后台脚本的菜单
if (backgroundMenu) {
n += this.genScriptMenuByTabMap(backgroundMenu);
}
if (n === 0) {
// 如果没有菜单,删除菜单
chrome.contextMenus.remove("scriptMenu");
}
}
async registerMenuCommand(message: ScriptMenuRegisterCallbackValue) {
// 给脚本添加菜单
return this.txUpdateScriptMenu(message.tabId, async (data) => {
const script = data.find((item) => item.uuid === message.uuid);
if (script) {
const menu = script.menus.find((item) => item.id === message.id);
if (!menu) {
script.menus.push({
id: message.id,
name: message.name,
accessKey: message.accessKey,
tabId: message.tabId,
frameId: message.frameId,
documentId: message.documentId,
});
}
}
this.updateScriptMenu();
return data;
});
}
async unregisterMenuCommand({ id, uuid, tabId }: { id: number; uuid: string; tabId: number }) {
return this.txUpdateScriptMenu(tabId, async (data) => {
// 删除脚本菜单
const script = data.find((item) => item.uuid === uuid);
if (script) {
script.menus = script.menus.filter((item) => item.id !== id);
}
this.updateScriptMenu();
return data;
});
}
updateScriptMenu() {
// 获取当前页面并更新菜单
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
if (!tabs.length) {
return;
}
const tab = tabs[0];
// 生成菜单
tab.id && this.genScriptMenu(tab.id);
});
}
scriptToMenu(script: Script): ScriptMenu {
return {
uuid: script.uuid,
name: script.name,
storageName: getStorageName(script),
enable: script.status === SCRIPT_STATUS_ENABLE,
updatetime: script.updatetime || 0,
hasUserConfig: !!script.config,
metadata: script.metadata,
runStatus: script.runStatus,
runNum: script.type === SCRIPT_TYPE_NORMAL ? 0 : script.runStatus === SCRIPT_RUN_STATUS_RUNNING ? 1 : 0,
runNumByIframe: 0,
menus: [],
customExclude: (script as ScriptMatchInfo).customizeExcludeMatches || [],
};
}
// 获取popup页面数据
async getPopupData(req: GetPopupDataReq): Promise<GetPopupDataRes> {
// 获取当前tabId
const script = await this.runtime.getPageScriptByUrl(req.url, true);
// 与运行时脚本进行合并
const runScript = await this.getScriptMenu(req.tabId);
// 合并数据
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.forEach((script) => {
const index = scriptMenu.findIndex((item) => item.uuid === script.uuid);
// 把运行了但是不在匹配中的脚本加入菜单
if (index === -1) {
scriptMenu.push(script);
}
});
// 后台脚本只显示开启或者运行中的脚本
return { scriptList: scriptMenu, backScriptList: await this.getScriptMenu(-1) };
}
async getScriptMenu(tabId: number) {
return ((await Cache.getInstance().get("tabScript:" + tabId)) || []) as ScriptMenu[];
}
// 事务更新脚本菜单
txUpdateScriptMenu(tabId: number, callback: (menu: ScriptMenu[]) => Promise<any>) {
return Cache.getInstance().tx<ScriptMenu[]>("tabScript:" + tabId, async (menu) => {
return callback(menu || []);
});
}
async addScriptRunNumber({
tabId,
frameId,
scripts,
}: {
tabId: number;
frameId: number;
scripts: ScriptMatchInfo[];
}) {
// 设置数据
return this.txUpdateScriptMenu(tabId, async (data) => {
if (!frameId) {
data = [];
}
// 设置脚本运行次数
scripts.forEach((script) => {
const scriptMenu = data.find((item) => item.uuid === script.uuid);
if (scriptMenu) {
scriptMenu.runNum = (scriptMenu.runNum || 0) + 1;
if (frameId) {
scriptMenu.runNumByIframe = (scriptMenu.runNumByIframe || 0) + 1;
}
} else {
const item = this.scriptToMenu(script);
item.runNum = 1;
if (frameId) {
item.runNumByIframe = 1;
}
data.push(item);
}
});
return data;
});
}
dealBackgroundScriptInstall() {
// 处理后台脚本
subscribeScriptInstall(this.mq, async ({ script }) => {
if (script.type === SCRIPT_TYPE_NORMAL) {
return;
}
if (script.status !== SCRIPT_STATUS_ENABLE) {
return;
}
return this.txUpdateScriptMenu(-1, async (menu) => {
const scriptMenu = menu.find((item) => item.uuid === script.uuid);
// 加入菜单
if (!scriptMenu) {
const item = this.scriptToMenu(script);
menu.push(item);
}
return menu;
});
});
subscribeScriptEnable(this.mq, async ({ uuid }) => {
const script = await this.scriptDAO.get(uuid);
if (!script) {
return;
}
if (script.type === SCRIPT_TYPE_NORMAL) {
return;
}
return this.txUpdateScriptMenu(-1, async (menu) => {
const index = menu.findIndex((item) => item.uuid === uuid);
if (script.status === SCRIPT_STATUS_ENABLE) {
// 加入菜单
if (index === -1) {
const item = this.scriptToMenu(script);
menu.push(item);
}
} else {
// 移出菜单
if (index !== -1) {
menu.splice(index, 1);
}
}
return menu;
});
});
subscribeScriptDelete(this.mq, async ({ uuid }) => {
return this.txUpdateScriptMenu(-1, async (menu) => {
const index = menu.findIndex((item) => item.uuid === uuid);
if (index !== -1) {
menu.splice(index, 1);
}
return menu;
});
});
subscribeScriptRunStatus(this.mq, async ({ uuid, runStatus }) => {
return this.txUpdateScriptMenu(-1, async (menu) => {
const scriptMenu = menu.find((item) => item.uuid === uuid);
if (scriptMenu) {
scriptMenu.runStatus = runStatus;
if (runStatus === SCRIPT_RUN_STATUS_RUNNING) {
scriptMenu.runNum = 1;
} else {
scriptMenu.runNum = 0;
}
}
return menu;
});
});
}
menuClick({ uuid, id, sender }: { uuid: string; id: number; sender: ExtMessageSender }) {
// 菜单点击事件
this.runtime.emitEventToTab(sender, {
uuid,
event: "menuClick",
eventId: id.toString(),
});
return Promise.resolve(true);
}
init() {
// 处理脚本菜单数据
subscribeScriptMenuRegister(this.mq, this.registerMenuCommand.bind(this));
this.mq.subscribe("unregisterMenuCommand", this.unregisterMenuCommand.bind(this));
this.group.on("getPopupData", this.getPopupData.bind(this));
this.group.on("menuClick", this.menuClick.bind(this));
this.dealBackgroundScriptInstall();
// 监听tab开关
chrome.tabs.onRemoved.addListener((tabId) => {
// 清理数据
this.txUpdateScriptMenu(tabId, async () => {
return [];
});
});
// 监听页面切换加载菜单
chrome.tabs.onActivated.addListener((activeInfo) => {
this.genScriptMenu(activeInfo.tabId);
});
// 处理chrome菜单点击
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
const menuIds = (info.menuItemId as string).split("_");
if (menuIds.length === 4) {
const [, , uuid, id] = menuIds;
// 寻找menu信息
const menu = await this.getScriptMenu(tab!.id!);
let script = menu.find((item) => item.uuid === uuid);
let bgscript = false;
if (!script) {
// 从后台脚本中寻找
const backgroundMenu = await this.getScriptMenu(-1);
script = backgroundMenu.find((item) => item.uuid === uuid);
bgscript = true;
}
if (script) {
const menuItem = script.menus.find((item) => item.id === parseInt(id, 10));
if (menuItem) {
this.menuClick({
uuid: script.uuid,
id: menuItem.id,
sender: {
tabId: bgscript ? -1 : tab!.id!,
frameId: menuItem.frameId || 0,
documentId: menuItem.documentId || "",
},
});
return;
}
}
}
});
// 监听运行次数
this.mq.subscribe(
"pageLoad",
async ({
tabId,
frameId,
scripts,
}: {
tabId: number;
frameId: number;
document: string;
scripts: ScriptMatchInfo[];
}) => {
this.addScriptRunNumber({ tabId, frameId, scripts });
// 设置角标和脚本
chrome.action.getBadgeText(
{
tabId: tabId,
},
(res: string) => {
if (res || scripts.length) {
chrome.action.setBadgeText({
text: (scripts.length + (parseInt(res, 10) || 0)).toString(),
tabId: tabId,
});
chrome.action.setBadgeBackgroundColor({
color: "#4e5969",
tabId: tabId,
});
}
}
);
}
);
}
}