编辑器
Some checks failed
test / Run tests (push) Failing after 9s
build / Build (push) Failing after 14s

This commit is contained in:
王一之 2025-03-18 15:40:39 +08:00
parent 98c86d61f1
commit d682b4d566
8 changed files with 111 additions and 83 deletions

View File

@ -2,13 +2,12 @@ import * as path from "path";
import { defineConfig } from "@rspack/cli"; import { defineConfig } from "@rspack/cli";
import { rspack } from "@rspack/core"; import { rspack } from "@rspack/core";
import { version } from "./package.json"; import { version } from "./package.json";
import CompressionPlugin from "compression-webpack-plugin"; // eslint-disable-next-line @typescript-eslint/no-require-imports
const CompressionPlugin = require("compression-webpack-plugin");
const isDev = process.env.NODE_ENV === "development"; const isDev = process.env.NODE_ENV === "development";
const isBeta = version.includes("-"); const isBeta = version.includes("-");
console.log(CompressionPlugin);
// Target browsers, see: https://github.com/browserslist/browserslist // Target browsers, see: https://github.com/browserslist/browserslist
const targets = ["chrome >= 87", "edge >= 88", "firefox >= 78", "safari >= 14"]; const targets = ["chrome >= 87", "edge >= 88", "firefox >= 78", "safari >= 14"];

View File

@ -25,7 +25,7 @@ export class ScriptClient extends Client {
return this.do("getInstallInfo", uuid); return this.do("getInstallInfo", uuid);
} }
install(script: Script, code: string, upsertBy: InstallSource = "user") { install(script: Script, code: string, upsertBy: InstallSource = "user"): Promise<{ update: boolean }> {
return this.do("install", { script, code, upsertBy }); return this.do("install", { script, code, upsertBy });
} }

View File

@ -178,7 +178,7 @@ export class ScriptService {
logger.info("install success"); logger.info("install success");
// 广播一下 // 广播一下
this.mq.publish("installScript", { script, update }); this.mq.publish("installScript", { script, update });
return Promise.resolve(true); return Promise.resolve({ update });
}) })
.catch((e: any) => { .catch((e: any) => {
logger.error("install error", Logger.E(e)); logger.error("install error", Logger.E(e));

View File

@ -31,6 +31,10 @@
"sync_delete": "同步删除", "sync_delete": "同步删除",
"enable_script_sync_to": "启用脚本同步至", "enable_script_sync_to": "启用脚本同步至",
"save": "保存", "save": "保存",
"save_as": "另存为",
"file": "文件",
"run": "运行",
"debug": "调试",
"cloud_sync_account_verification": "云同步账号信息验证中...", "cloud_sync_account_verification": "云同步账号信息验证中...",
"cloud_sync_verification_failed": "云同步账号信息验证失败", "cloud_sync_verification_failed": "云同步账号信息验证失败",
"save_success": "保存成功", "save_success": "保存成功",
@ -358,5 +362,6 @@
"collapse": "收起", "collapse": "收起",
"expand": "展开", "expand": "展开",
"menu_expand_num_before": "菜单项超过", "menu_expand_num_before": "菜单项超过",
"menu_expand_num_after": "个时,自动隐藏" "menu_expand_num_after": "个时,自动隐藏",
"script_name_cannot_be_set_to_empty": "脚本name不可以设置为空"
} }

View File

@ -12,12 +12,12 @@ type Props = {
code?: string; code?: string;
}; };
const CodeEditor: React.ForwardRefRenderFunction<{ editor: editor.ICodeEditor | undefined }, Props> = ( const CodeEditor: React.ForwardRefRenderFunction<{ editor: editor.IStandaloneCodeEditor | undefined }, Props> = (
{ id, className, code, diffCode, editable }, { id, className, code, diffCode, editable },
ref ref
) => { ) => {
const settings = useAppSelector((state) => state.setting); const settings = useAppSelector((state) => state.setting);
const [monacoEditor, setEditor] = useState<editor.ICodeEditor>(); const [monacoEditor, setEditor] = useState<editor.IStandaloneCodeEditor>();
const div = useRef<HTMLDivElement>(null); const div = useRef<HTMLDivElement>(null);
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
editor: monacoEditor, editor: monacoEditor,

View File

@ -44,7 +44,7 @@ function App() {
permission.push({ permission.push({
label: t("subscribe_install_label"), label: t("subscribe_install_label"),
color: "#ff0000", color: "#ff0000",
value: metadata.scripturl, value: metadata.scripturl!,
}); });
} }
if (metadata.match) { if (metadata.match) {
@ -311,8 +311,8 @@ function App() {
<div> <div>
<Space> <Space>
{oldScript && ( {oldScript && (
<Tooltip content={`${t("current_version")}: v${oldScript.metadata.version[0]}`}> <Tooltip content={`${t("current_version")}: v${oldScript.metadata.version![0]}`}>
<Tag bordered>{oldScript.metadata.version[0]}</Tag> <Tag bordered>{oldScript.metadata.version![0]}</Tag>
</Tooltip> </Tooltip>
)} )}
{metadata.version && ( {metadata.version && (

View File

@ -1,6 +1,6 @@
import { Script, ScriptAndCode, ScriptCodeDAO, ScriptDAO } from "@App/app/repo/scripts"; import { Script, ScriptAndCode, ScriptCodeDAO, ScriptDAO } from "@App/app/repo/scripts";
import CodeEditor from "@App/pages/components/CodeEditor"; import CodeEditor from "@App/pages/components/CodeEditor";
import React, { useEffect, useRef, useState } from "react"; import React, { useCallback, useEffect, useRef, useState } from "react";
import { useNavigate, useParams, useSearchParams } from "react-router-dom"; import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import { editor, KeyCode, KeyMod } from "monaco-editor"; import { editor, KeyCode, KeyMod } from "monaco-editor";
import { Button, Dropdown, Grid, Menu, Message, Tabs, Tooltip } from "@arco-design/web-react"; import { Button, Dropdown, Grid, Menu, Message, Tabs, Tooltip } from "@arco-design/web-react";
@ -16,14 +16,15 @@ import { prepareScriptByCode } from "@App/pkg/utils/script";
import ScriptStorage from "@App/pages/components/ScriptStorage"; import ScriptStorage from "@App/pages/components/ScriptStorage";
import ScriptResource from "@App/pages/components/ScriptResource"; import ScriptResource from "@App/pages/components/ScriptResource";
import ScriptSetting from "@App/pages/components/ScriptSetting"; import ScriptSetting from "@App/pages/components/ScriptSetting";
import { scriptClient } from "@App/pages/store/features/script";
import { useTranslation } from "react-i18next";
const { Row } = Grid; const { Row } = Grid;
const { Col } = Grid; const { Col } = Grid;
// 声明一个Map存储Script
const ScriptMap = new Map();
type HotKey = { type HotKey = {
id: string;
title: string;
hotKey: number; hotKey: number;
action: (script: Script, codeEditor: editor.IStandaloneCodeEditor) => void; action: (script: Script, codeEditor: editor.IStandaloneCodeEditor) => void;
}; };
@ -35,51 +36,59 @@ const Editor: React.FC<{
callbackEditor: (e: editor.IStandaloneCodeEditor) => void; callbackEditor: (e: editor.IStandaloneCodeEditor) => void;
onChange: (code: string) => void; onChange: (code: string) => void;
}> = ({ id, script, hotKeys, callbackEditor, onChange }) => { }> = ({ id, script, hotKeys, callbackEditor, onChange }) => {
const [init, setInit] = useState(false); const [node, setNode] = useState<{ editor: editor.IStandaloneCodeEditor }>();
const codeEditor = useRef<{ editor: editor.IStandaloneCodeEditor }>(null); const ref = useCallback<(node: { editor: editor.IStandaloneCodeEditor }) => void>(
// Script.uuid为keyScript为value储存Script (inlineNode) => {
ScriptMap.set(script.uuid, script); if (inlineNode && inlineNode.editor && !node) {
setNode(inlineNode);
}
},
[node]
);
useEffect(() => { useEffect(() => {
if (!codeEditor.current || !codeEditor.current.editor) { if (!node || !node.editor) {
return () => {}; return;
} }
console.log(codeEditor);
// 初始化editor时将Script的uuid绑定到editor上
// @ts-ignore // @ts-ignore
if (!codeEditor.current.editor.uuid) { if (!node.editor.uuid) {
// @ts-ignore // @ts-ignore
codeEditor.current.editor.uuid = script.uuid; node.editor.uuid = script.uuid;
} }
//@ts-ignore
console.log(node.editor.uuid);
hotKeys.forEach((item) => { hotKeys.forEach((item) => {
codeEditor.current?.editor.addCommand(item.hotKey, () => { node.editor.addAction({
// 获取当前激活的editor通过editor._focusTracker._hasFocus判断editor激活状态 可能有更好的方法) id: item.id,
const activeEditor = editor label: item.title,
.getEditors() keybindings: [item.hotKey],
run(editor) {
// @ts-ignore // @ts-ignore
.find((i) => i._focusTracker._hasFocus); item.action(script, editor);
},
});
});
node.editor.onKeyUp(() => {
onChange(node.editor.getValue() || "");
});
callbackEditor(node.editor);
return () => {
node.editor.dispose();
};
}, [node?.editor]);
// 仅在获取到激活的editor时通过editor上绑定的uuid获取Script并指定激活的editor执行快捷键action return <CodeEditor key={id} id={id} ref={ref} code={script.code} diffCode="" editable />;
if (activeEditor) {
// @ts-ignore
item.action(ScriptMap.get(activeEditor.uuid), activeEditor);
}
});
});
codeEditor.current.editor.onKeyUp(() => {
onChange(codeEditor.current?.editor.getValue() || "");
});
callbackEditor(codeEditor.current.editor);
return () => {};
}, []);
return <CodeEditor id={id} ref={codeEditor} code={script.code} diffCode="" editable />;
}; };
const WarpEditor = React.memo(Editor, (prev, next) => {
return prev.script.uuid === next.script.uuid;
});
type EditorMenu = { type EditorMenu = {
title: string; title: string;
tooltip?: string; tooltip?: string;
action?: (script: Script, e: editor.IStandaloneCodeEditor) => void; action?: (script: Script, e: editor.IStandaloneCodeEditor) => void;
items?: { items?: {
id: string;
title: string; title: string;
tooltip?: string; tooltip?: string;
hotKey?: number; hotKey?: number;
@ -140,10 +149,6 @@ const popstate = () => {
}; };
function ScriptEditor() { function ScriptEditor() {
const scriptDAO = new ScriptDAO();
const scriptCodeDAO = new ScriptCodeDAO();
const template = useSearchParams()[0].get("template") || "";
const target = useSearchParams()[0].get("target") || "";
const navigate = useNavigate(); const navigate = useNavigate();
const [visible, setVisible] = useState<{ [key: string]: boolean }>({}); const [visible, setVisible] = useState<{ [key: string]: boolean }>({});
const [editors, setEditors] = useState< const [editors, setEditors] = useState<
@ -164,6 +169,11 @@ function ScriptEditor() {
selectSciptButtonAndTab: string; selectSciptButtonAndTab: string;
}>(); }>();
const { uuid } = useParams(); const { uuid } = useParams();
const { t } = useTranslation();
const template = useSearchParams()[0].get("template") || "";
const target = useSearchParams()[0].get("target") || "";
const scriptDAO = new ScriptDAO();
const scriptCodeDAO = new ScriptCodeDAO();
const setShow = (key: visibleItem, show: boolean) => { const setShow = (key: visibleItem, show: boolean) => {
Object.keys(visible).forEach((k) => { Object.keys(visible).forEach((k) => {
@ -175,17 +185,17 @@ function ScriptEditor() {
const save = (script: Script, e: editor.IStandaloneCodeEditor): Promise<Script> => { const save = (script: Script, e: editor.IStandaloneCodeEditor): Promise<Script> => {
// 解析code生成新的script并更新 // 解析code生成新的script并更新
return new Promise((resolve) => { return new Promise(() => {
prepareScriptByCode(e.getValue(), script.origin || "", script.uuid) prepareScriptByCode(e.getValue(), script.origin || "", script.uuid)
.then((prepareScript) => { .then((prepareScript) => {
const newScript = prepareScript.script; const newScript = prepareScript.script;
scriptCtrl.upsert(newScript).then(
() => {
if (!newScript.name) { if (!newScript.name) {
Message.warning("脚本name不可以设置为空"); Message.warning(t("script_name_cannot_be_set_to_empty"));
return; return;
} }
if (newScript.id === 0) { scriptClient.install(newScript, e.getValue()).then(
(update) => {
if (!update) {
Message.success("新建成功,请注意后台脚本不会默认开启"); Message.success("新建成功,请注意后台脚本不会默认开启");
// 保存的时候如何左侧没有脚本即新建 // 保存的时候如何左侧没有脚本即新建
setScriptList((prev) => { setScriptList((prev) => {
@ -207,17 +217,16 @@ function ScriptEditor() {
setEditors((prev) => { setEditors((prev) => {
for (let i = 0; i < prev.length; i += 1) { for (let i = 0; i < prev.length; i += 1) {
if (prev[i].script.uuid === newScript.uuid) { if (prev[i].script.uuid === newScript.uuid) {
prev[i].code = newScript.code; prev[i].script.code = newScript.code;
prev[i].isChanged = false; prev[i].isChanged = false;
prev[i].script.name = newScript.name; prev[i].script.name = newScript.name;
break; break;
} }
} }
resolve(newScript);
return [...prev]; return [...prev];
}); });
}, },
(err) => { (err: any) => {
Message.error(`保存失败: ${err}`); Message.error(`保存失败: ${err}`);
} }
); );
@ -255,16 +264,18 @@ function ScriptEditor() {
}; };
const menu: EditorMenu[] = [ const menu: EditorMenu[] = [
{ {
title: "文件", title: t("file"),
items: [ items: [
{ {
title: "保存", id: "save",
title: t("save"),
hotKey: KeyMod.CtrlCmd | KeyCode.KeyS, hotKey: KeyMod.CtrlCmd | KeyCode.KeyS,
hotKeyString: "Ctrl+S", hotKeyString: "Ctrl+S",
action: save, action: save,
}, },
{ {
title: "另存为", id: "saveAs",
title: t("save_as"),
hotKey: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyS, hotKey: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyS,
hotKeyString: "Ctrl+Shift+S", hotKeyString: "Ctrl+Shift+S",
action: saveAs, action: saveAs,
@ -272,10 +283,11 @@ function ScriptEditor() {
], ],
}, },
{ {
title: "运行", title: t("run"),
items: [ items: [
{ {
title: "调试", id: "debug",
title: t("debug"),
hotKey: KeyMod.CtrlCmd | KeyCode.F5, hotKey: KeyMod.CtrlCmd | KeyCode.F5,
hotKeyString: "Ctrl+F5", hotKeyString: "Ctrl+F5",
tooltip: "只有后台脚本/定时脚本才能调试, 且调试模式下不对进行权限校验(例如@connect)", tooltip: "只有后台脚本/定时脚本才能调试, 且调试模式下不对进行权限校验(例如@connect)",
@ -312,6 +324,7 @@ function ScriptEditor() {
title: "工具", title: "工具",
items: [ items: [
{ {
id: "scriptStorage",
title: "脚本储存", title: "脚本储存",
tooltip: "可以管理脚本GM_value的储存数据", tooltip: "可以管理脚本GM_value的储存数据",
action(script) { action(script) {
@ -320,6 +333,7 @@ function ScriptEditor() {
}, },
}, },
{ {
id: "scriptResource",
title: "脚本资源", title: "脚本资源",
tooltip: "管理@resource,@require下载的资源", tooltip: "管理@resource,@require下载的资源",
action(script) { action(script) {
@ -353,6 +367,8 @@ function ScriptEditor() {
item.items.forEach((menuItem) => { item.items.forEach((menuItem) => {
if (menuItem.hotKey) { if (menuItem.hotKey) {
hotKeys.push({ hotKeys.push({
id: menuItem.id,
title: menuItem.title,
hotKey: menuItem.hotKey, hotKey: menuItem.hotKey,
action: menuItem.action, action: menuItem.action,
}); });
@ -361,7 +377,6 @@ function ScriptEditor() {
}); });
useEffect(() => { useEffect(() => {
scriptDAO.all().then(async (scripts) => { scriptDAO.all().then(async (scripts) => {
scripts.sort((a, b) => a.sort - b.sort);
setScriptList(scripts); setScriptList(scripts);
// 如果有id则打开对应的脚本 // 如果有id则打开对应的脚本
if (uuid) { if (uuid) {
@ -703,15 +718,20 @@ function ScriptEditor() {
} }
if (!flag) { if (!flag) {
// 如果没有打开则打开 // 如果没有打开则打开
// 获取code
scriptCodeDAO.findByUUID(script.uuid).then((code) => {
if (!code) {
return;
}
editors.push({ editors.push({
script, script: Object.assign(script, code),
code: script.code,
active: true, active: true,
hotKeys, hotKeys,
isChanged: false, isChanged: false,
}); });
}
setEditors([...editors]); setEditors([...editors]);
});
}
}} }}
> >
{script.name} {script.name}
@ -850,7 +870,8 @@ function ScriptEditor() {
display: item.active ? "block" : "none", display: item.active ? "block" : "none",
}} }}
> >
<Editor <WarpEditor
key={`e_${item.script.uuid}`}
id={`e_${item.script.uuid}`} id={`e_${item.script.uuid}`}
script={item.script} script={item.script}
hotKeys={item.hotKeys} hotKeys={item.hotKeys}

View File

@ -8,12 +8,14 @@ import {
SCRIPT_TYPE_BACKGROUND, SCRIPT_TYPE_BACKGROUND,
SCRIPT_TYPE_CRONTAB, SCRIPT_TYPE_CRONTAB,
SCRIPT_TYPE_NORMAL, SCRIPT_TYPE_NORMAL,
ScriptAndCode,
ScriptCode,
ScriptCodeDAO, ScriptCodeDAO,
ScriptDAO, ScriptDAO,
UserConfig, UserConfig,
} from "@App/app/repo/scripts"; } from "@App/app/repo/scripts";
import YAML from "yaml"; import YAML from "yaml";
import { Subscribe, SUBSCRIBE_STATUS_ENABLE, SubscribeDAO } from "@App/app/repo/subscribe"; import { Subscribe, SUBSCRIBE_STATUS_ENABLE, SubscribeDAO, Metadata as SubMetadata } from "@App/app/repo/subscribe";
import { nextTime } from "./utils"; import { nextTime } from "./utils";
import { InstallSource } from "@App/app/service/service_worker"; import { InstallSource } from "@App/app/service/service_worker";
@ -137,7 +139,7 @@ export async function fetchScriptInfo(
return ret; return ret;
} }
export function copyScript(script: Script, old: Script): Script { export function copyScript(script: ScriptAndCode, old: Script): ScriptAndCode {
const ret = script; const ret = script;
ret.uuid = old.uuid; ret.uuid = old.uuid;
ret.createtime = old.createtime; ret.createtime = old.createtime;
@ -204,7 +206,7 @@ export function prepareScriptByCode(
url: string, url: string,
uuid?: string, uuid?: string,
override?: boolean override?: boolean
): Promise<{ script: Script; oldScript?: Script; oldScriptCode?: string }> { ): Promise<{ script: ScriptAndCode; oldScript?: ScriptAndCode }> {
const dao = new ScriptDAO(); const dao = new ScriptDAO();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const metadata = parseMetadata(code); const metadata = parseMetadata(code);
@ -253,9 +255,10 @@ export function prepareScriptByCode(
} else { } else {
newUUID = uuidv4(); newUUID = uuidv4();
} }
let script: Script = { let script: ScriptAndCode = {
uuid: newUUID, uuid: newUUID,
name: metadata.name[0], name: metadata.name[0],
code: code,
author: metadata.author && metadata.author[0], author: metadata.author && metadata.author[0],
namespace: metadata.namespace && metadata.namespace[0], namespace: metadata.namespace && metadata.namespace[0],
originDomain: domain, originDomain: domain,
@ -275,7 +278,7 @@ export function prepareScriptByCode(
}; };
const handler = async () => { const handler = async () => {
let old: Script | undefined; let old: Script | undefined;
let oldCode: string | undefined; let oldCode: ScriptCode | undefined;
if (uuid) { if (uuid) {
old = await dao.get(uuid); old = await dao.get(uuid);
if (!old && override) { if (!old && override) {
@ -293,11 +296,11 @@ export function prepareScriptByCode(
return; return;
} }
const scriptCode = await new ScriptCodeDAO().get(old.uuid); const scriptCode = await new ScriptCodeDAO().get(old.uuid);
if(!scriptCode) { if (!scriptCode) {
reject(new Error("旧的脚本代码不存在")); reject(new Error("旧的脚本代码不存在"));
return; return;
} }
oldCode = scriptCode.code; oldCode = scriptCode;
script = copyScript(script, old); script = copyScript(script, old);
} else { } else {
// 前台脚本默认开启 // 前台脚本默认开启
@ -306,7 +309,7 @@ export function prepareScriptByCode(
} }
script.checktime = new Date().getTime(); script.checktime = new Date().getTime();
} }
resolve({ script, oldScript: old, oldScriptCode: oldCode }); resolve({ script, oldScript: old ? Object.assign(old, oldCode) : undefined });
}; };
handler(); handler();
}); });
@ -317,8 +320,8 @@ export async function prepareSubscribeByCode(
url: string url: string
): Promise<{ subscribe: Subscribe; oldSubscribe?: Subscribe }> { ): Promise<{ subscribe: Subscribe; oldSubscribe?: Subscribe }> {
const dao = new SubscribeDAO(); const dao = new SubscribeDAO();
const metadata = parseMetadata(code); const metadata = parseMetadata(code) as SubMetadata;
if (metadata == null) { if (!metadata) {
throw new Error("MetaData信息错误"); throw new Error("MetaData信息错误");
} }
if (metadata.name === undefined) { if (metadata.name === undefined) {
@ -329,9 +332,9 @@ export async function prepareSubscribeByCode(
url, url,
name: metadata.name[0], name: metadata.name[0],
code, code,
author: metadata.author && metadata.author[0], author: (metadata.author && metadata.author[0]) || "",
scripts: {}, scripts: {},
metadata, metadata: metadata,
status: SUBSCRIBE_STATUS_ENABLE, status: SUBSCRIBE_STATUS_ENABLE,
createtime: Date.now(), createtime: Date.now(),
updatetime: Date.now(), updatetime: Date.now(),