diff --git a/eslint.config.mjs b/eslint.config.mjs index 1d2768d..02d56ae 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -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, }, }, diff --git a/package.json b/package.json index b750129..8b979a9 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c1c1bb4..c0a38b7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/rspack.config.ts b/rspack.config.ts index 8c47697..d1fe169 100644 --- a/rspack.config.ts +++ b/rspack.config.ts @@ -89,6 +89,16 @@ export default defineConfig({ }, ], }, + { + type: "asset", + test: /\.d\.ts$/, + exclude: /node_modules/, + }, + { + type: "asset", + test: /\.tpl$/, + exclude: /node_modules/, + }, ], }, plugins: [ diff --git a/src/pages/components/CodeEditor/index.tsx b/src/pages/components/CodeEditor/index.tsx new file mode 100644 index 0000000..2575a7d --- /dev/null +++ b/src/pages/components/CodeEditor/index.tsx @@ -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(); + 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 ( +
+ ); +}; + +export default React.forwardRef(CodeEditor); diff --git a/src/pages/components/layout/MainLayout.tsx b/src/pages/components/layout/MainLayout.tsx index d1ee594..e312076 100644 --- a/src/pages/components/layout/MainLayout.tsx +++ b/src/pages/components/layout/MainLayout.tsx @@ -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(null); const [importVisible, setImportVisible] = useState(false); const { t } = useTranslation(); @@ -62,8 +65,7 @@ const MainLayout: React.FC<{ droplist={ { - setLightMode(key); - localStorage.lightMode = key; + dispatch(setDarkMode(key as "light" | "dark" | "auto")); }} selectedKeys={[lightMode]} > diff --git a/src/pages/install/App.tsx b/src/pages/install/App.tsx index 5985f3b..423ba94 100644 --- a/src/pages/install/App.tsx +++ b/src/pages/install/App.tsx @@ -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([]); + const [metadata, setMetadata] = useState({}); + // 脚本信息包括脚本代码、下载url,但是不包括解析代码后得到的metadata,通过background的缓存获取 + const [info, setInfo] = useState(); + // 对脚本详细的描述 + const [description, setDescription] = useState(); + // 是系统检测到脚本更新时打开的窗口会有一个倒计时 + const [countdown, setCountdown] = useState(-1); + // 是否为更新 + const [isUpdate, setIsUpdate] = useState(false); + // 脚本信息 + const [upsertScript, setUpsertScript] = useState