popup页面
Some checks failed
build / Build (push) Failing after 7s
test / Run tests (push) Failing after 8s

This commit is contained in:
2025-04-07 01:35:43 +08:00
parent 1a55bb348f
commit a7620dd7e5
14 changed files with 400 additions and 177 deletions

View File

@ -116,7 +116,6 @@ export class Group {
// 转发消息
export function forwardMessage(prefix: string, path: string, from: Server, to: MessageSend, middleware?: ApiFunction) {
from.on(path, async (params, fromCon) => {
console.log("forwardMessage", path, prefix, params);
if (middleware) {
const resp = await middleware(params, new GetSender(fromCon));
if (resp !== false) {

View File

@ -152,7 +152,7 @@ export default defineConfig({
}),
new rspack.HtmlRspackPlugin({
filename: `${dist}/ext/src/popup.html`,
template: `${src}/pages/popup/index.html`,
template: `${src}/pages/popup.html`,
inject: "head",
title: "Home - ScriptCat",
minify: true,

View File

@ -8,6 +8,7 @@ import { connect } from "@Packages/message/client";
import Cache, { incr } from "@App/app/cache";
import { unsafeHeaders } from "@App/runtime/utils";
import EventEmitter from "eventemitter3";
import { MessageQueue } from "@Packages/message/message_queue";
// GMApi,处理脚本的GM API调用请求
@ -35,6 +36,7 @@ export default class GMApi {
constructor(
private group: Group,
private send: MessageSend,
private mq: MessageQueue,
private value: ValueService
) {
this.logger = LoggerCore.logger().with({ service: "runtime/gm_api" });
@ -83,7 +85,6 @@ export default class GMApi {
async buildDNRRule(reqeustId: number, params: GMSend.XHRDetails): Promise<{ [key: string]: string }> {
// 检查是否有unsafe header,有则生成dnr规则
const headers = params.headers;
console.log(headers, !headers);
if (!headers) {
return Promise.resolve({});
}
@ -181,12 +182,42 @@ export default class GMApi {
});
}
start() {
this.group.on("gmApi", this.handlerRequest.bind(this));
@PermissionVerify.API()
GM_registerMenuCommand(request: Request, con: GetSender) {
console.log("registerMenuCommand", request.params);
const [id, name, accessKey] = request.params;
// 触发菜单注册, 在popup中处理
this.mq.emit("registerMenuCommand", {
uuid: request.script.uuid,
id: id,
name: name,
accessKey: accessKey,
con: con.getConnect(),
});
con.getConnect().onDisconnect(() => {
// 取消注册
this.mq.emit("unregisterMenuCommand", {
uuid: request.script.uuid,
name: name,
});
});
}
@PermissionVerify.API()
GM_unregisterMenuCommand(request: Request) {
const [id] = request.params;
// 触发菜单取消注册, 在popup中处理
this.mq.emit("unregisterMenuCommand", {
uuid: request.script.uuid,
id: id,
});
}
// 处理GM_xmlhttpRequest请求
handlerGmXhr() {
chrome.webRequest.onBeforeSendHeaders.addListener(
(details) => {
if (details.tabId === -1) {
console.log(details);
// 判断是否存在X-Scriptcat-GM-XHR-Request-Id
// 讲请求id与chrome.webRequest的请求id关联
if (details.requestHeaders) {
@ -228,4 +259,9 @@ export default class GMApi {
["responseHeaders", "extraHeaders"]
);
}
start() {
this.group.on("gmApi", this.handlerRequest.bind(this));
this.handlerGmXhr();
}
}

View File

@ -5,6 +5,7 @@ import { ResourceService } from "./resource";
import { ValueService } from "./value";
import { RuntimeService } from "./runtime";
import { ServiceWorkerMessageSend } from "@Packages/message/window_message";
import { PopupService } from "./popup";
export type InstallSource = "user" | "system" | "sync" | "subscribe" | "vscode";
@ -31,5 +32,7 @@ export default class ServiceWorkerManager {
script.init();
const runtime = new RuntimeService(this.api.group("runtime"), this.sender, this.mq, value, script);
runtime.init();
const popup = new PopupService(this.api.group("popup"), this.mq, runtime);
popup.init();
}
}

View File

@ -0,0 +1,26 @@
import { MessageQueue } from "@Packages/message/message_queue";
import { GetSender, Group } from "@Packages/message/server";
import { RuntimeService } from "./runtime";
// 处理popup页面的数据
export class PopupService {
constructor(
private group: Group,
private mq: MessageQueue,
private runtime: RuntimeService
) {}
registerMenuCommand(message: { uuid: string; id: string; name: string; accessKey: string; con: GetSender }) {
console.log("registerMenuCommand", message);
}
unregisterMenuCommand(message: { id: string }) {
console.log("unregisterMenuCommand", message);
}
init() {
// 处理脚本菜单数据
this.mq.subscribe("registerMenuCommand", this.registerMenuCommand.bind(this));
this.mq.subscribe("unregisterMenuCommand", this.unregisterMenuCommand.bind(this));
}
}

View File

@ -34,7 +34,7 @@ export class RuntimeService {
async init() {
// 启动gm api
const gmApi = new GMApi(this.group, this.sender, this.value);
const gmApi = new GMApi(this.group, this.sender, this.mq, this.value);
gmApi.start();
this.group.on("stopScript", this.stopScript.bind(this));
@ -238,6 +238,15 @@ export class RuntimeService {
this.saveScriptMatchInfo();
}
async deleteScriptMatch(uuid: string) {
if (!this.scriptMatchCache) {
await this.loadScriptMatchInfo();
}
this.scriptMatchCache!.delete(uuid);
this.scriptMatch.del(uuid);
this.saveScriptMatchInfo();
}
async registryPageScript(script: Script) {
if (await Cache.getInstance().has("registryScript:" + script.uuid)) {
return;

View File

@ -1,7 +1,5 @@
/* eslint-disable no-nested-ternary */
import React, { useEffect, useState } from "react";
import MessageInternal from "@App/app/message/internal";
import { MessageSender } from "@App/app/message/message";
import { ScriptMenu } from "@App/runtime/service_worker/runtime";
import {
Button,
@ -21,13 +19,9 @@ import {
IconMinus,
IconSettings,
} from "@arco-design/web-react/icon";
import IoC from "@App/app/ioc";
import ScriptController from "@App/app/service/script/controller";
import { SCRIPT_RUN_STATUS_RUNNING } from "@App/app/repo/scripts";
import { RiPlayFill, RiStopFill } from "react-icons/ri";
import RuntimeController from "@App/runtime/content/runtime";
import { useTranslation } from "react-i18next";
import { SystemConfig } from "@App/pkg/config/config";
import { ScriptIcons } from "@App/pages/options/routes/utils";
const CollapseItem = Collapse.Item;
@ -51,10 +45,6 @@ const ScriptMenuList: React.FC<{
currentUrl: string;
}> = ({ script, isBackscript, currentUrl }) => {
const [list, setList] = useState([] as ScriptMenu[]);
const message = IoC.instance(MessageInternal) as MessageInternal;
const scriptCtrl = IoC.instance(ScriptController) as ScriptController;
const runtimeCtrl = IoC.instance(RuntimeController) as RuntimeController;
const systemConfig = IoC.instance(SystemConfig) as SystemConfig;
const [expandMenuIndex, setExpandMenuIndex] = useState<{
[key: string]: boolean;
}>({});
@ -70,23 +60,23 @@ 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 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();
// };
// }, []);
const sendMenuAction = (sender: MessageSender, channelFlag: string) => {
let id = sender.tabId;

View File

@ -14,11 +14,8 @@
padding: 0;
border: 0;
width: 320px;
/* height: 500px; */
min-height: 150px;
max-height: 500px;
/* overflow-y: auto; */
/* overflow: hidden; */
}
</style>
</head>

View File

@ -1,41 +0,0 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a > .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

View File

@ -1,31 +1,221 @@
import { useState } from "react";
import reactLogo from "@App/assets/logo.png";
import "./App.css";
import { ExtVersion } from "@App/app/const";
import { Alert, Badge, Button, Card, Collapse, Dropdown, Menu, Switch } from "@arco-design/web-react";
import {
IconBook,
IconBug,
IconGithub,
IconHome,
IconMoreVertical,
IconNotification,
IconPlus,
IconSearch,
} from "@arco-design/web-react/icon";
import React, { useEffect, useState } from "react";
import { RiMessage2Line } from "react-icons/ri";
import semver from "semver";
import { useTranslation } from "react-i18next";
import ScriptMenuList from "../components/ScriptMenuList";
import { ScriptMenu } from "@App/runtime/service_worker/runtime";
const CollapseItem = Collapse.Item;
const iconStyle = {
marginRight: 8,
fontSize: 16,
transform: "translateY(1px)",
};
function App() {
const [count, setCount] = useState(0);
const [scriptList, setScriptList] = useState<ScriptMenu[]>([]);
const [backScriptList, setBackScriptList] = useState<ScriptMenu[]>([]);
const [showAlert, setShowAlert] = useState(false);
const [notice, setNotice] = useState("");
const [isRead, setIsRead] = useState(true);
const [version, setVersion] = useState(ExtVersion);
const [currentUrl, setCurrentUrl] = useState("");
const [isEnableScript, setIsEnableScript] = useState(localStorage.enable_script !== "false");
const { t } = useTranslation();
return (
<div className="App">
<div>
<a href="https://reactjs.org" target="_blank" rel="noreferrer">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Rspack + React3 + TypeScript</h1>
<div className="card">
<button type="button" onClick={() => setCount(count => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Rspack and React logos to learn more
</p>
</div>
);
let url: URL | undefined;
try {
url = new URL(currentUrl);
} catch (e) {
// ignore error
}
// const message = IoC.instance(MessageInternal) as MessageInternal;
// useEffect(() => {
// systemManage.getNotice().then((res) => {
// if (res) {
// setNotice(res.notice);
// setIsRead(res.isRead);
// }
// });
// systemManage.getVersion().then((res) => {
// res && setVersion(res);
// });
// chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
// if (!tabs.length) {
// return;
// }
// setCurrentUrl(tabs[0].url || "");
// message
// .syncSend("queryPageScript", { url: tabs[0].url, tabId: tabs[0].id })
// .then((resp: { scriptList: ScriptMenu[]; backScriptList: ScriptMenu[] }) => {
// // 按照开启状态和更新时间排序
// const list = resp.scriptList;
// list.sort((a, b) => {
// if (a.enable === b.enable) {
// if (a.runNum !== b.runNum) {
// return b.runNum - a.runNum;
// }
// return b.updatetime - a.updatetime;
// }
// return a.enable ? -1 : 1;
// });
// setScriptList(list);
// setBackScriptList(resp.backScriptList);
// });
// });
// }, []);
return (
<Card
size="small"
title={
<div className="flex justify-between">
<span className="text-xl">ScriptCat</span>
<div className="flex flex-row items-center">
<Switch
size="small"
checked={isEnableScript}
onChange={(val) => {
setIsEnableScript(val);
if (val) {
localStorage.enable_script = "true";
} else {
localStorage.enable_script = "false";
}
}}
/>
<Button
type="text"
icon={<IconHome />}
iconOnly
onClick={() => {
// 用a链接的方式,vivaldi竟然会直接崩溃
window.open("/src/options.html", "_blank");
}}
/>
<Badge count={isRead ? 0 : 1} dot offset={[-8, 6]}>
<Button
type="text"
icon={<IconNotification />}
iconOnly
onClick={() => {
setShowAlert(!showAlert);
setIsRead(true);
systemManage.setRead(true);
}}
/>
</Badge>
<Dropdown
droplist={
<Menu
style={{
maxHeight: "none",
}}
onClickMenuItem={async (key) => {
switch (key) {
case "newScript":
await chrome.storage.local.set({
activeTabUrl: {
url: currentUrl,
},
});
window.open("/src/options.html#/script/editor?target=initial", "_blank");
break;
default:
window.open(key, "_blank");
break;
}
}}
>
<Menu.Item key="newScript">
<IconPlus style={iconStyle} />
{t("create_script")}
</Menu.Item>
<Menu.Item key={`https://scriptcat.org/search?domain=${url && url.host}`}>
<IconSearch style={iconStyle} />
{t("get_script")}
</Menu.Item>
<Menu.Item key="https://github.com/scriptscat/scriptcat/issues">
<IconBug style={iconStyle} />
{t("report_issue")}
</Menu.Item>
<Menu.Item key="https://docs.scriptcat.org/">
<IconBook style={iconStyle} />
{t("project_docs")}
</Menu.Item>
<Menu.Item key="https://bbs.tampermonkey.net.cn/">
<RiMessage2Line style={iconStyle} />
{t("community")}
</Menu.Item>
<Menu.Item key="https://github.com/scriptscat/scriptcat">
<IconGithub style={iconStyle} />
GitHub
</Menu.Item>
</Menu>
}
trigger="click"
>
<Button type="text" icon={<IconMoreVertical />} iconOnly />
</Dropdown>
</div>
</div>
}
bodyStyle={{ padding: 0 }}
>
<Alert
style={{ marginBottom: 20, display: showAlert ? "flex" : "none" }}
type="info"
// eslint-disable-next-line react/no-danger
content={<div dangerouslySetInnerHTML={{ __html: notice }} />}
/>
<Collapse bordered={false} defaultActiveKey={["script", "background"]} style={{ maxWidth: 640 }}>
<CollapseItem
header={t("current_page_scripts")}
name="script"
style={{ padding: "0" }}
contentStyle={{ padding: "0" }}
>
<ScriptMenuList script={scriptList} isBackscript={false} currentUrl={currentUrl} />
</CollapseItem>
<CollapseItem
header={t("enabled_background_scripts")}
name="background"
style={{ padding: "0" }}
contentStyle={{ padding: "0" }}
>
<ScriptMenuList script={backScriptList} isBackscript currentUrl={currentUrl} />
</CollapseItem>
</Collapse>
<div className="flex flex-row arco-card-header !h-6">
<span className="text-[12px] font-500">{`v${ExtVersion}`}</span>
{semver.lt(ExtVersion, version) && (
<span
onClick={() => {
window.open(`https://github.com/scriptscat/scriptcat/releases/tag/v${version}`);
}}
className="text-1 font-500"
style={{ cursor: "pointer" }}
>
{t("popup.new_version_available")}
</span>
)}
</div>
</Card>
);
}
export default App;

View File

@ -1,70 +1,19 @@
:root {
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 24px;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
.arco-collapse-item-header-title {
width: 100%;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
.arco-collapse-item-header-title .arco-space {
width: 100%;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
.arco-space-item:last-child {
overflow: hidden;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
.arco-collapse {
border-bottom: 1px solid var(--color-neutral-3) !important;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
.arco-collapse-item {
border: 0;
}

View File

@ -1,10 +1,36 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
import LoggerCore from "@App/app/logger/core.ts";
import migrate from "@App/app/migrate.ts";
import { LoggerDAO } from "@App/app/repo/logger.ts";
import DBWriter from "@App/app/logger/db_writer.ts";
import "@arco-design/web-react/dist/css/arco.css";
import "@App/locales/locales";
import "@App/index.css";
import { Provider } from "react-redux";
import { store } from "../store/store.ts";
// 初始化数据库
migrate();
// 初始化日志组件
const loggerCore = new LoggerCore({
writer: new DBWriter(new LoggerDAO()),
labels: { env: "install" },
});
loggerCore.logger().debug("page start");
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>
<React.StrictMode>
<Provider store={store}>
<div
style={{
borderBottom: "1px solid var(--color-neutral-3)",
}}
>
<App />
</div>
</Provider>
</React.StrictMode>
);

View File

@ -11,20 +11,13 @@ export default class ContentRuntime {
) {}
start(scripts: ScriptRunResouce[]) {
this.msg.onMessage((msg, sendResponse) => {
console.log("content onMessage", msg);
});
this.msg.onConnect((msg, connect) => {
console.log(msg, connect);
});
forwardMessage(
"serviceWorker",
"runtime/gmApi",
this.server,
this.send,
(data: { api: string; params: any }, con: GetSender) => {
// 拦截关注的action
console.log("拦截", data);
// 拦截关注的api
switch (data.api) {
case "CAT_createBlobUrl": {
const file = data.params[0] as File;

View File

@ -198,6 +198,53 @@ export default class GMApi {
return (<CustomEventMessage>this.message).getAndDelRelatedTarget(data.relatedTarget);
}
menuId: number | undefined;
menuMap: Map<number, string> | undefined;
@GMContext.API()
GM_registerMenuCommand(name: string, listener: () => void, accessKey?: string): number {
if (!this.menuMap) {
this.menuMap = new Map();
}
let flag = 0;
this.menuMap.forEach((val, key) => {
if (val === name) {
flag = key;
}
});
if (flag) {
return flag;
}
if (!this.menuId) {
this.menuId = 1;
} else {
this.menuId += 1;
}
const id = this.menuId;
this.connect("GM_registerMenuCommand", [id, name, accessKey]).then((con) => {
con.onMessage((data: { action: string; data: any }) => {
if (data.action === "onClick") {
listener();
}
});
con.onDisconnect(() => {
this.menuMap?.delete(id);
});
});
this.menuMap.set(id, name);
return id;
}
@GMContext.API()
GM_unregisterMenuCommand(id: number): void {
if (!this.menuMap) {
this.menuMap = new Map();
}
this.menuMap.delete(id);
this.sendMessage("GM_unregisterMenuCommand", [id]);
}
// 用于脚本跨域请求,需要@connect domain指定允许的域名
@GMContext.API({
depend: ["CAT_fetchBlob", "CAT_createBlobUrl", "CAT_fetchDocument"],
@ -252,7 +299,6 @@ export default class GMApi {
values.map(async (val) => {
if (val instanceof File) {
const url = await this.CAT_createBlobUrl(val);
console.log(url);
data.push({
key,
type: "file",