页面组件
This commit is contained in:
parent
804266c6dd
commit
84261e22bd
@ -24,6 +24,8 @@ export default [
|
||||
"react-hooks": reactHooks,
|
||||
},
|
||||
rules: {
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
...reactHooks.configs.recommended.rules,
|
||||
},
|
||||
},
|
||||
|
@ -23,6 +23,8 @@
|
||||
"dexie": "^4.0.10",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"i18next": "^23.16.4",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"pako": "^2.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^15.1.0",
|
||||
@ -38,6 +40,7 @@
|
||||
"@rspack/cli": "^1.0.14",
|
||||
"@rspack/core": "^1.0.14",
|
||||
"@types/chrome": "^0.0.279",
|
||||
"@types/pako": "^2.0.3",
|
||||
"@types/react": "^18.2.48",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/semver": "^7.5.8",
|
||||
|
24
pnpm-lock.yaml
generated
24
pnpm-lock.yaml
generated
@ -29,6 +29,12 @@ importers:
|
||||
i18next:
|
||||
specifier: ^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:
|
||||
specifier: ^18.2.0
|
||||
version: 18.3.1
|
||||
@ -69,6 +75,9 @@ importers:
|
||||
'@types/chrome':
|
||||
specifier: ^0.0.279
|
||||
version: 0.0.279
|
||||
'@types/pako':
|
||||
specifier: ^2.0.3
|
||||
version: 2.0.3
|
||||
'@types/react':
|
||||
specifier: ^18.2.48
|
||||
version: 18.3.12
|
||||
@ -1019,6 +1028,9 @@ packages:
|
||||
'@types/node@22.8.1':
|
||||
resolution: {integrity: sha512-k6Gi8Yyo8EtrNtkHXutUu2corfDf9su95VYVP10aGYMMROM6SAItZi0w1XszA6RtWTHSVp5OeFof37w0IEqCQg==}
|
||||
|
||||
'@types/pako@2.0.3':
|
||||
resolution: {integrity: sha512-bq0hMV9opAcrmE0Byyo0fY3Ew4tgOevJmQ9grUhpXQhYfyLJ1Kqg3P33JT5fdbT2AjeAjR51zqqVjAL/HMkx7Q==}
|
||||
|
||||
'@types/prop-types@15.7.13':
|
||||
resolution: {integrity: sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==}
|
||||
|
||||
@ -2630,6 +2642,9 @@ packages:
|
||||
mlly@1.7.3:
|
||||
resolution: {integrity: sha512-xUsx5n/mN0uQf4V548PKQ+YShA4/IW0KI1dZhrNrPCLG+xizETbHTkOa1f8/xut9JRPp8kQuMnz0oqwkTiLo/A==}
|
||||
|
||||
monaco-editor@0.52.2:
|
||||
resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==}
|
||||
|
||||
mrmime@1.0.1:
|
||||
resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==}
|
||||
engines: {node: '>=10'}
|
||||
@ -2771,6 +2786,9 @@ packages:
|
||||
package-manager-detector@0.2.5:
|
||||
resolution: {integrity: sha512-3dS7y28uua+UDbRCLBqltMBrbI+A5U2mI9YuxHRxIWYmLj3DwntEBmERYzIAQ4DMeuCUOBSak7dBHHoXKpOTYQ==}
|
||||
|
||||
pako@2.1.0:
|
||||
resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==}
|
||||
|
||||
parent-module@1.0.1:
|
||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||
engines: {node: '>=6'}
|
||||
@ -4484,6 +4502,8 @@ snapshots:
|
||||
dependencies:
|
||||
undici-types: 6.19.8
|
||||
|
||||
'@types/pako@2.0.3': {}
|
||||
|
||||
'@types/prop-types@15.7.13': {}
|
||||
|
||||
'@types/qs@6.9.16': {}
|
||||
@ -6597,6 +6617,8 @@ snapshots:
|
||||
pkg-types: 1.2.1
|
||||
ufo: 1.5.4
|
||||
|
||||
monaco-editor@0.52.2: {}
|
||||
|
||||
mrmime@1.0.1: {}
|
||||
|
||||
mrmime@2.0.0: {}
|
||||
@ -6728,6 +6750,8 @@ snapshots:
|
||||
|
||||
package-manager-detector@0.2.5: {}
|
||||
|
||||
pako@2.1.0: {}
|
||||
|
||||
parent-module@1.0.1:
|
||||
dependencies:
|
||||
callsites: 3.1.0
|
||||
|
@ -89,6 +89,16 @@ export default defineConfig({
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "asset",
|
||||
test: /\.d\.ts$/,
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
{
|
||||
type: "asset",
|
||||
test: /\.tpl$/,
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
|
225
src/pages/components/CodeEditor/index.tsx
Normal file
225
src/pages/components/CodeEditor/index.tsx
Normal 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);
|
@ -15,12 +15,15 @@ import { IconDesktop, IconMoonFill, IconSunFill } from "@arco-design/web-react/i
|
||||
import React, { ReactNode, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import "./index.css";
|
||||
import { useAppDispatch, useAppSelector } from "@App/store/hooks";
|
||||
import { selectThemeMode, setDarkMode } from "@App/store/features/setting";
|
||||
|
||||
const MainLayout: React.FC<{
|
||||
children: ReactNode;
|
||||
className: string;
|
||||
}> = ({ children, className }) => {
|
||||
const [lightMode, setLightMode] = useState(localStorage.lightMode || "auto");
|
||||
const lightMode = useAppSelector(selectThemeMode);
|
||||
const dispatch = useAppDispatch();
|
||||
const importRef = useRef<RefInputType>(null);
|
||||
const [importVisible, setImportVisible] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
@ -62,8 +65,7 @@ const MainLayout: React.FC<{
|
||||
droplist={
|
||||
<Menu
|
||||
onClickMenuItem={(key) => {
|
||||
setLightMode(key);
|
||||
localStorage.lightMode = key;
|
||||
dispatch(setDarkMode(key as "light" | "dark" | "auto"));
|
||||
}}
|
||||
selectedKeys={[lightMode]}
|
||||
>
|
||||
|
@ -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() {
|
||||
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 (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
@ -5,11 +5,15 @@ import MainLayout from "../components/layout/MainLayout.tsx";
|
||||
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 "@App/store/store.ts";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<MainLayout className="!flex-col !px-4 box-border">
|
||||
<App />
|
||||
</MainLayout>
|
||||
<Provider store={store}>
|
||||
<MainLayout className="!flex-col !px-4 box-border">
|
||||
<App />
|
||||
</MainLayout>
|
||||
</Provider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
149
src/pkg/utils/monaco-editor.ts
Normal file
149
src/pkg/utils/monaco-editor.ts
Normal 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 * * * 每6小时的0分时执行一次
|
||||
15 */6 * * * 每6小时的15分时执行一次
|
||||
* once * * * 每小时运行一次
|
||||
* * once * * 每天运行一次
|
||||
* 10 once * * 每天10点-10:59中运行一次,假设当10:04时运行了一次,10:05-10:59的后续的时间将不会再运行
|
||||
* 1,3,5 once * * 每天1点3点5点中运行一次,假设当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);
|
||||
};
|
53
src/store/features/setting.ts
Normal file
53
src/store/features/setting.ts
Normal 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;
|
@ -1,10 +1,11 @@
|
||||
import type { Action, ThunkAction } from "@reduxjs/toolkit";
|
||||
import { combineSlices, configureStore } from "@reduxjs/toolkit";
|
||||
import { setupListeners } from "@reduxjs/toolkit/query";
|
||||
import { settingSlice } from "./features/setting";
|
||||
|
||||
// `combineSlices` automatically combines the reducers using
|
||||
// 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
|
||||
export type RootState = ReturnType<typeof rootReducer>;
|
||||
|
||||
|
4
src/types/main.d.ts
vendored
Normal file
4
src/types/main.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
declare module "@App/types/scriptcat.d.ts";
|
||||
declare module "*.tpl";
|
||||
declare module "*.json";
|
||||
declare module "*.yaml";
|
2
src/types/scriptcat.d.ts
vendored
2
src/types/scriptcat.d.ts
vendored
@ -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_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;
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user