添加包

This commit is contained in:
王一之 2024-11-28 18:00:58 +08:00
parent 82e2c29937
commit 4b7957256e
11 changed files with 1225 additions and 47 deletions

View File

@ -16,6 +16,7 @@
"lint-fix": "eslint --fix ." "lint-fix": "eslint --fix ."
}, },
"dependencies": { "dependencies": {
"@reduxjs/toolkit": "^2.3.0",
"cron": "^3.2.1", "cron": "^3.2.1",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"dexie": "^4.0.10", "dexie": "^4.0.10",
@ -24,6 +25,7 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-i18next": "^15.1.0", "react-i18next": "^15.1.0",
"react-redux": "^9.1.2",
"semver": "^7.6.3", "semver": "^7.6.3",
"uuid": "^11.0.3", "uuid": "^11.0.3",
"yaml": "^2.6.1" "yaml": "^2.6.1"
@ -38,13 +40,17 @@
"@types/react-dom": "^18.2.18", "@types/react-dom": "^18.2.18",
"@types/semver": "^7.5.8", "@types/semver": "^7.5.8",
"@vitest/coverage-v8": "2.1.4", "@vitest/coverage-v8": "2.1.4",
"autoprefixer": "^10.4.20",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"eslint": "^9.12.0", "eslint": "^9.12.0",
"eslint-plugin-react": "^7.37.1", "eslint-plugin-react": "^7.37.1",
"eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-hooks": "^5.0.0",
"globals": "^15.11.0", "globals": "^15.11.0",
"jsdom": "^25.0.1", "jsdom": "^25.0.1",
"postcss": "^8.4.49",
"postcss-loader": "^8.1.1",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"tailwindcss": "^3.4.15",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.6.3", "typescript": "^5.6.3",
"typescript-eslint": "^8.8.1", "typescript-eslint": "^8.8.1",

644
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -27,6 +27,7 @@ export default defineConfig({
offscreen: `${src}/offscreen.ts`, offscreen: `${src}/offscreen.ts`,
sandbox: `${src}/sandbox.ts`, sandbox: `${src}/sandbox.ts`,
popup: `${src}/pages/popup/main.tsx`, popup: `${src}/pages/popup/main.tsx`,
install: `${src}/pages/install/main.tsx`,
}, },
output: { output: {
path: `${dist}/ext/src`, path: `${dist}/ext/src`,
@ -45,6 +46,23 @@ export default defineConfig({
}, },
module: { module: {
rules: [ rules: [
{
test: /\.css$/,
use: [
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
},
},
},
],
type: "css",
},
{ {
test: /\.(svg|png)$/, test: /\.(svg|png)$/,
type: "asset", type: "asset",
@ -101,6 +119,14 @@ export default defineConfig({
}, },
], ],
}), }),
new rspack.HtmlRspackPlugin({
filename: `${dist}/ext/src/install.html`,
template: `${src}/pages/install/index.html`,
inject: "head",
title: "Install - ScriptCat",
minify: true,
chunks: ["install"],
}),
new rspack.HtmlRspackPlugin({ new rspack.HtmlRspackPlugin({
filename: `${dist}/ext/src/popup.html`, filename: `${dist}/ext/src/popup.html`,
template: `${src}/pages/popup/index.html`, template: `${src}/pages/popup/index.html`,

View File

@ -13,8 +13,8 @@ export default class Manager {
listenerScriptInstall() { listenerScriptInstall() {
// 初始化脚本安装监听 // 初始化脚本安装监听
chrome.webRequest.onBeforeRequest.addListener( chrome.webRequest.onCompleted.addListener(
(req: chrome.webRequest.WebRequestBodyDetails) => { (req: chrome.webRequest.WebResponseCacheDetails) => {
// 处理url, 实现安装脚本 // 处理url, 实现安装脚本
if (req.method !== "GET") { if (req.method !== "GET") {
return; return;
@ -30,20 +30,41 @@ export default class Manager {
} }
// 获取url参数 // 获取url参数
const targetUrl = url.hash.split("url=")[1]; const targetUrl = url.hash.split("url=")[1];
// 判断是否有bypass参数
if (url.hash.includes("bypass=true")) {
return;
}
// 读取脚本url内容, 进行安装 // 读取脚本url内容, 进行安装
LoggerCore.getInstance().logger().debug("install script", { url: targetUrl }); LoggerCore.getInstance().logger().debug("install script", { url: targetUrl });
this.openInstallPageByUrl(targetUrl).catch(() => { this.openInstallPageByUrl(targetUrl).catch(() => {
// 如果打开失败, 则重定向到安装页 // 如果打开失败, 则重定向到安装页
chrome.scripting.executeScript({ chrome.scripting.executeScript({
target: { tabId: req.tabId }, target: { tabId: req.tabId },
func: () => { func: function () {
history.back(); history.back();
}, },
}); });
// 并不再重定向当前url
chrome.declarativeNetRequest.updateDynamicRules(
{
removeRuleIds: [2],
addRules: [
{
id: 2,
priority: 1,
action: {
type: chrome.declarativeNetRequest.RuleActionType.ALLOW,
},
condition: {
regexFilter: targetUrl,
resourceTypes: [chrome.declarativeNetRequest.ResourceType.MAIN_FRAME],
requestMethods: [chrome.declarativeNetRequest.RequestMethod.GET],
},
},
],
},
() => {
if (chrome.runtime.lastError) {
console.error(chrome.runtime.lastError);
}
}
);
}); });
}, },
{ {
@ -54,37 +75,6 @@ export default class Manager {
types: ["main_frame"], types: ["main_frame"],
} }
); );
// 屏蔽某个tab都安装脚本
this.connect.on("install_script", (data: { req: chrome.webRequest.WebRequestBodyDetails }) => {
chrome.declarativeNetRequest.updateDynamicRules(
{
removeRuleIds: [2],
addRules: [
{
id: 2,
priority: 1,
action: {
type: chrome.declarativeNetRequest.RuleActionType.ALLOW,
redirect: {
regexSubstitution: "https://docs.scriptcat.org/docs/script_installation#url=\\0",
},
},
condition: {
regexFilter: "^([^#]+?)\\.user(\\.bg|\\.sub)?\\.js((\\?).*|$)",
resourceTypes: [chrome.declarativeNetRequest.ResourceType.MAIN_FRAME],
requestMethods: [chrome.declarativeNetRequest.RequestMethod.GET],
tabIds: [data.req.tabId],
},
},
],
},
() => {
if (chrome.runtime.lastError) {
console.error(chrome.runtime.lastError);
}
}
);
});
// 重定向到脚本安装页 // 重定向到脚本安装页
chrome.declarativeNetRequest.updateDynamicRules( chrome.declarativeNetRequest.updateDynamicRules(
{ {

3
src/index.css Normal file
View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@ -20,6 +20,7 @@
"permissions": [ "permissions": [
"offscreen", "offscreen",
"scripting", "scripting",
"activeTab",
"webRequest", "webRequest",
"declarativeNetRequest" "declarativeNetRequest"
], ],

View File

@ -0,0 +1,9 @@
function App() {
return (
<div className="h-full">
<p className="text-lg">aa</p>
</div>
);
}
export default App;

View File

@ -0,0 +1,468 @@
import React, { useEffect, useState } from "react";
import {
Avatar,
Button,
Grid,
Message,
Space,
Switch,
Tag,
Tooltip,
Typography,
} from "@arco-design/web-react";
import ScriptController from "@App/app/service/script/controller";
import {
prepareScriptByCode,
prepareSubscribeByCode,
ScriptInfo,
} from "@App/pkg/utils/script";
import {
Metadata,
Script,
SCRIPT_STATUS_DISABLE,
SCRIPT_STATUS_ENABLE,
} from "@App/app/repo/scripts";
import { nextTime } from "@App/pkg/utils/utils";
import IoC from "@App/app/ioc";
import { Subscribe, SUBSCRIBE_STATUS_ENABLE } from "@App/app/repo/subscribe";
import SubscribeController from "@App/app/service/subscribe/controller";
import { useTranslation } from "react-i18next";
import { i18nDescription, i18nName } from "@App/locales/locales";
import CodeEditor from "../components/CodeEditor";
type Permission = { label: string; color?: string; value: string[] }[];
const closeWindow = () => {
window.close();
};
export default function Description() {
const [permission, setPermission] = useState<Permission>([]);
const [metadata, setMetadata] = useState<Metadata>({});
// 脚本信息包括脚本代码、下载url但是不包括解析代码后得到的metadata通过background的缓存获取
const [info, setInfo] = useState<ScriptInfo>();
// 对脚本详细的描述
const [description, setDescription] = useState<any>();
// 是系统检测到脚本更新时打开的窗口会有一个倒计时
const [countdown, setCountdown] = useState<number>(-1);
// 是否为更新
const [isUpdate, setIsUpdate] = useState<boolean>(false);
// 脚本信息
const [upsertScript, setUpsertScript] = useState<Script | Subscribe>();
// 更新的情况下会有老版本的脚本信息
const [oldScript, setOldScript] = useState<Script | Subscribe>();
// 脚本开启状态
const [enable, setEnable] = useState<boolean>(false);
const scriptCtrl = IoC.instance(ScriptController) as ScriptController;
const subscribeCtrl = IoC.instance(
SubscribeController
) as SubscribeController;
const [isSub, setIsSub] = useState<boolean>(false);
// 按钮文案
const [btnText, setBtnText] = useState<string>();
const { t } = useTranslation();
// 不推荐的内容标签与描述
const antifeatures: {
[key: string]: { color: string; title: string; description: string };
} = {
"referral-link": {
color: "purple",
title: t("antifeature_referral_link_title"),
description: t("antifeature_referral_link_description"),
},
ads: {
color: "orange",
title: t("antifeature_ads_title"),
description: t("antifeature_ads_description"),
},
payment: {
color: "magenta",
title: t("antifeature_payment_title"),
description: t("antifeature_payment_description"),
},
miner: {
color: "orangered",
title: t("antifeature_miner_title"),
description: t("antifeature_miner_description"),
},
membership: {
color: "blue",
title: t("antifeature_membership_title"),
description: t("antifeature_membership_description"),
},
tracking: {
color: "pinkpurple",
title: t("antifeature_tracking_title"),
description: t("antifeature_tracking_description"),
},
};
useEffect(() => {
if (isSub) {
setBtnText(isUpdate ? t("update_subscribe")! : t("install_subscribe")!);
} else {
setBtnText(isUpdate ? t("update_script")! : t("install_script")!);
}
}, [isSub, isUpdate]);
useEffect(() => {
if (countdown === -1) {
return;
}
setTimeout(() => {
setCountdown((time) => {
if (time > 0) {
return time - 1;
}
if (time === 0) {
closeWindow();
}
return time;
});
}, 1000);
}, [countdown]);
const url = new URL(window.location.href);
const uuid = url.searchParams.get("uuid");
if (!uuid) {
return <p>{t("invalid_link")}</p>;
}
useEffect(() => {
scriptCtrl.fetchScriptInfo(uuid).then(async (resp: ScriptInfo) => {
if (!resp) {
return;
}
let prepare:
| { script: Script; oldScript?: Script }
| { subscribe: Subscribe; oldSubscribe?: Subscribe };
let action: Script | Subscribe;
if (resp.isSubscribe) {
setIsSub(true);
prepare = await prepareSubscribeByCode(resp.code, resp.url);
action = prepare.subscribe;
setOldScript(prepare.oldSubscribe);
delete prepare.oldSubscribe;
} else {
if (resp.isUpdate) {
prepare = await prepareScriptByCode(resp.code, resp.url, resp.uuid);
} else {
prepare = await prepareScriptByCode(resp.code, resp.url);
}
action = prepare.script;
setOldScript(prepare.oldScript);
delete prepare.oldScript;
}
setEnable(action.status === SUBSCRIBE_STATUS_ENABLE);
if (resp.source === "system") {
setCountdown(60);
}
const meta = action.metadata;
if (!meta) {
return;
}
const perm: Permission = [];
if (resp.isSubscribe) {
perm.push({
label: t("subscribe_install_label"),
color: "#ff0000",
value: meta.scripturl,
});
}
if (meta.match) {
perm.push({ label: t("script_runs_in"), value: meta.match });
}
if (meta.connect) {
perm.push({
label: t("script_has_full_access_to"),
color: "#F9925A",
value: meta.connect,
});
}
if (meta.require) {
perm.push({ label: t("script_requires"), value: meta.require });
}
setUpsertScript(action);
if (action.id !== 0) {
setIsUpdate(true);
}
setPermission(perm);
setMetadata(meta);
setInfo(resp);
const desList = [];
let isCookie = false;
metadata.grant?.forEach((val) => {
if (val === "GM_cookie") {
isCookie = true;
}
});
if (isCookie) {
desList.push(
<Typography.Text type="error" key="cookie">
{t("cookie_warning")}
</Typography.Text>
);
}
if (meta.crontab) {
desList.push(
<Typography.Text key="crontab">
{t("scheduled_script_description_1")}
</Typography.Text>
);
desList.push(
<Typography.Text key="cronta-nexttime">
{t("scheduled_script_description_2", {
expression: meta.crontab[0],
time: nextTime(meta.crontab[0]),
})}
</Typography.Text>
);
} else if (meta.background) {
desList.push(
<Typography.Text key="background">
{t("background_script_description")}
</Typography.Text>
);
}
if (desList.length) {
setDescription(<div>{desList.map((item) => item)}</div>);
}
// 修改网页显示title
document.title = `${
action.id === 0 ? t("install_script") : t("update_script")
} - ${meta.name} - ScriptCat`;
});
}, []);
return (
<div className="h-full">
<Grid.Row gutter={8}>
<Grid.Col flex={1} className="flex-col p-8px">
<Space direction="vertical">
<div>
{upsertScript?.metadata.icon && (
<Avatar size={32} shape="square" style={{ marginRight: "8px" }}>
<img
src={upsertScript.metadata.icon[0]}
alt={upsertScript?.name}
/>
</Avatar>
)}
<Typography.Text bold className="text-size-lg">
{upsertScript && i18nName(upsertScript)}
<Tooltip
content={
isSub
? t("subscribe_source_tooltip")
: t("script_status_tooltip")
}
>
<Switch
style={{ marginLeft: "8px" }}
checked={enable}
onChange={(checked) => {
setUpsertScript((script) => {
if (!script) {
return script;
}
script.status = checked
? SCRIPT_STATUS_ENABLE
: SCRIPT_STATUS_DISABLE;
setEnable(checked);
return script;
});
}}
/>
</Tooltip>
</Typography.Text>
</div>
<div>
<Typography.Text bold>
{upsertScript && i18nDescription(upsertScript)}
</Typography.Text>
</div>
<div>
<Typography.Text bold>
{t("author")}: {metadata.author}
</Typography.Text>
</div>
<div>
<Typography.Text
bold
style={{
overflowWrap: "break-word",
wordBreak: "break-all",
maxHeight: "70px",
display: "block",
overflowY: "auto",
}}
>
{t("source")}: {info?.url}
</Typography.Text>
</div>
<div className="text-end">
<Space>
<Button
type="primary"
size="small"
onClick={() => {
if (!upsertScript) {
Message.error(t("script_info_load_failed")!);
return;
}
if (isSub) {
subscribeCtrl
.upsert(upsertScript as Subscribe)
.then(() => {
Message.success(t("subscribe_success")!);
setBtnText(t("subscribe_success")!);
setTimeout(() => {
closeWindow();
}, 200);
})
.catch((e) => {
Message.error(`${t("subscribe_failed")}: ${e}`);
});
return;
}
scriptCtrl
.upsert(upsertScript as Script)
.then(() => {
if (isUpdate) {
Message.success(t("install.update_success")!);
setBtnText(t("install.update_success")!);
} else {
Message.success(t("install_success")!);
setBtnText(t("install_success")!);
}
setTimeout(() => {
closeWindow();
}, 200);
})
.catch((e) => {
Message.error(`${t("install_failed")}: ${e}`);
});
}}
>
{btnText}
</Button>
<Button
type="primary"
status="danger"
size="small"
onClick={() => {
if (countdown === -1) {
closeWindow();
} else {
setCountdown(-1);
}
}}
>
{countdown === -1
? t("close")
: `${t("stop")} (${countdown})`}
</Button>
</Space>
</div>
</Space>
</Grid.Col>
<Grid.Col flex={1} className="p-8px">
<Space direction="vertical">
<div>
<Space>
{oldScript && (
<Tooltip
content={`${t("current_version")}: v${
oldScript.metadata.version[0]
}`}
>
<Tag bordered>{oldScript.metadata.version[0]}</Tag>
</Tooltip>
)}
{metadata.version && (
<Tooltip
color="red"
content={`${t("update_version")}: v${metadata.version[0]}`}
>
<Tag bordered color="red">
{metadata.version[0]}
</Tag>
</Tooltip>
)}
{(metadata.background || metadata.crontab) && (
<Tooltip color="green" content={t("background_script_tag")}>
<Tag bordered color="green">
{t("background_script")}
</Tag>
</Tooltip>
)}
{metadata.crontab && (
<Tooltip color="green" content={t("scheduled_script_tag")}>
<Tag bordered color="green">
{t("scheduled_script")}
</Tag>
</Tooltip>
)}
{metadata.antifeature &&
metadata.antifeature.map((antifeature) => {
const item = antifeature.split(" ")[0];
return (
antifeatures[item] && (
<Tooltip
color={antifeatures[item].color}
content={antifeatures[item].description}
>
<Tag bordered color={antifeatures[item].color}>
{antifeatures[item].title}
</Tag>
</Tooltip>
)
);
})}
</Space>
</div>
{description && description}
<div>
<Typography.Text type="error">
{t("install_from_legitimate_sources_warning")}
</Typography.Text>
</div>
</Space>
</Grid.Col>
<Grid.Col span={24}>
<Grid.Row>
{permission.map((item) => (
<Grid.Col
key={item.label}
span={8}
style={{
maxHeight: "200px",
overflowY: "auto",
overflowX: "auto",
boxSizing: "border-box",
}}
className="p-8px"
>
<Typography.Text bold color={item.color}>
{item.label}
</Typography.Text>
{item.value.map((v) => (
<div key={v}>
<Typography.Text
style={{ wordBreak: "unset", color: item.color }}
>
{v}
</Typography.Text>
</div>
))}
</Grid.Col>
))}
</Grid.Row>
</Grid.Col>
</Grid.Row>
<CodeEditor
id="show-code"
code={upsertScript?.code || undefined}
diffCode={oldScript?.code || ""}
/>
</div>
);
}

View File

@ -0,0 +1,30 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title><%= htmlRspackPlugin.options.title %></title>
<style>
html,
body {
margin: 0;
padding: 0;
border: 0;
height: 100%;
}
body {
background-color: var(--color-bg-2);
color: var(--color-text-1);
}
</style>
</head>
<body>
<div id="root"></div>
</body>
<% if rspackConfig.mode=="script" { %>
<script type="text/javascript" src="/_locales/i18n.js"></script>
<script type="text/javascript" src="https://cdn.crowdin.com/jipt/jipt.js"></script>
<% } %>
</html>

View File

@ -0,0 +1,10 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import "@App/index.css";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

9
tailwind.config.js Normal file
View File

@ -0,0 +1,9 @@
/* eslint-disable no-undef */
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.{html,tsx}"],
theme: {
extend: {},
},
plugins: [],
};