popup页与注册菜单
Some checks failed
test / Run tests (push) Failing after 3s
build / Build (push) Failing after 6s

This commit is contained in:
王一之 2025-04-07 18:01:44 +08:00
parent a7620dd7e5
commit c43afb0a94
17 changed files with 667 additions and 359 deletions

View File

@ -84,3 +84,25 @@ export class ExtensionMessageConnect implements MessageConnect {
this.con.onDisconnect.addListener(callback); this.con.onDisconnect.addListener(callback);
} }
} }
export class ExtensionContentMessageSend extends ExtensionMessageSend {
constructor(private tabId: number) {
super();
}
sendMessage(data: any): Promise<any> {
return new Promise((resolve) => {
chrome.tabs.sendMessage(this.tabId, data, (resp) => {
resolve(resp);
});
});
}
connect(data: any): Promise<MessageConnect> {
return new Promise((resolve) => {
const con = chrome.tabs.connect(this.tabId);
con.postMessage(data);
resolve(new ExtensionMessageConnect(con));
});
}
}

View File

@ -17,7 +17,7 @@ export interface MessageConnect {
onDisconnect(callback: () => void): void; onDisconnect(callback: () => void): void;
} }
export type MessageSender = any; export type MessageSender = chrome.runtime.MessageSender;
export class GetSender { export class GetSender {
constructor(private sender: MessageConnect | MessageSender) {} constructor(private sender: MessageConnect | MessageSender) {}
@ -117,13 +117,13 @@ export class Group {
export function forwardMessage(prefix: string, path: string, from: Server, to: MessageSend, middleware?: ApiFunction) { export function forwardMessage(prefix: string, path: string, from: Server, to: MessageSend, middleware?: ApiFunction) {
from.on(path, async (params, fromCon) => { from.on(path, async (params, fromCon) => {
if (middleware) { if (middleware) {
const resp = await middleware(params, new GetSender(fromCon)); const resp = await middleware(params, fromCon);
if (resp !== false) { if (resp !== false) {
return resp; return resp;
} }
} }
if (fromCon) { const fromConnect = fromCon.getConnect();
const fromConnect = fromCon.getConnect(); if (fromConnect) {
to.connect({ action: prefix + "/" + path, data: params }).then((toCon) => { to.connect({ action: prefix + "/" + path, data: params }).then((toCon) => {
fromConnect.onMessage((data) => { fromConnect.onMessage((data) => {
toCon.sendMessage(data); toCon.sendMessage(data);

View File

@ -14,7 +14,7 @@ export class OffscreenManager {
private windowMessage = new WindowMessage(window, sandbox, true); private windowMessage = new WindowMessage(window, sandbox, true);
private windowApi: Server = new Server("offscreen", this.windowMessage); private windowServer: Server = new Server("offscreen", this.windowMessage);
private messageQueue: MessageQueue = new MessageQueue(); private messageQueue: MessageQueue = new MessageQueue();
@ -36,21 +36,21 @@ export class OffscreenManager {
async initManager() { async initManager() {
// 监听消息 // 监听消息
this.windowApi.on("logger", this.logger.bind(this)); this.windowServer.on("logger", this.logger.bind(this));
this.windowApi.on("preparationSandbox", this.preparationSandbox.bind(this)); this.windowServer.on("preparationSandbox", this.preparationSandbox.bind(this));
this.windowApi.on("sendMessageToServiceWorker", this.sendMessageToServiceWorker.bind(this)); this.windowServer.on("sendMessageToServiceWorker", this.sendMessageToServiceWorker.bind(this));
const script = new ScriptService( const script = new ScriptService(
this.windowApi.group("script"), this.windowServer.group("script"),
this.extensionMessage, this.extensionMessage,
this.windowMessage, this.windowMessage,
this.messageQueue this.messageQueue
); );
script.init(); script.init();
// 转发从sandbox来的gm api请求 // 转发从sandbox来的gm api请求
forwardMessage("serviceWorker", "runtime/gmApi", this.windowApi, this.extensionMessage); forwardMessage("serviceWorker", "runtime/gmApi", this.windowServer, this.extensionMessage);
// 转发message queue请求 // 转发message queue请求
const gmApi = new GMApi(this.windowApi.group("gmApi")); const gmApi = new GMApi(this.windowServer.group("gmApi"));
gmApi.init(); gmApi.init();
} }
} }

View File

@ -27,3 +27,19 @@ export function subscribeScriptRunStatus(
) { ) {
return messageQueue.subscribe("scriptRunStatus", callback); return messageQueue.subscribe("scriptRunStatus", callback);
} }
export type ScriptMenuRegisterCallbackValue = {
uuid: string;
id: number;
name: string;
accessKey: string;
tabId: number;
frameId: number;
};
export function subscribeScriptMenuRegister(
messageQueue: MessageQueue,
callback: (message: ScriptMenuRegisterCallbackValue) => void
) {
return messageQueue.subscribe("registerMenuCommand", callback);
}

View File

@ -3,6 +3,7 @@ import { Client } from "@Packages/message/client";
import { InstallSource } from "."; import { InstallSource } from ".";
import { Resource } from "@App/app/repo/resource"; import { Resource } from "@App/app/repo/resource";
import { MessageSend } from "@Packages/message/server"; import { MessageSend } from "@Packages/message/server";
import { ScriptMenu, ScriptMenuItem } from "./popup";
export class ServiceWorkerClient extends Client { export class ServiceWorkerClient extends Client {
constructor(msg: MessageSend) { constructor(msg: MessageSend) {
@ -90,3 +91,27 @@ export class RuntimeClient extends Client {
return this.do("scriptLoad", { flag, uuid }); return this.do("scriptLoad", { flag, uuid });
} }
} }
export type GetPopupDataReq = {
tabId: number;
url: string;
};
export type GetPopupDataRes = {
scriptList: ScriptMenu[];
backScriptList: ScriptMenu[];
};
export class PopupClient extends Client {
constructor(msg: MessageSend) {
super(msg, "serviceWorker/popup");
}
getPopupData(data: GetPopupDataReq): Promise<GetPopupDataRes> {
return this.do("getPopupData", data);
}
menuClick(uuid: string, data: ScriptMenuItem) {
return this.do("menuClick", { uuid, id: data.id, tabId: data.tabId, frameId: data.frameId });
}
}

View File

@ -1,7 +1,7 @@
import LoggerCore from "@App/app/logger/core"; import LoggerCore from "@App/app/logger/core";
import Logger from "@App/app/logger/logger"; import Logger from "@App/app/logger/logger";
import { Script, ScriptDAO } from "@App/app/repo/scripts"; import { Script, ScriptDAO } from "@App/app/repo/scripts";
import { GetSender, Group, MessageSend, MessageSender } from "@Packages/message/server"; import { GetSender, Group, MessageSend } from "@Packages/message/server";
import { ValueService } from "@App/app/service/service_worker/value"; import { ValueService } from "@App/app/service/service_worker/value";
import PermissionVerify from "./permission_verify"; import PermissionVerify from "./permission_verify";
import { connect } from "@Packages/message/client"; import { connect } from "@Packages/message/client";
@ -21,7 +21,6 @@ export type MessageRequest = {
export type Request = MessageRequest & { export type Request = MessageRequest & {
script: Script; script: Script;
sender: MessageSender;
}; };
export type Api = (request: Request, con: GetSender) => Promise<any>; export type Api = (request: Request, con: GetSender) => Promise<any>;
@ -48,7 +47,7 @@ export default class GMApi {
if (!api) { if (!api) {
return Promise.reject(new Error("gm api is not found")); return Promise.reject(new Error("gm api is not found"));
} }
const req = await this.parseRequest(data, { tabId: 0 }); const req = await this.parseRequest(data);
try { try {
await this.permissionVerify.verify(req, api); await this.permissionVerify.verify(req, api);
} catch (e) { } catch (e) {
@ -59,14 +58,13 @@ export default class GMApi {
} }
// 解析请求 // 解析请求
async parseRequest(data: MessageRequest, sender: MessageSender): Promise<Request> { async parseRequest(data: MessageRequest): Promise<Request> {
const script = await this.scriptDAO.get(data.uuid); const script = await this.scriptDAO.get(data.uuid);
if (!script) { if (!script) {
return Promise.reject(new Error("script is not found")); return Promise.reject(new Error("script is not found"));
} }
const req: Request = <Request>data; const req: Request = <Request>data;
req.script = script; req.script = script;
req.sender = sender;
return Promise.resolve(req); return Promise.resolve(req);
} }
@ -76,8 +74,6 @@ export default class GMApi {
return Promise.reject(new Error("param is failed")); return Promise.reject(new Error("param is failed"));
} }
const [key, value] = request.params; const [key, value] = request.params;
const sender = <MessageSender & { runFlag: string }>request.sender;
sender.runFlag = request.runFlag;
return this.value.setValue(request.script.uuid, key, value); return this.value.setValue(request.script.uuid, key, value);
} }
@ -183,8 +179,8 @@ export default class GMApi {
} }
@PermissionVerify.API() @PermissionVerify.API()
GM_registerMenuCommand(request: Request, con: GetSender) { GM_registerMenuCommand(request: Request, sender: GetSender) {
console.log("registerMenuCommand", request.params); console.log("registerMenuCommand", request.params, sender.getSender(), sender.getSender().tab!.id!);
const [id, name, accessKey] = request.params; const [id, name, accessKey] = request.params;
// 触发菜单注册, 在popup中处理 // 触发菜单注册, 在popup中处理
this.mq.emit("registerMenuCommand", { this.mq.emit("registerMenuCommand", {
@ -192,24 +188,20 @@ export default class GMApi {
id: id, id: id,
name: name, name: name,
accessKey: accessKey, accessKey: accessKey,
con: con.getConnect(), tabId: sender.getSender().tab!.id!,
}); frameId: sender.getSender().frameId,
con.getConnect().onDisconnect(() => {
// 取消注册
this.mq.emit("unregisterMenuCommand", {
uuid: request.script.uuid,
name: name,
});
}); });
} }
@PermissionVerify.API() @PermissionVerify.API()
GM_unregisterMenuCommand(request: Request) { GM_unregisterMenuCommand(request: Request, sender: GetSender) {
const [id] = request.params; const [id] = request.params;
// 触发菜单取消注册, 在popup中处理 // 触发菜单取消注册, 在popup中处理
this.mq.emit("unregisterMenuCommand", { this.mq.emit("unregisterMenuCommand", {
uuid: request.script.uuid, uuid: request.script.uuid,
id: id, id: id,
tabId: sender.getSender().tab!.id!,
frameId: sender.getSender().frameId,
}); });
} }

View File

@ -1,26 +1,279 @@
import { MessageQueue } from "@Packages/message/message_queue"; import { MessageQueue } from "@Packages/message/message_queue";
import { GetSender, Group } from "@Packages/message/server"; import { Group } from "@Packages/message/server";
import { RuntimeService } from "./runtime"; 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,
} from "@App/app/repo/scripts";
import {
ScriptMenuRegisterCallbackValue,
subscribeScriptDelete,
subscribeScriptEnable,
subscribeScriptInstall,
subscribeScriptMenuRegister,
subscribeScriptRunStatus,
} from "../queue";
export type ScriptMenuItem = {
id: number;
name: string;
accessKey?: string;
tabId: number | "background";
frameId: number;
};
export type ScriptMenu = {
uuid: string; // 脚本uuid
name: string; // 脚本名称
enable: boolean; // 脚本是否启用
updatetime: number; // 脚本更新时间
hasUserConfig: boolean; // 是否有用户配置
metadata: Metadata; // 脚本元数据
runStatus?: SCRIPT_RUN_STATUS; // 脚本运行状态
runNum: number; // 脚本运行次数
runNumByIframe: number; // iframe运行次数
menus: ScriptMenuItem[]; // 脚本菜单
customExclude: string[]; // 自定义排除
};
// 处理popup页面的数据 // 处理popup页面的数据
export class PopupService { export class PopupService {
scriptDAO = new ScriptDAO();
constructor( constructor(
private group: Group, private group: Group,
private mq: MessageQueue, private mq: MessageQueue,
private runtime: RuntimeService private runtime: RuntimeService
) {} ) {}
registerMenuCommand(message: { uuid: string; id: string; name: string; accessKey: string; con: GetSender }) { async registerMenuCommand(message: ScriptMenuRegisterCallbackValue) {
console.log("registerMenuCommand", message); // 给脚本添加菜单
const data = await this.getScriptMenu(message.tabId);
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,
});
} else {
menu.name = message.name;
menu.accessKey = message.accessKey;
menu.tabId = message.tabId;
}
}
console.log(data);
Cache.getInstance().set("tabScript:" + message.tabId, data);
} }
unregisterMenuCommand(message: { id: string }) { async unregisterMenuCommand({ id, uuid, tabId }: { id: number; uuid: string; tabId: number }) {
console.log("unregisterMenuCommand", message); const data = await this.getScriptMenu(tabId);
// 删除脚本菜单
const script = data.find((item) => item.uuid === uuid);
if (script) {
script.menus = script.menus.filter((item) => item.id !== id);
}
Cache.getInstance().set("tabScript:" + tabId, data);
}
scriptToMenu(script: Script): ScriptMenu {
return {
uuid: script.uuid,
name: script.name,
enable: script.status === SCRIPT_STATUS_ENABLE,
updatetime: script.updatetime || 0,
hasUserConfig: !!script.config,
metadata: script.metadata,
runStatus: script.runStatus,
runNum: 0,
runNumByIframe: 0,
menus: [],
customExclude: (script as ScriptMatchInfo).customizeExcludeMatches || [],
};
}
// 获取popup页面数据
async getPopupData(req: GetPopupDataReq): Promise<GetPopupDataRes> {
// 获取当前tabId
const scriptUuid = await this.runtime.getPageScriptByUrl(req.url);
// 与运行时脚本进行合并
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 => {
return this.scriptToMenu(script);
});
runScript.push(...scriptList);
// 后台脚本只显示开启或者运行中的脚本
return { scriptList: runScript, backScriptList: await this.getScriptMenu(-1) };
}
async getScriptMenu(tabId: number) {
return ((await Cache.getInstance().get("tabScript:" + tabId)) || []) as ScriptMenu[];
}
async addScriptRunNumber({
tabId,
frameId,
scripts,
}: {
tabId: number;
frameId: number;
scripts: ScriptMatchInfo[];
}) {
if (frameId === undefined) {
// 清理数据
await Cache.getInstance().del("tabScript:" + tabId);
}
// 设置数据
const data = await this.getScriptMenu(tabId);
// 设置脚本运行次数
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);
}
});
Cache.getInstance().set("tabScript:" + tabId, data);
}
dealBackgroundScriptInstall() {
// 处理后台脚本
subscribeScriptInstall(this.mq, async ({ script }) => {
if (script.type === SCRIPT_TYPE_NORMAL) {
return;
}
const menu = await this.getScriptMenu(-1);
const scriptMenu = menu.find((item) => item.uuid === script.uuid);
if (script.status === SCRIPT_STATUS_ENABLE) {
// 加入菜单
if (!scriptMenu) {
const item = this.scriptToMenu(script);
menu.push(item);
}
} else {
// 移出菜单
if (scriptMenu) {
menu.splice(menu.indexOf(scriptMenu), 1);
}
}
Cache.getInstance().set("tabScript:" + -1, menu);
});
subscribeScriptEnable(this.mq, async ({ uuid }) => {
const script = await this.scriptDAO.get(uuid);
if (!script) {
return;
}
if (script.type === SCRIPT_TYPE_NORMAL) {
return;
}
const menu = await this.getScriptMenu(-1);
const scriptMenu = menu.find((item) => item.uuid === uuid);
if (script.status === SCRIPT_STATUS_ENABLE) {
// 加入菜单
if (!scriptMenu) {
const item = this.scriptToMenu(script);
menu.push(item);
}
} else {
// 移出菜单
if (scriptMenu) {
menu.splice(menu.indexOf(scriptMenu), 1);
}
}
Cache.getInstance().set("tabScript:" + -1, menu);
});
subscribeScriptDelete(this.mq, async ({ uuid }) => {
const menu = await this.getScriptMenu(-1);
const scriptMenu = menu.find((item) => item.uuid === uuid);
if (scriptMenu) {
menu.splice(menu.indexOf(scriptMenu), 1);
Cache.getInstance().set("tabScript:" + -1, menu);
}
});
subscribeScriptRunStatus(this.mq, async ({ uuid, runStatus }) => {
const menu = await this.getScriptMenu(-1);
const scriptMenu = menu.find((item) => item.uuid === uuid);
if (scriptMenu) {
scriptMenu.runStatus = runStatus;
Cache.getInstance().set("tabScript:" + -1, menu);
}
});
}
menuClick({ uuid, id, tabId, frameId }: { uuid: string; id: number; tabId: number; frameId: number }) {
// 菜单点击事件
console.log("click menu", uuid, id, tabId);
this.runtime.sendMessageToTab(tabId, "menuClick", {
uuid,
id,
tabId,
frameId,
});
return Promise.resolve(true);
} }
init() { init() {
// 处理脚本菜单数据 // 处理脚本菜单数据
this.mq.subscribe("registerMenuCommand", this.registerMenuCommand.bind(this)); subscribeScriptMenuRegister(this.mq, this.registerMenuCommand.bind(this));
this.mq.subscribe("unregisterMenuCommand", this.unregisterMenuCommand.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) => {
// 清理数据
Cache.getInstance().del("tabScript:" + tabId);
});
// 监听运行次数
this.mq.subscribe(
"pageLoad",
async ({ tabId, frameId, scripts }: { tabId: number; frameId: number; 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,
});
}
}
);
}
);
} }
} }

View File

@ -1,6 +1,14 @@
import { MessageQueue } from "@Packages/message/message_queue"; import { MessageQueue } from "@Packages/message/message_queue";
import { GetSender, Group, MessageSend } from "@Packages/message/server"; import { GetSender, Group, MessageSend } from "@Packages/message/server";
import { Script, SCRIPT_STATUS_ENABLE, SCRIPT_TYPE_NORMAL, ScriptDAO, ScriptRunResouce } from "@App/app/repo/scripts"; import {
Script,
SCRIPT_STATUS,
SCRIPT_STATUS_DISABLE,
SCRIPT_STATUS_ENABLE,
SCRIPT_TYPE_NORMAL,
ScriptDAO,
ScriptRunResouce,
} from "@App/app/repo/scripts";
import { ValueService } from "./value"; import { ValueService } from "./value";
import GMApi from "./gm_api"; import GMApi from "./gm_api";
import { subscribeScriptDelete, subscribeScriptEnable, subscribeScriptInstall } from "../queue"; import { subscribeScriptDelete, subscribeScriptEnable, subscribeScriptInstall } from "../queue";
@ -11,17 +19,21 @@ import { randomString } from "@App/pkg/utils/utils";
import { compileInjectScript } from "@App/runtime/content/utils"; import { compileInjectScript } from "@App/runtime/content/utils";
import Cache from "@App/app/cache"; import Cache from "@App/app/cache";
import { dealPatternMatches, UrlMatch } from "@App/pkg/utils/match"; import { dealPatternMatches, UrlMatch } from "@App/pkg/utils/match";
import { ExtensionContentMessageSend } from "@Packages/message/extension_message";
import { sendMessage } from "@Packages/message/client";
// 为了优化性能存储到缓存时删除了code与value // 为了优化性能存储到缓存时删除了code与value
export interface ScriptMatchInfo extends ScriptRunResouce { export interface ScriptMatchInfo extends ScriptRunResouce {
matches: string[]; matches: string[];
excludeMatches: string[]; excludeMatches: string[];
customizeExcludeMatches: string[];
} }
export class RuntimeService { export class RuntimeService {
scriptDAO: ScriptDAO = new ScriptDAO(); scriptDAO: ScriptDAO = new ScriptDAO();
scriptMatch: UrlMatch<string> = new UrlMatch<string>(); scriptMatch: UrlMatch<string> = new UrlMatch<string>();
scriptCustomizeMatch: UrlMatch<string> = new UrlMatch<string>();
scriptMatchCache: Map<string, ScriptMatchInfo> | null | undefined; scriptMatchCache: Map<string, ScriptMatchInfo> | null | undefined;
constructor( constructor(
@ -52,11 +64,10 @@ export class RuntimeService {
// 如果是普通脚本, 在service worker中进行注册 // 如果是普通脚本, 在service worker中进行注册
// 如果是后台脚本, 在offscreen中进行处理 // 如果是后台脚本, 在offscreen中进行处理
if (script.type === SCRIPT_TYPE_NORMAL) { if (script.type === SCRIPT_TYPE_NORMAL) {
// 注册入页面脚本 // 加载页面脚本
if (data.enable) { await this.loadPageScript(script);
this.registryPageScript(script); if (!data.enable) {
} else { await this.unregistryPageScript(script);
this.unregistryPageScript(script);
} }
} }
}); });
@ -67,7 +78,7 @@ export class RuntimeService {
return; return;
} }
if (script.type === SCRIPT_TYPE_NORMAL) { if (script.type === SCRIPT_TYPE_NORMAL) {
this.registryPageScript(script); await this.loadPageScript(script);
} }
}); });
// 监听脚本删除 // 监听脚本删除
@ -77,7 +88,8 @@ export class RuntimeService {
return; return;
} }
if (script.type === SCRIPT_TYPE_NORMAL) { if (script.type === SCRIPT_TYPE_NORMAL) {
this.unregistryPageScript(script); await this.unregistryPageScript(script);
this.deleteScriptMatch(script.uuid);
} }
}); });
@ -85,20 +97,22 @@ export class RuntimeService {
const scriptDao = new ScriptDAO(); const scriptDao = new ScriptDAO();
const list = await scriptDao.all(); const list = await scriptDao.all();
list.forEach((script) => { list.forEach((script) => {
if (script.status !== SCRIPT_STATUS_ENABLE || script.type !== SCRIPT_TYPE_NORMAL) { if (script.type !== SCRIPT_TYPE_NORMAL) {
return; return;
} }
this.mq.publish("enableScript", { uuid: script.uuid, enable: true }); this.mq.publish("enableScript", { uuid: script.uuid, enable: script.status === SCRIPT_STATUS_ENABLE });
}); });
// 监听offscreen环境初始化, 初始化完成后, 再将后台脚本运行起来 // 监听offscreen环境初始化, 初始化完成后, 再将后台脚本运行起来
this.mq.subscribe("preparationOffscreen", () => { this.mq.subscribe("preparationOffscreen", () => {
list.forEach((script) => { list.forEach((script) => {
if (script.status !== SCRIPT_STATUS_ENABLE || script.type === SCRIPT_TYPE_NORMAL) { if (script.type === SCRIPT_TYPE_NORMAL) {
return; return;
} }
this.mq.publish("enableScript", { uuid: script.uuid, enable: true }); this.mq.publish("enableScript", { uuid: script.uuid, enable: script.status === SCRIPT_STATUS_ENABLE });
}); });
}); });
this.loadScriptMatchInfo();
} }
messageFlag() { messageFlag() {
@ -107,23 +121,72 @@ export class RuntimeService {
}); });
} }
async pageLoad(_, sender: GetSender) { // 给指定tab发送消息
const [scriptFlag, match] = await Promise.all([this.messageFlag(), this.loadScriptMatchInfo()]); sendMessageToTab(tabId: number, action: string, data: any) {
if (tabId === -1) {
// 如果是-1, 代表给offscreen发送消息
return sendMessage(this.sender, "offscreen/runtime/" + action, data);
}
return sendMessage(new ExtensionContentMessageSend(tabId), "content/runtime/" + action, data);
}
async getPageScriptUuidByUrl(url: string) {
const match = await this.loadScriptMatchInfo();
// 匹配当前页面的脚本
const matchScriptUuid = match.match(url!);
// 排除自定义匹配
const excludeScriptUuid = this.scriptCustomizeMatch.match(url!);
const excludeMatch = new Set<string>();
excludeScriptUuid.forEach((uuid) => {
excludeMatch.add(uuid);
});
return matchScriptUuid.filter((value) => {
// 过滤掉自定义排除的脚本
return !excludeMatch.has(value);
});
}
async getPageScriptByUrl(url: string) {
const matchScriptUuid = await this.getPageScriptUuidByUrl(url);
return matchScriptUuid.map((uuid) => {
return Object.assign({}, this.scriptMatchCache?.get(uuid));
});
}
async pageLoad(_: any, sender: GetSender) {
const [scriptFlag] = await Promise.all([this.messageFlag(), this.loadScriptMatchInfo()]);
const chromeSender = sender.getSender() as chrome.runtime.MessageSender; const chromeSender = sender.getSender() as chrome.runtime.MessageSender;
// 匹配当前页面的脚本 // 匹配当前页面的脚本
const matchScriptUuid = match.match(chromeSender.url!); const matchScriptUuid = await this.getPageScriptUuidByUrl(chromeSender.url!);
const scripts = await Promise.all( const scripts = await Promise.all(
matchScriptUuid.map( matchScriptUuid.map(async (uuid): Promise<undefined | ScriptRunResouce> => {
(uuid) => const scriptRes = Object.assign({}, this.scriptMatchCache?.get(uuid));
new Promise((resolve) => { // 判断脚本是否开启
// 获取value if (scriptRes.status === SCRIPT_STATUS_DISABLE) {
const scriptRes = Object.assign({}, this.scriptMatchCache?.get(uuid)); return undefined;
resolve(scriptRes); }
}) // 如果是iframe,判断是否允许在iframe里运行
) if (chromeSender.frameId !== undefined) {
if (scriptRes.metadata.noframes) {
return undefined;
}
}
// 获取value
return scriptRes;
})
); );
return Promise.resolve({ flag: scriptFlag, scripts: scripts });
const enableScript = scripts.filter((item) => item);
this.mq.emit("pageLoad", {
tabId: chromeSender.tab?.id,
frameId: chromeSender.frameId,
scripts: enableScript,
});
return Promise.resolve({ flag: scriptFlag, scripts: enableScript });
} }
// 停止脚本 // 停止脚本
@ -174,7 +237,7 @@ export class RuntimeService {
}); });
} }
loadScripting: Promise<void> | null | undefined; loadingScript: Promise<void> | null | undefined;
// 加载脚本匹配信息由于service_worker的机制如果由不活动状态恢复过来时会优先触发事件 // 加载脚本匹配信息由于service_worker的机制如果由不活动状态恢复过来时会优先触发事件
// 可能当时会没有脚本匹配信息,所以使用脚本信息时,尽量使用此方法获取 // 可能当时会没有脚本匹配信息,所以使用脚本信息时,尽量使用此方法获取
@ -182,29 +245,33 @@ export class RuntimeService {
if (this.scriptMatchCache) { if (this.scriptMatchCache) {
return this.scriptMatch; return this.scriptMatch;
} }
if (this.loadScripting) { if (this.loadingScript) {
await this.loadScripting; await this.loadingScript;
} else { } else {
// 如果没有缓存, 则创建一个新的缓存 // 如果没有缓存, 则创建一个新的缓存
this.loadScripting = Cache.getInstance() const cache = new Map<string, ScriptMatchInfo>();
this.loadingScript = Cache.getInstance()
.get("scriptMatch") .get("scriptMatch")
.then((data: { [key: string]: ScriptMatchInfo }) => { .then((data: { [key: string]: ScriptMatchInfo }) => {
this.scriptMatchCache = new Map<string, ScriptMatchInfo>();
if (data) { if (data) {
Object.keys(data).forEach((key) => { Object.keys(data).forEach((key) => {
const item = data[key]; const item = data[key];
this.scriptMatchCache!.set(item.uuid, item); cache.set(item.uuid, item);
item.matches.forEach((match) => { item.matches.forEach((match) => {
this.scriptMatch.add(match, item.uuid); this.scriptMatch.add(match, item.uuid);
}); });
item.excludeMatches.forEach((match) => { item.excludeMatches.forEach((match) => {
this.scriptMatch.exclude(match, item.uuid); this.scriptMatch.exclude(match, item.uuid);
}); });
item.customizeExcludeMatches.forEach((match) => {
this.scriptCustomizeMatch.exclude(match, item.uuid);
});
}); });
} }
}); });
await this.loadScripting; await this.loadingScript;
this.loadScripting = null; this.loadingScript = null;
this.scriptMatchCache = cache;
} }
return this.scriptMatch; return this.scriptMatch;
} }
@ -229,28 +296,43 @@ export class RuntimeService {
await this.loadScriptMatchInfo(); await this.loadScriptMatchInfo();
} }
this.scriptMatchCache!.set(item.uuid, item); this.scriptMatchCache!.set(item.uuid, item);
// 清理一下老数据
this.scriptMatch.del(item.uuid);
this.scriptCustomizeMatch.del(item.uuid);
// 添加新的数据
item.matches.forEach((match) => { item.matches.forEach((match) => {
this.scriptMatch.add(match, item.uuid); this.scriptMatch.add(match, item.uuid);
}); });
item.excludeMatches.forEach((match) => { item.excludeMatches.forEach((match) => {
this.scriptMatch.exclude(match, item.uuid); this.scriptMatch.exclude(match, item.uuid);
}); });
item.customizeExcludeMatches.forEach((match) => {
this.scriptCustomizeMatch.exclude(match, item.uuid);
});
this.saveScriptMatchInfo(); this.saveScriptMatchInfo();
} }
async deleteScriptMatch(uuid: string) { async updateScriptStatus(uuid: string, status: SCRIPT_STATUS) {
if (!this.scriptMatchCache) { if (!this.scriptMatchCache) {
await this.loadScriptMatchInfo(); await this.loadScriptMatchInfo();
} }
this.scriptMatchCache!.delete(uuid); this.scriptMatchCache!.get(uuid)!.status = status;
this.scriptMatch.del(uuid);
this.saveScriptMatchInfo(); this.saveScriptMatchInfo();
} }
async registryPageScript(script: Script) { deleteScriptMatch(uuid: string) {
if (await Cache.getInstance().has("registryScript:" + script.uuid)) { if (!this.scriptMatchCache) {
return; return;
} }
this.scriptMatchCache.delete(uuid);
this.scriptMatch.del(uuid);
this.scriptCustomizeMatch.del(uuid);
this.saveScriptMatchInfo();
}
// 加载页面脚本, 会把脚本信息放入缓存中
// 如果脚本开启, 则注册脚本
async loadPageScript(script: Script) {
const matches = script.metadata["match"]; const matches = script.metadata["match"];
if (!matches) { if (!matches) {
return; return;
@ -262,7 +344,7 @@ export class RuntimeService {
matches.push(...(script.metadata["include"] || [])); matches.push(...(script.metadata["include"] || []));
const patternMatches = dealPatternMatches(matches); const patternMatches = dealPatternMatches(matches);
const scriptMatchInfo: ScriptMatchInfo = Object.assign( const scriptMatchInfo: ScriptMatchInfo = Object.assign(
{ matches: patternMatches.result, excludeMatches: [] }, { matches: patternMatches.result, excludeMatches: [], customizeExcludeMatches: [] },
scriptRes scriptRes
); );
@ -272,30 +354,46 @@ export class RuntimeService {
matches: patternMatches.patternResult, matches: patternMatches.patternResult,
world: "MAIN", world: "MAIN",
}; };
if (!script.metadata["noframes"]) {
registerScript.allFrames = true;
}
if (script.metadata["exclude-match"]) { if (script.metadata["exclude"]) {
const excludeMatches = script.metadata["exclude-match"]; const excludeMatches = script.metadata["exclude"];
excludeMatches.push(...(script.metadata["exclude"] || []));
const result = dealPatternMatches(excludeMatches); const result = dealPatternMatches(excludeMatches);
registerScript.excludeMatches = result.patternResult; registerScript.excludeMatches = result.patternResult;
scriptMatchInfo.excludeMatches = result.result; scriptMatchInfo.excludeMatches = result.result;
} }
if (script.metadata["run-at"]) { // 自定义排除
registerScript.runAt = getRunAt(script.metadata["run-at"]); if (script.selfMetadata && script.selfMetadata.exclude) {
const excludeMatches = script.selfMetadata.exclude;
const result = dealPatternMatches(excludeMatches);
if (!registerScript.excludeMatches) {
registerScript.excludeMatches = [];
}
registerScript.excludeMatches.push(...result.patternResult);
scriptMatchInfo.customizeExcludeMatches = result.result;
}
// 将脚本match信息放入缓存中
this.addScriptMatch(scriptMatchInfo);
// 如果脚本开启, 则注册脚本
if (script.status === SCRIPT_STATUS_ENABLE) {
if (!script.metadata["noframes"]) {
registerScript.allFrames = true;
}
if (script.metadata["run-at"]) {
registerScript.runAt = getRunAt(script.metadata["run-at"]);
}
await chrome.userScripts.register([registerScript]);
await Cache.getInstance().set("registryScript:" + script.uuid, true);
} }
chrome.userScripts.register([registerScript], async () => {
// 标记为已注册
Cache.getInstance().set("registryScript:" + script.uuid, true);
// 将脚本match信息放入缓存中
this.addScriptMatch(scriptMatchInfo);
});
} }
unregistryPageScript(script: Script) { async unregistryPageScript(script: Script) {
if (!(await Cache.getInstance().get("registryScript:" + script.uuid))) {
return;
}
chrome.userScripts.unregister( chrome.userScripts.unregister(
{ {
ids: [script.uuid], ids: [script.uuid],
@ -303,6 +401,8 @@ export class RuntimeService {
() => { () => {
// 删除缓存 // 删除缓存
Cache.getInstance().del("registryScript:" + script.uuid); Cache.getInstance().del("registryScript:" + script.uuid);
// 修改脚本状态为disable
this.updateScriptStatus(script.uuid, SCRIPT_STATUS_DISABLE);
} }
); );
} }

View File

@ -17,3 +17,45 @@ export function getRunAt(runAts: string[]): chrome.userScripts.RunAt {
} }
return "document_idle"; return "document_idle";
} }
export function mapToObject(map: Map<string, any>): { [key: string]: any } {
const obj: { [key: string]: any } = {};
map.forEach((value, key) => {
if (value instanceof Map) {
obj[key] = mapToObject(value);
} else if (obj[key] instanceof Array) {
obj[key].push(value);
} else {
obj[key] = value;
}
});
return obj;
}
export function objectToMap(obj: { [key: string]: any }): Map<string, any> {
const map = new Map<string, any>();
Object.keys(obj).forEach((key) => {
if (obj[key] instanceof Map) {
map.set(key, objectToMap(obj[key]));
} else if (obj[key] instanceof Array) {
map.set(key, obj[key]);
} else {
map.set(key, obj[key]);
}
});
return map;
}
export function arrayToObject(arr: Array<any>): any[] {
const obj: any[] = [];
arr.forEach((item) => {
if (item instanceof Map) {
obj.push(mapToObject(item));
} else if (item instanceof Array) {
obj.push(arrayToObject(item));
} else {
obj.push(item);
}
});
return obj;
}

View File

@ -1,6 +1,6 @@
import LoggerCore from "./app/logger/core"; import LoggerCore from "./app/logger/core";
import MessageWriter from "./app/logger/message_writer"; import MessageWriter from "./app/logger/message_writer";
import { ExtensionMessageSend } from "@Packages/message/extension_message"; import { ExtensionMessage, ExtensionMessageSend } from "@Packages/message/extension_message";
import { CustomEventMessage } from "@Packages/message/custom_event_message"; import { CustomEventMessage } from "@Packages/message/custom_event_message";
import { RuntimeClient } from "./app/service/service_worker/client"; import { RuntimeClient } from "./app/service/service_worker/client";
import ContentRuntime from "./runtime/content/content"; import ContentRuntime from "./runtime/content/content";
@ -18,9 +18,11 @@ const loggerCore = new LoggerCore({
const client = new RuntimeClient(send); const client = new RuntimeClient(send);
client.pageLoad().then((data) => { client.pageLoad().then((data) => {
loggerCore.logger().debug("content start"); loggerCore.logger().debug("content start");
const extMsg = new ExtensionMessage();
const msg = new CustomEventMessage(data.flag, true); const msg = new CustomEventMessage(data.flag, true);
const server = new Server("content", msg); const server = new Server("content", msg);
const extServer = new Server("content", extMsg);
// 初始化运行环境 // 初始化运行环境
const runtime = new ContentRuntime(server, send, msg); const runtime = new ContentRuntime(extServer, server, send, msg);
runtime.start(data.scripts); runtime.start(data.scripts);
}); });

View File

@ -23,6 +23,7 @@
"default_locale": "zh_CN", "default_locale": "zh_CN",
"permissions": [ "permissions": [
"tabs", "tabs",
"action",
"storage", "storage",
"offscreen", "offscreen",
"scripting", "scripting",

View File

@ -1,15 +1,5 @@
/* eslint-disable no-nested-ternary */
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { ScriptMenu } from "@App/runtime/service_worker/runtime"; import { Button, Collapse, Empty, Message, Popconfirm, Space, Switch } from "@arco-design/web-react";
import {
Button,
Collapse,
Empty,
Message,
Popconfirm,
Space,
Switch,
} from "@arco-design/web-react";
import { import {
IconCaretDown, IconCaretDown,
IconCaretUp, IconCaretUp,
@ -23,6 +13,11 @@ import { SCRIPT_RUN_STATUS_RUNNING } from "@App/app/repo/scripts";
import { RiPlayFill, RiStopFill } from "react-icons/ri"; import { RiPlayFill, RiStopFill } from "react-icons/ri";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ScriptIcons } from "@App/pages/options/routes/utils"; 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";
const CollapseItem = Collapse.Item; const CollapseItem = Collapse.Item;
@ -49,12 +44,13 @@ const ScriptMenuList: React.FC<{
[key: string]: boolean; [key: string]: boolean;
}>({}); }>({});
const { t } = useTranslation(); const { t } = useTranslation();
const menuExpandNum = useAppSelector(selectMenuExpandNum);
let url: URL; let url: URL;
try { try {
url = new URL(currentUrl); url = new URL(currentUrl);
} catch (e) { } catch (e: any) {
// ignore error console.error("Invalid URL:", e);
} }
useEffect(() => { useEffect(() => {
setList(script); setList(script);
@ -78,20 +74,10 @@ const ScriptMenuList: React.FC<{
// }; // };
// }, []); // }, []);
const sendMenuAction = (sender: MessageSender, channelFlag: string) => { const sendMenuAction = (uuid: string, menu: ScriptMenuItem) => {
let id = sender.tabId; popupClient.menuClick(uuid, menu).then(() => {
if (sender.frameId) { window.close();
id = sender.frameId; });
}
message.broadcastChannel(
{
tag: sender.targetTag,
id: [id!],
},
channelFlag,
"click"
);
window.close();
}; };
// 监听菜单按键 // 监听菜单按键
@ -101,16 +87,14 @@ const ScriptMenuList: React.FC<{
<> <>
{list.length === 0 && <Empty />} {list.length === 0 && <Empty />}
{list.map((item, index) => ( {list.map((item, index) => (
<Collapse bordered={false} expandIconPosition="right" key={item.id}> <Collapse bordered={false} expandIconPosition="right" key={item.uuid}>
<CollapseItem <CollapseItem
header={ header={
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<div <div
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
}} }}
title={ title={
// eslint-disable-next-line no-nested-ternary
item.enable item.enable
? item.runNumByIframe ? item.runNumByIframe
? t("script_total_runs", { ? t("script_total_runs", {
@ -159,7 +143,7 @@ const ScriptMenuList: React.FC<{
</Space> </Space>
</div> </div>
} }
name={item.id.toString()} name={item.uuid}
contentStyle={{ padding: "0 0 0 40px" }} contentStyle={{ padding: "0 0 0 40px" }}
> >
<div className="flex flex-col"> <div className="flex flex-col">
@ -167,13 +151,7 @@ const ScriptMenuList: React.FC<{
<Button <Button
className="text-left" className="text-left"
type="secondary" type="secondary"
icon={ icon={item.runStatus !== SCRIPT_RUN_STATUS_RUNNING ? <RiPlayFill /> : <RiStopFill />}
item.runStatus !== SCRIPT_RUN_STATUS_RUNNING ? (
<RiPlayFill />
) : (
<RiStopFill />
)
}
onClick={() => { onClick={() => {
if (item.runStatus !== SCRIPT_RUN_STATUS_RUNNING) { if (item.runStatus !== SCRIPT_RUN_STATUS_RUNNING) {
runtimeCtrl.startScript(item.id); runtimeCtrl.startScript(item.id);
@ -182,9 +160,7 @@ const ScriptMenuList: React.FC<{
} }
}} }}
> >
{item.runStatus !== SCRIPT_RUN_STATUS_RUNNING {item.runStatus !== SCRIPT_RUN_STATUS_RUNNING ? t("run_once") : t("stop")}
? t("run_once")
: t("stop")}
</Button> </Button>
)} )}
<Button <Button
@ -192,10 +168,7 @@ const ScriptMenuList: React.FC<{
type="secondary" type="secondary"
icon={<IconEdit />} icon={<IconEdit />}
onClick={() => { onClick={() => {
window.open( window.open(`/src/options.html#/script/editor/${item.id}`, "_blank");
`/src/options.html#/script/editor/${item.id}`,
"_blank"
);
window.close(); window.close();
}} }}
> >
@ -208,20 +181,12 @@ const ScriptMenuList: React.FC<{
type="secondary" type="secondary"
icon={<IconMinus />} icon={<IconMinus />}
onClick={() => { onClick={() => {
scriptCtrl scriptCtrl.exclude(item.id, `*://${url.host}*`, isExclude(item, url.host)).finally(() => {
.exclude( window.close();
item.id, });
`*://${url.host}*`,
isExclude(item, url.host)
)
.finally(() => {
window.close();
});
}} }}
> >
{isExclude(item, url.host) {isExclude(item, url.host) ? t("exclude_on") : t("exclude_off")}
? t("exclude_on")
: t("exclude_off")}
{` ${url.host} ${t("exclude_execution")}`} {` ${url.host} ${t("exclude_execution")}`}
</Button> </Button>
)} )}
@ -235,32 +200,24 @@ const ScriptMenuList: React.FC<{
}); });
}} }}
> >
<Button <Button className="text-left" status="danger" type="secondary" icon={<IconDelete />}>
className="text-left"
status="danger"
type="secondary"
icon={<IconDelete />}
>
{t("delete")} {t("delete")}
</Button> </Button>
</Popconfirm> </Popconfirm>
</div> </div>
</CollapseItem> </CollapseItem>
<div <div className="arco-collapse-item-content-box flex flex-col" style={{ padding: "0 0 0 40px" }}>
className="arco-collapse-item-content-box flex flex-col"
style={{ padding: "0 0 0 40px" }}
>
{/* 判断菜单数量,再判断是否展开 */} {/* 判断菜单数量,再判断是否展开 */}
{(item.menus && item.menus?.length > systemConfig.menuExpandNum {(item.menus.length > menuExpandNum
? expandMenuIndex[index] ? expandMenuIndex[index]
? item.menus ? item.menus
: item.menus?.slice(0, systemConfig.menuExpandNum) : item.menus?.slice(0, menuExpandNum)
: item.menus : item.menus
)?.map((menu) => { )?.map((menu) => {
if (menu.accessKey) { if (menu.accessKey) {
document.addEventListener("keypress", (e) => { document.addEventListener("keypress", (e) => {
if (e.key.toUpperCase() === menu.accessKey!.toUpperCase()) { if (e.key.toUpperCase() === menu.accessKey!.toUpperCase()) {
sendMenuAction(menu.sender, menu.channelFlag); sendMenuAction(item.uuid, menu);
} }
}); });
} }
@ -271,7 +228,7 @@ const ScriptMenuList: React.FC<{
type="secondary" type="secondary"
icon={<IconMenu />} icon={<IconMenu />}
onClick={() => { onClick={() => {
sendMenuAction(menu.sender, menu.channelFlag); sendMenuAction(item.uuid, menu);
}} }}
> >
{menu.name} {menu.name}
@ -279,14 +236,12 @@ const ScriptMenuList: React.FC<{
</Button> </Button>
); );
})} })}
{item.menus && item.menus?.length > systemConfig.menuExpandNum && ( {item.menus.length > menuExpandNum && (
<Button <Button
className="text-left" className="text-left"
key="expand" key="expand"
type="secondary" type="secondary"
icon={ icon={expandMenuIndex[index] ? <IconCaretUp /> : <IconCaretDown />}
expandMenuIndex[index] ? <IconCaretUp /> : <IconCaretDown />
}
onClick={() => { onClick={() => {
setExpandMenuIndex({ setExpandMenuIndex({
...expandMenuIndex, ...expandMenuIndex,
@ -304,10 +259,7 @@ const ScriptMenuList: React.FC<{
type="secondary" type="secondary"
icon={<IconSettings />} icon={<IconSettings />}
onClick={() => { onClick={() => {
window.open( window.open(`/src/options.html#/?userConfig=${item.id}`, "_blank");
`/src/options.html#/?userConfig=${item.id}`,
"_blank"
);
window.close(); window.close();
}} }}
> >

View File

@ -15,7 +15,8 @@ import { RiMessage2Line } from "react-icons/ri";
import semver from "semver"; import semver from "semver";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import ScriptMenuList from "../components/ScriptMenuList"; import ScriptMenuList from "../components/ScriptMenuList";
import { ScriptMenu } from "@App/runtime/service_worker/runtime"; import { popupClient } from "../store/features/script";
import { ScriptMenu } from "@App/app/service/service_worker/popup";
const CollapseItem = Collapse.Item; const CollapseItem = Collapse.Item;
@ -43,41 +44,39 @@ function App() {
// ignore error // ignore error
} }
// const message = IoC.instance(MessageInternal) as MessageInternal; useEffect(() => {
// useEffect(() => { // systemManage.getNotice().then((res) => {
// systemManage.getNotice().then((res) => { // if (res) {
// if (res) { // setNotice(res.notice);
// setNotice(res.notice); // setIsRead(res.isRead);
// setIsRead(res.isRead); // }
// } // });
// }); // systemManage.getVersion().then((res) => {
// systemManage.getVersion().then((res) => { // res && setVersion(res);
// res && setVersion(res); // });
// }); chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
// chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { if (!tabs.length) {
// if (!tabs.length) { return;
// return; }
// } setCurrentUrl(tabs[0].url || "");
// setCurrentUrl(tabs[0].url || ""); popupClient.getPopupData({ url: tabs[0].url!, tabId: tabs[0].id! }).then((resp) => {
// message console.log(resp);
// .syncSend("queryPageScript", { url: tabs[0].url, tabId: tabs[0].id }) // 按照开启状态和更新时间排序
// .then((resp: { scriptList: ScriptMenu[]; backScriptList: ScriptMenu[] }) => { const list = resp.scriptList;
// // 按照开启状态和更新时间排序 list.sort((a, b) => {
// const list = resp.scriptList; if (a.enable === b.enable) {
// list.sort((a, b) => { if (a.runNum !== b.runNum) {
// if (a.enable === b.enable) { return b.runNum - a.runNum;
// if (a.runNum !== b.runNum) { }
// return b.runNum - a.runNum; return b.updatetime - a.updatetime;
// } }
// return b.updatetime - a.updatetime; return a.enable ? -1 : 1;
// } });
// return a.enable ? -1 : 1; setScriptList(list);
// }); setBackScriptList(resp.backScriptList);
// setScriptList(list); });
// setBackScriptList(resp.backScriptList); });
// }); }, []);
// });
// }, []);
return ( return (
<Card <Card
size="small" size="small"
@ -178,7 +177,6 @@ function App() {
<Alert <Alert
style={{ marginBottom: 20, display: showAlert ? "flex" : "none" }} style={{ marginBottom: 20, display: showAlert ? "flex" : "none" }}
type="info" type="info"
// eslint-disable-next-line react/no-danger
content={<div dangerouslySetInnerHTML={{ __html: notice }} />} content={<div dangerouslySetInnerHTML={{ __html: notice }} />}
/> />
<Collapse bordered={false} defaultActiveKey={["script", "background"]} style={{ maxWidth: 640 }}> <Collapse bordered={false} defaultActiveKey={["script", "background"]} style={{ maxWidth: 640 }}>

View File

@ -8,11 +8,12 @@ import {
ScriptDAO, ScriptDAO,
} from "@App/app/repo/scripts"; } from "@App/app/repo/scripts";
import { arrayMove } from "@dnd-kit/sortable"; import { arrayMove } from "@dnd-kit/sortable";
import { RuntimeClient, ScriptClient } from "@App/app/service/service_worker/client"; import { PopupClient, RuntimeClient, ScriptClient } from "@App/app/service/service_worker/client";
import { message } from "../global"; import { message } from "../global";
export const scriptClient = new ScriptClient(message); export const scriptClient = new ScriptClient(message);
export const runtimeClient = new RuntimeClient(message); export const runtimeClient = new RuntimeClient(message);
export const popupClient = new PopupClient(message);
export const fetchAndSortScriptList = createAsyncThunk("script/fetchScriptList", async () => { export const fetchAndSortScriptList = createAsyncThunk("script/fetchScriptList", async () => {
// 排序 // 排序

View File

@ -11,6 +11,7 @@ export const settingSlice = createAppSlice({
config: "", config: "",
}, },
scriptListColumnWidth: {} as { [key: string]: number }, scriptListColumnWidth: {} as { [key: string]: number },
menuExpandNum: 5,
}, },
reducers: (create) => { reducers: (create) => {
// 初始化黑夜模式 // 初始化黑夜模式
@ -42,14 +43,18 @@ export const settingSlice = createAppSlice({
editor.setTheme(action.payload === "dark" ? "vs-dark" : "vs"); editor.setTheme(action.payload === "dark" ? "vs-dark" : "vs");
} }
}), }),
menuExpandNum: create.reducer((state, action: PayloadAction<number>) => {
state.menuExpandNum = action.payload;
}),
}; };
}, },
selectors: { selectors: {
selectThemeMode: (state) => state.lightMode, selectThemeMode: (state) => state.lightMode,
selectScriptListColumnWidth: (state) => state.scriptListColumnWidth, selectScriptListColumnWidth: (state) => state.scriptListColumnWidth,
selectMenuExpandNum: (state) => state.menuExpandNum,
}, },
}); });
export const { setDarkMode } = settingSlice.actions; export const { setDarkMode } = settingSlice.actions;
export const { selectThemeMode, selectScriptListColumnWidth } = settingSlice.selectors; export const { selectThemeMode, selectScriptListColumnWidth, selectMenuExpandNum } = settingSlice.selectors;

View File

@ -1,158 +1,56 @@
import { ScriptRunResouce } from "@App/app/repo/scripts"; import { ScriptRunResouce } from "@App/app/repo/scripts";
import { Client } from "@Packages/message/client"; import { Client } from "@Packages/message/client";
import { forwardMessage, GetSender, Message, MessageSend, Server } from "@Packages/message/server"; import { forwardMessage, Message, MessageSend, Server } from "@Packages/message/server";
// content页的处理 // content页的处理
export default class ContentRuntime { export default class ContentRuntime {
constructor( constructor(
private extServer: Server,
private server: Server, private server: Server,
private send: MessageSend, private send: MessageSend,
private msg: Message private msg: Message
) {} ) {}
start(scripts: ScriptRunResouce[]) { start(scripts: ScriptRunResouce[]) {
forwardMessage( this.extServer.on("runtime/menuClick", (action, data) => {
"serviceWorker", // gm菜单点击
"runtime/gmApi", console.log("runtime/menuClick", action, data);
this.server, });
this.send, this.extServer.on("runtime/valueUpdate", (action, data) => {
(data: { api: string; params: any }, con: GetSender) => { // gm value变化
// 拦截关注的api console.log(action, data);
switch (data.api) { });
case "CAT_createBlobUrl": { forwardMessage("serviceWorker", "runtime/gmApi", this.server, this.send, (data: { api: string; params: any }) => {
const file = data.params[0] as File; // 拦截关注的api
const url = URL.createObjectURL(file); switch (data.api) {
setTimeout(() => { case "CAT_createBlobUrl": {
URL.revokeObjectURL(url); const file = data.params[0] as File;
}, 60 * 1000); const url = URL.createObjectURL(file);
return Promise.resolve(url); setTimeout(() => {
} URL.revokeObjectURL(url);
case "CAT_fetchBlob": { }, 60 * 1000);
return fetch(data.params[0]).then((res) => res.blob()); return Promise.resolve(url);
} }
case "CAT_fetchDocument": { case "CAT_fetchBlob": {
return new Promise((resolve) => { return fetch(data.params[0]).then((res) => res.blob());
const xhr = new XMLHttpRequest(); }
xhr.responseType = "document"; case "CAT_fetchDocument": {
xhr.open("GET", data.params[0]); return new Promise((resolve) => {
xhr.onload = () => { const xhr = new XMLHttpRequest();
resolve({ xhr.responseType = "document";
relatedTarget: xhr.response, xhr.open("GET", data.params[0]);
}); xhr.onload = () => {
}; resolve({
xhr.send(); relatedTarget: xhr.response,
}); });
} };
xhr.send();
});
} }
return Promise.resolve(false);
} }
); return Promise.resolve(false);
// 由content到background });
// 转发gmApi消息
// this.contentMessage.setHandler("gmApi", (action, data) => {
// return this.internalMessage.syncSend(action, data);
// });
// // 转发log消息
// this.contentMessage.setHandler("log", (action, data) => {
// this.internalMessage.send(action, data);
// });
// // 转发externalMessage消息
// this.contentMessage.setHandler(ExternalMessage, (action, data) => {
// return this.internalMessage.syncSend(action, data);
// });
// 处理GM_addElement
// @ts-ignore
// this.contentMessage.setHandler("GM_addElement", (action, data) => {
// const parma = data.param;
// let attr: { [x: string]: any; textContent?: any };
// let textContent = "";
// if (!parma[1]) {
// attr = {};
// } else {
// attr = { ...parma[1] };
// if (attr.textContent) {
// textContent = attr.textContent;
// delete attr.textContent;
// }
// }
// const el = <Element>document.createElement(parma[0]);
// Object.keys(attr).forEach((key) => {
// el.setAttribute(key, attr[key]);
// });
// if (textContent) {
// el.innerHTML = textContent;
// }
// let parentNode;
// if (data.relatedTarget) {
// parentNode = (<MessageContent>this.contentMessage).getAndDelRelatedTarget(data.relatedTarget);
// }
// (<Element>parentNode || document.head || document.body || document.querySelector("*")).appendChild(el);
// return {
// relatedTarget: el,
// };
// });
// // 转发长连接的gmApi消息
// this.contentMessage.setHandlerWithChannel("gmApiChannel", (inject, action, data) => {
// const background = this.internalMessage.channel();
// // 转发inject->background
// inject.setHandler((req) => {
// background.send(req.data);
// });
// inject.setCatch((err) => {
// background.throw(err);
// });
// inject.setDisChannelHandler(() => {
// background.disChannel();
// });
// // 转发background->inject
// background.setHandler((bgResp) => {
// inject.send(bgResp);
// });
// background.setCatch((err) => {
// inject.throw(err);
// });
// background.setDisChannelHandler(() => {
// inject.disChannel();
// });
// // 建立连接
// background.channel(action, data);
// });
// this.listenCATApi();
// // 由background到content
// // 转发value更新事件
// this.internalMessage.setHandler("valueUpdate", (action, data) => {
// this.contentMessage.send(action, data);
// });
// this.msg.sendMessage({ action: "pageLoad", data: { scripts } });
const client = new Client(this.msg, "inject"); const client = new Client(this.msg, "inject");
client.do("pageLoad", { scripts }); client.do("pageLoad", { scripts });
} }
listenCATApi() {
// 处理特殊的消息,不需要转发到background
this.contentMessage.setHandler("CAT_fetchBlob", (_action, data: string) => {
return fetch(data).then((res) => res.blob());
});
this.contentMessage.setHandler("CAT_createBlobUrl", (_action, data: Blob) => {
const url = URL.createObjectURL(data);
setTimeout(() => {
URL.revokeObjectURL(url);
}, 60 * 1000);
return Promise.resolve(url);
});
// 处理CAT_fetchDocument
this.contentMessage.setHandler("CAT_fetchDocument", (_action, data) => {
return new Promise((resolve) => {
const xhr = new XMLHttpRequest();
xhr.responseType = "document";
xhr.open("GET", data);
xhr.onload = () => {
resolve({
relatedTarget: xhr.response,
});
};
xhr.send();
});
});
}
} }

View File

@ -222,16 +222,17 @@ export default class GMApi {
this.menuId += 1; this.menuId += 1;
} }
const id = this.menuId; const id = this.menuId;
this.connect("GM_registerMenuCommand", [id, name, accessKey]).then((con) => { this.sendMessage("GM_registerMenuCommand", [id, name, accessKey]);
con.onMessage((data: { action: string; data: any }) => { // .then((con) => {
if (data.action === "onClick") { // con.onMessage((data: { action: string; data: any }) => {
listener(); // if (data.action === "onClick") {
} // listener();
}); // }
con.onDisconnect(() => { // });
this.menuMap?.delete(id); // con.onDisconnect(() => {
}); // this.menuMap?.delete(id);
}); // });
// });
this.menuMap.set(id, name); this.menuMap.set(id, name);
return id; return id;
} }