页面组件

This commit is contained in:
王一之 2024-12-13 18:04:59 +08:00
parent 804266c6dd
commit 84261e22bd
13 changed files with 754 additions and 9 deletions

View File

@ -24,6 +24,8 @@ export default [
"react-hooks": reactHooks, "react-hooks": reactHooks,
}, },
rules: { rules: {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/ban-ts-comment": "off",
...reactHooks.configs.recommended.rules, ...reactHooks.configs.recommended.rules,
}, },
}, },

View File

@ -23,6 +23,8 @@
"dexie": "^4.0.10", "dexie": "^4.0.10",
"eventemitter3": "^5.0.1", "eventemitter3": "^5.0.1",
"i18next": "^23.16.4", "i18next": "^23.16.4",
"monaco-editor": "^0.52.2",
"pako": "^2.1.0",
"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",
@ -38,6 +40,7 @@
"@rspack/cli": "^1.0.14", "@rspack/cli": "^1.0.14",
"@rspack/core": "^1.0.14", "@rspack/core": "^1.0.14",
"@types/chrome": "^0.0.279", "@types/chrome": "^0.0.279",
"@types/pako": "^2.0.3",
"@types/react": "^18.2.48", "@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18", "@types/react-dom": "^18.2.18",
"@types/semver": "^7.5.8", "@types/semver": "^7.5.8",

24
pnpm-lock.yaml generated
View File

@ -29,6 +29,12 @@ importers:
i18next: i18next:
specifier: ^23.16.4 specifier: ^23.16.4
version: 23.16.4 version: 23.16.4
monaco-editor:
specifier: ^0.52.2
version: 0.52.2
pako:
specifier: ^2.1.0
version: 2.1.0
react: react:
specifier: ^18.2.0 specifier: ^18.2.0
version: 18.3.1 version: 18.3.1
@ -69,6 +75,9 @@ importers:
'@types/chrome': '@types/chrome':
specifier: ^0.0.279 specifier: ^0.0.279
version: 0.0.279 version: 0.0.279
'@types/pako':
specifier: ^2.0.3
version: 2.0.3
'@types/react': '@types/react':
specifier: ^18.2.48 specifier: ^18.2.48
version: 18.3.12 version: 18.3.12
@ -1019,6 +1028,9 @@ packages:
'@types/node@22.8.1': '@types/node@22.8.1':
resolution: {integrity: sha512-k6Gi8Yyo8EtrNtkHXutUu2corfDf9su95VYVP10aGYMMROM6SAItZi0w1XszA6RtWTHSVp5OeFof37w0IEqCQg==} resolution: {integrity: sha512-k6Gi8Yyo8EtrNtkHXutUu2corfDf9su95VYVP10aGYMMROM6SAItZi0w1XszA6RtWTHSVp5OeFof37w0IEqCQg==}
'@types/pako@2.0.3':
resolution: {integrity: sha512-bq0hMV9opAcrmE0Byyo0fY3Ew4tgOevJmQ9grUhpXQhYfyLJ1Kqg3P33JT5fdbT2AjeAjR51zqqVjAL/HMkx7Q==}
'@types/prop-types@15.7.13': '@types/prop-types@15.7.13':
resolution: {integrity: sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==} resolution: {integrity: sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==}
@ -2630,6 +2642,9 @@ packages:
mlly@1.7.3: mlly@1.7.3:
resolution: {integrity: sha512-xUsx5n/mN0uQf4V548PKQ+YShA4/IW0KI1dZhrNrPCLG+xizETbHTkOa1f8/xut9JRPp8kQuMnz0oqwkTiLo/A==} resolution: {integrity: sha512-xUsx5n/mN0uQf4V548PKQ+YShA4/IW0KI1dZhrNrPCLG+xizETbHTkOa1f8/xut9JRPp8kQuMnz0oqwkTiLo/A==}
monaco-editor@0.52.2:
resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==}
mrmime@1.0.1: mrmime@1.0.1:
resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==} resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -2771,6 +2786,9 @@ packages:
package-manager-detector@0.2.5: package-manager-detector@0.2.5:
resolution: {integrity: sha512-3dS7y28uua+UDbRCLBqltMBrbI+A5U2mI9YuxHRxIWYmLj3DwntEBmERYzIAQ4DMeuCUOBSak7dBHHoXKpOTYQ==} resolution: {integrity: sha512-3dS7y28uua+UDbRCLBqltMBrbI+A5U2mI9YuxHRxIWYmLj3DwntEBmERYzIAQ4DMeuCUOBSak7dBHHoXKpOTYQ==}
pako@2.1.0:
resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==}
parent-module@1.0.1: parent-module@1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -4484,6 +4502,8 @@ snapshots:
dependencies: dependencies:
undici-types: 6.19.8 undici-types: 6.19.8
'@types/pako@2.0.3': {}
'@types/prop-types@15.7.13': {} '@types/prop-types@15.7.13': {}
'@types/qs@6.9.16': {} '@types/qs@6.9.16': {}
@ -6597,6 +6617,8 @@ snapshots:
pkg-types: 1.2.1 pkg-types: 1.2.1
ufo: 1.5.4 ufo: 1.5.4
monaco-editor@0.52.2: {}
mrmime@1.0.1: {} mrmime@1.0.1: {}
mrmime@2.0.0: {} mrmime@2.0.0: {}
@ -6728,6 +6750,8 @@ snapshots:
package-manager-detector@0.2.5: {} package-manager-detector@0.2.5: {}
pako@2.1.0: {}
parent-module@1.0.1: parent-module@1.0.1:
dependencies: dependencies:
callsites: 3.1.0 callsites: 3.1.0

View File

@ -89,6 +89,16 @@ export default defineConfig({
}, },
], ],
}, },
{
type: "asset",
test: /\.d\.ts$/,
exclude: /node_modules/,
},
{
type: "asset",
test: /\.tpl$/,
exclude: /node_modules/,
},
], ],
}, },
plugins: [ plugins: [

View File

@ -0,0 +1,225 @@
import Cache from "@App/app/cache";
import { LinterWorker } from "@App/pkg/utils/monaco-editor";
import { useAppSelector } from "@App/store/hooks";
import { editor, Range } from "monaco-editor";
import React, { useEffect, useImperativeHandle, useState } from "react";
type Props = {
className?: string;
diffCode?: string; // 因为代码加载是异步的,diifCode有3种状态:undefined不确定,""没有diff,有diff,不确定的情况下,编辑器不会加载
editable?: boolean;
id: string;
code?: string;
};
const CodeEditor: React.ForwardRefRenderFunction<{ editor: editor.ICodeEditor | undefined }, Props> = (
{ id, className, code, diffCode, editable },
ref
) => {
const settings = useAppSelector((state) => state.setting);
const [monacoEditor, setEditor] = useState<editor.ICodeEditor>();
useImperativeHandle(ref, () => ({
editor: monacoEditor,
}));
useEffect(() => {
if (diffCode === undefined || code === undefined) {
return () => {};
}
let edit: editor.IStandaloneDiffEditor | editor.IStandaloneCodeEditor;
// @ts-ignore
const ts = window.tsUrl ? 0 : 200;
setTimeout(() => {
const div = document.getElementById(id) as HTMLDivElement;
if (diffCode) {
edit = editor.createDiffEditor(div, {
enableSplitViewResizing: false,
renderSideBySide: false,
folding: true,
foldingStrategy: "indentation",
automaticLayout: true,
overviewRulerBorder: false,
scrollBeyondLastLine: false,
readOnly: true,
diffWordWrap: "off",
glyphMargin: true,
});
edit.setModel({
original: editor.createModel(diffCode, "javascript"),
modified: editor.createModel(code, "javascript"),
});
} else {
edit = editor.create(div, {
language: "javascript",
theme: document.body.getAttribute("arco-theme") === "dark" ? "vs-dark" : "vs",
folding: true,
foldingStrategy: "indentation",
automaticLayout: true,
overviewRulerBorder: false,
scrollBeyondLastLine: false,
readOnly: !editable,
glyphMargin: true,
});
edit.setValue(code);
setEditor(edit);
}
}, ts);
return () => {
if (edit) {
edit.dispose();
}
};
}, [code, diffCode, editable, id]);
useEffect(() => {
if (!settings.eslint.enable) {
return () => {};
}
if (!monacoEditor) {
return () => {};
}
const model = monacoEditor.getModel();
if (!model) {
return () => {};
}
let timer: NodeJS.Timeout | null;
const lint = () => {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
LinterWorker.sendLinterMessage({
code: model.getValue(),
id,
config: JSON.parse(settings.eslint.config),
});
}, 500);
};
// 加载完成就检测一次
lint();
model.onDidChangeContent(() => {
lint();
});
// 在行号旁显示ESLint错误/警告图标
const diffEslint = (
makers: {
startLineNumber: number;
endLineNumber: number;
severity: number;
}[]
) => {
// 定义glyph class
const glyphMarginClassList = {
4: "icon-warn",
8: "icon-error",
};
// 先移除所有旧的Decorations
const oldDecorations = model
.getAllDecorations()
.filter(
(i) =>
i.options.glyphMarginClassName &&
Object.values(glyphMarginClassList).includes(i.options.glyphMarginClassName)
);
monacoEditor.removeDecorations(oldDecorations.map((i) => i.id));
/* monaco
// 获取所有ESLint ModelMarkers
const allMarkers = editor.getModelMarkers({ owner: "ESLint" });
*/
// 再重新添加新的Decorations
monacoEditor.createDecorationsCollection(
makers.map(({ startLineNumber, endLineNumber, severity }) => ({
range: new Range(startLineNumber, 1, endLineNumber, 1),
options: {
isWholeLine: true,
// @ts-ignore
glyphMarginClassName: glyphMarginClassList[severity],
/* monaco
glyphMarginHoverMessage: allMarkers.reduce(
(prev: any, next: any) => {
if (
next.startLineNumber === startLineNumber &&
next.endLineNumber === endLineNumber
) {
prev.push({
value: `${next.message} ESLinter [(${next.code.value})](${next.code.target})`,
isTrusted: true,
});
}
return prev;
},
[]
),
*/
},
}))
);
};
const handler = (message: any) => {
if (id !== message.id) {
return;
}
editor.setModelMarkers(model, "ESLint", message.markers);
const fix = new Map();
// 设置fix
message.markers.forEach(
(val: {
code: { value: any };
startLineNumber: any;
endLineNumber: any;
startColumn: any;
endColumn: any;
fix: any;
}) => {
if (val.fix) {
fix.set(
`${val.code.value}|${val.startLineNumber}|${val.endLineNumber}|${val.startColumn}|${val.endColumn}`,
val.fix
);
}
}
);
Cache.getInstance().set("eslint-fix", fix);
// 在行号旁显示ESLint错误/警告图标
const formatMarkers = message.markers.map(
({
startLineNumber,
endLineNumber,
severity,
}: {
startLineNumber: number;
endLineNumber: number;
severity: number;
}) => ({ startLineNumber, endLineNumber, severity })
);
diffEslint(formatMarkers);
};
LinterWorker.hook.addListener("message", handler);
return () => {
LinterWorker.hook.removeListener("message", handler);
};
}, [id, monacoEditor, settings.eslint.config, settings.eslint.enable]);
return (
<div
id={id}
style={{
margin: 0,
padding: 0,
border: 0,
width: "100%",
height: "100%",
overflow: "hidden",
}}
className={className}
/>
);
};
export default React.forwardRef(CodeEditor);

View File

@ -15,12 +15,15 @@ import { IconDesktop, IconMoonFill, IconSunFill } from "@arco-design/web-react/i
import React, { ReactNode, useRef, useState } from "react"; import React, { ReactNode, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import "./index.css"; import "./index.css";
import { useAppDispatch, useAppSelector } from "@App/store/hooks";
import { selectThemeMode, setDarkMode } from "@App/store/features/setting";
const MainLayout: React.FC<{ const MainLayout: React.FC<{
children: ReactNode; children: ReactNode;
className: string; className: string;
}> = ({ children, className }) => { }> = ({ children, className }) => {
const [lightMode, setLightMode] = useState(localStorage.lightMode || "auto"); const lightMode = useAppSelector(selectThemeMode);
const dispatch = useAppDispatch();
const importRef = useRef<RefInputType>(null); const importRef = useRef<RefInputType>(null);
const [importVisible, setImportVisible] = useState(false); const [importVisible, setImportVisible] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
@ -62,8 +65,7 @@ const MainLayout: React.FC<{
droplist={ droplist={
<Menu <Menu
onClickMenuItem={(key) => { onClickMenuItem={(key) => {
setLightMode(key); dispatch(setDarkMode(key as "light" | "dark" | "auto"));
localStorage.lightMode = key;
}} }}
selectedKeys={[lightMode]} selectedKeys={[lightMode]}
> >

View File

@ -1,7 +1,275 @@
import { Avatar, Button, Grid, Message, Space, Switch, Tag, Tooltip, Typography } from "@arco-design/web-react";
import CodeEditor from "../components/CodeEditor";
import { 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 { ScriptInfo } from "@App/pkg/utils/script";
type Permission = { label: string; color?: string; value: string[] }[];
const closeWindow = () => {
window.close();
};
function App() { function App() {
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 [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"),
},
};
return ( return (
<div className="h-full"> <div className="h-full">
<p className="text-lg">aa</p> <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>
</div> </div>
); );
} }

View File

@ -5,11 +5,15 @@ import MainLayout from "../components/layout/MainLayout.tsx";
import "@arco-design/web-react/dist/css/arco.css"; import "@arco-design/web-react/dist/css/arco.css";
import "@App/locales/locales"; import "@App/locales/locales";
import "@App/index.css"; import "@App/index.css";
import { Provider } from "react-redux";
import { store } from "@App/store/store.ts";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode> <React.StrictMode>
<Provider store={store}>
<MainLayout className="!flex-col !px-4 box-border"> <MainLayout className="!flex-col !px-4 box-border">
<App /> <App />
</MainLayout> </MainLayout>
</Provider>
</React.StrictMode> </React.StrictMode>
); );

View File

@ -0,0 +1,149 @@
import dts from "@App/types/scriptcat.d.ts";
import { languages } from "monaco-editor";
import pako from "pako";
import Cache from "@App/app/cache";
import { isFirefox } from "./utils";
import EventEmitter from "eventemitter3";
// 注册eslint
const linterWorker = new Worker("/src/linter.worker.js");
export default function registerEditor() {
// @ts-ignore
window.tsUrl = "";
fetch(chrome.runtime.getURL(`/src/ts.worker.js${isFirefox() ? ".gz" : ""}`))
.then((resp) => resp.blob())
.then(async (blob) => {
const result = pako.inflate(await blob.arrayBuffer());
// @ts-ignore
window.tsUrl = URL.createObjectURL(new Blob([result]));
});
// @ts-ignore
window.MonacoEnvironment = {
getWorkerUrl(moduleId: any, label: any) {
if (label === "typescript" || label === "javascript") {
// return "/src/ts.worker.js";
// @ts-ignore
return window.tsUrl;
}
return "/src/editor.worker.js";
},
};
languages.typescript.javascriptDefaults.addExtraLib(dts, "tampermonkey.d.ts");
// 悬停提示
const prompt: { [key: string]: any } = {
name: "脚本名称",
description: "脚本描述",
namespace: "脚本命名空间",
version: "脚本版本",
author: "脚本作者",
background: "后台脚本",
crontab: `定时脚本 crontab 参考(不适用于云端脚本)
* * * * * *
* * * * *
0 */6 * * * 60
15 */6 * * * 615
* once * * *
* * once * *
* 10 once * * 10-10:59中运行一次,假设当10:04时运行了一次,10:05-10:59的后续的时间将不会再运行
* 1,3,5 once * * 135,1,3,5
* */4 once * * 4,4,8,12,16,20,24
* 10-23 once * * 10-23:59中运行一次,假设当10:04时运行了一次,10:05-23:59的后续时间将不会再运行
* once 13 * * 13`.replace(/\n/g, "<br>"),
};
languages.registerHoverProvider("javascript", {
provideHover: (model, position) => {
return new Promise((resolve) => {
const line = model.getLineContent(position.lineNumber);
const flag = /^\/\/\s*@(\w+?)(\s+(.*?)|)$/.exec(line);
if (flag) {
resolve({
contents: [{ value: prompt[flag[1]], supportHtml: true }],
});
} else if (/==UserScript==/.test(line)) {
// 匹配==UserScript==
resolve({
contents: [{ value: "一个用户脚本" }],
});
} else {
resolve(null);
}
});
},
});
// 处理quick fix
languages.registerCodeActionProvider("javascript", {
provideCodeActions: (model /** ITextModel */, range /** Range */, context /** CodeActionContext */) => {
const actions: languages.CodeAction[] = [];
const eslintFix = <Map<string, any>>Cache.getInstance().get("eslint-fix");
for (let i = 0; i < context.markers.length; i += 1) {
// 判断有没有修复方案
const val = context.markers[i];
const code = typeof val.code === "string" ? val.code : val.code!.value;
const fix = eslintFix.get(
`${code}|${val.startLineNumber}|${val.endLineNumber}|${val.startColumn}|${val.endColumn}`
);
if (fix) {
const edit: languages.IWorkspaceTextEdit = {
resource: model.uri,
textEdit: {
range: fix.range,
text: fix.text,
},
versionId: undefined,
};
actions.push(<languages.CodeAction>{
title: `修复 ${code} 问题`,
diagnostics: [val],
kind: "quickfix",
edit: {
edits: [edit],
},
isPreferred: true,
});
}
}
// const actions = context.markers.map((error) => {
// const edit: languages.IWorkspaceTextEdit = {
// resource: model.uri,
// textEdit: {
// range,
// text: "console.log(1)",
// },
// versionId: undefined,
// };
// return <languages.CodeAction>{
// title: ``,
// diagnostics: [error],
// kind: "quickfix",
// edit: {
// edits: [edit],
// },
// isPreferred: true,
// };
// });
return {
actions,
dispose: () => {},
};
},
});
}
export class LinterWorker {
static hook = new EventEmitter();
static sendLinterMessage(data: unknown) {
linterWorker.postMessage(data);
}
}
linterWorker.onmessage = (event) => {
LinterWorker.hook.emit("message", event.data);
};

View File

@ -0,0 +1,53 @@
import { createAppSlice } from "../hooks";
import { PayloadAction } from "@reduxjs/toolkit";
import { editor } from "monaco-editor";
export const settingSlice = createAppSlice({
name: "setting",
initialState: {
lightMode: localStorage.lightMode || "auto",
eslint: {
enable: true,
config: "",
},
},
reducers: (create) => {
// 初始化黑夜模式
const setAutoMode = () => {
const darkTheme = window.matchMedia("(prefers-color-scheme: dark)");
const isMatch = (match: boolean) => {
if (match) {
document.body.setAttribute("arco-theme", "dark");
editor.setTheme("vs-dark");
} else {
document.body.removeAttribute("arco-theme");
editor.setTheme("vs");
}
};
darkTheme.addEventListener("change", (e) => {
isMatch(e.matches);
});
isMatch(darkTheme.matches);
};
setAutoMode();
return {
setDarkMode: create.reducer((state, action: PayloadAction<"light" | "dark" | "auto">) => {
localStorage.loghtMode = action.payload;
state.lightMode = action.payload;
if (action.payload === "auto") {
setAutoMode();
} else {
document.body.setAttribute("arco-theme", action.payload);
editor.setTheme(action.payload === "dark" ? "vs-dark" : "vs");
}
}),
};
},
selectors: {
selectThemeMode: (state) => state.lightMode,
},
});
export const { setDarkMode } = settingSlice.actions;
export const { selectThemeMode } = settingSlice.selectors;

View File

@ -1,10 +1,11 @@
import type { Action, ThunkAction } from "@reduxjs/toolkit"; import type { Action, ThunkAction } from "@reduxjs/toolkit";
import { combineSlices, configureStore } from "@reduxjs/toolkit"; import { combineSlices, configureStore } from "@reduxjs/toolkit";
import { setupListeners } from "@reduxjs/toolkit/query"; import { setupListeners } from "@reduxjs/toolkit/query";
import { settingSlice } from "./features/setting";
// `combineSlices` automatically combines the reducers using // `combineSlices` automatically combines the reducers using
// their `reducerPath`s, therefore we no longer need to call `combineReducers`. // their `reducerPath`s, therefore we no longer need to call `combineReducers`.
const rootReducer = combineSlices(); const rootReducer = combineSlices(settingSlice);
// Infer the `RootState` type from the root reducer // Infer the `RootState` type from the root reducer
export type RootState = ReturnType<typeof rootReducer>; export type RootState = ReturnType<typeof rootReducer>;

4
src/types/main.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
declare module "@App/types/scriptcat.d.ts";
declare module "*.tpl";
declare module "*.json";
declare module "*.yaml";

View File

@ -94,7 +94,7 @@ declare function GM_log(message: string, level?: GMTypes.LoggerLevel, labels?: G
declare function GM_getResourceText(name: string): string | undefined; declare function GM_getResourceText(name: string): string | undefined;
declare function GM_getResourceURL(name: string, isBlobUrl?: boolean = false): string | undefined; declare function GM_getResourceURL(name: string, isBlobUrl?: boolean): string | undefined;
declare function GM_registerMenuCommand(name: string, listener: () => void, accessKey?: string): number; declare function GM_registerMenuCommand(name: string, listener: () => void, accessKey?: string): number;