编辑器
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 { rspack } from "@rspack/core";
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 isBeta = version.includes("-");
console.log(CompressionPlugin);
// Target browsers, see: https://github.com/browserslist/browserslist
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);
}
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 });
}

View File

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

View File

@ -31,6 +31,10 @@
"sync_delete": "同步删除",
"enable_script_sync_to": "启用脚本同步至",
"save": "保存",
"save_as": "另存为",
"file": "文件",
"run": "运行",
"debug": "调试",
"cloud_sync_account_verification": "云同步账号信息验证中...",
"cloud_sync_verification_failed": "云同步账号信息验证失败",
"save_success": "保存成功",
@ -358,5 +362,6 @@
"collapse": "收起",
"expand": "展开",
"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;
};
const CodeEditor: React.ForwardRefRenderFunction<{ editor: editor.ICodeEditor | undefined }, Props> = (
const CodeEditor: React.ForwardRefRenderFunction<{ editor: editor.IStandaloneCodeEditor | undefined }, Props> = (
{ id, className, code, diffCode, editable },
ref
) => {
const settings = useAppSelector((state) => state.setting);
const [monacoEditor, setEditor] = useState<editor.ICodeEditor>();
const [monacoEditor, setEditor] = useState<editor.IStandaloneCodeEditor>();
const div = useRef<HTMLDivElement>(null);
useImperativeHandle(ref, () => ({
editor: monacoEditor,

View File

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

View File

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

View File

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