2025-01-03 17:09:16 +08:00

372 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Avatar, Button, Grid, Message, Space, Switch, Tag, Tooltip, Typography } from "@arco-design/web-react";
import CodeEditor from "../components/CodeEditor";
import { useEffect, useState } from "react";
import { Metadata, Script, SCRIPT_STATUS_DISABLE, SCRIPT_STATUS_ENABLE } from "@App/app/repo/scripts";
import { Subscribe } from "@App/app/repo/subscribe";
import { i18nDescription, i18nName } from "@App/locales/locales";
import { useTranslation } from "react-i18next";
import { prepareScriptByCode, prepareSubscribeByCode, ScriptInfo } from "@App/pkg/utils/script";
import { nextTime } from "@App/pkg/utils/utils";
import { ScriptClient } from "@App/app/service/service_worker/client";
type Permission = { label: string; color?: string; value: string[] }[];
const closeWindow = () => {
window.close();
};
function App() {
// 脚本信息包括脚本代码、下载url、metadata等信息通过service_worker的缓存获取
const [scriptInfo, setScriptInfo] = useState<ScriptInfo>();
// 是系统检测到脚本更新时打开的窗口会有一个倒计时
const [countdown, setCountdown] = useState<number>(-1);
// 脚本信息
const [upsertScript, setUpsertScript] = useState<Script | Subscribe>();
// 更新的情况下会有老版本的脚本信息
const [oldScript, setOldScript] = useState<Script | Subscribe>();
// 脚本开启状态
const [enable, setEnable] = useState<boolean>(false);
// 按钮文案
const [btnText, setBtnText] = useState<string>("");
const { t } = useTranslation();
const metadata: Metadata = scriptInfo?.metadata || {};
const permission: Permission = [];
const isUpdate = scriptInfo?.update;
const description = [];
if (scriptInfo) {
if (scriptInfo.userSubscribe) {
permission.push({
label: t("subscribe_install_label"),
color: "#ff0000",
value: metadata.scripturl,
});
}
if (metadata.match) {
permission.push({ label: t("script_runs_in"), value: metadata.match });
}
if (metadata.connect) {
permission.push({
label: t("script_has_full_access_to"),
color: "#F9925A",
value: metadata.connect,
});
}
if (metadata.require) {
permission.push({ label: t("script_requires"), value: metadata.require });
}
let isCookie = false;
metadata.grant?.forEach((val) => {
if (val === "GM_cookie") {
isCookie = true;
}
});
if (isCookie) {
description.push(
<Typography.Text type="error" key="cookie">
{t("cookie_warning")}
</Typography.Text>
);
}
if (metadata.crontab) {
description.push(<Typography.Text key="crontab">{t("scheduled_script_description_1")}</Typography.Text>);
description.push(
<Typography.Text key="cronta-nexttime">
{t("scheduled_script_description_2", {
expression: metadata.crontab[0],
time: nextTime(metadata.crontab[0]),
})}
</Typography.Text>
);
} else if (metadata.background) {
description.push(<Typography.Text key="background">{t("background_script_description")}</Typography.Text>);
}
}
// 不推荐的内容标签与描述
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(() => {
const url = new URL(window.location.href);
const uuid = url.searchParams.get("uuid");
if (!uuid) {
return;
}
new ScriptClient()
.getInstallInfo(uuid)
.then(async (info: ScriptInfo) => {
if (!info) {
throw new Error("fetch script info failed");
}
// 如果是更新的情况下, 获取老版本的脚本信息
let prepare: { script: Script; oldScript?: Script } | { subscribe: Subscribe; oldSubscribe?: Subscribe };
let action: Script | Subscribe;
if (info.userSubscribe) {
prepare = await prepareSubscribeByCode(info.code, info.url);
action = prepare.subscribe;
setOldScript(prepare.oldSubscribe);
} else {
if (info.update) {
prepare = await prepareScriptByCode(info.code, info.url, info.uuid);
} else {
prepare = await prepareScriptByCode(info.code, info.url);
}
action = prepare.script;
setOldScript(prepare.oldScript);
}
if (info.userSubscribe) {
setBtnText(isUpdate ? t("update_subscribe")! : t("install_subscribe"));
} else {
setBtnText(isUpdate ? t("update_script")! : t("install_script"));
}
setScriptInfo(info);
setEnable(action.status === SCRIPT_STATUS_ENABLE);
setUpsertScript(action);
// 修改网页显示title
document.title = `${!isUpdate ? t("install_script") : t("update_script")} - ${i18nName(action)} - ScriptCat`;
})
.catch(() => {
Message.error(t("script_info_load_failed"));
});
}, [isUpdate, t]);
return (
<div className="h-full">
<div className="h-full">
<Grid.Row className="mb-2" 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={scriptInfo?.userSubscribe ? 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")}: {scriptInfo?.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 (scriptInfo?.userSubscribe) {
// 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;
}
new ScriptClient()
.install(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();
}, 500);
})
.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>
</div>
);
}
export default App;