From 5c4ef70bf04652d2522eec6e184505c36e0acec4 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Mon, 16 Feb 2026 01:27:52 +0900 Subject: [PATCH 1/8] =?UTF-8?q?=E4=BC=98=E5=8C=96=20ScriptEditor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/service_worker/client.ts | 18 +- src/app/service/service_worker/script.ts | 30 +- .../options/routes/script/ScriptEditor.tsx | 565 ++++++++---------- 3 files changed, 275 insertions(+), 338 deletions(-) diff --git a/src/app/service/service_worker/client.ts b/src/app/service/service_worker/client.ts index 0961b1f16..fd4349177 100644 --- a/src/app/service/service_worker/client.ts +++ b/src/app/service/service_worker/client.ts @@ -11,7 +11,13 @@ import { type FileSystemType } from "@Packages/filesystem/factory"; import { type ResourceBackup } from "@App/pkg/backup/struct"; import { type VSCodeConnect } from "../offscreen/vscode-connect"; import { type ScriptInfo } from "@App/pkg/utils/scriptInstall"; -import type { ScriptService, TCheckScriptUpdateOption, TOpenBatchUpdatePageOption } from "./script"; +import type { + ScriptService, + TCheckScriptUpdateOption, + TOpenBatchUpdatePageOption, + TScriptInstallParam, + TScriptInstallReturn, +} from "./script"; import { encodeRValue, type TKeyValuePair } from "@App/pkg/utils/message_value"; import { type TSetValuesParams } from "./value"; @@ -40,15 +46,9 @@ export class ScriptClient extends Client { return this.do<[boolean, ScriptInfo, { byWebRequest?: boolean }]>("getInstallInfo", uuid); } - install(params: { - script: Script; - code: string; - upsertBy?: InstallSource; - createtime?: number; - updatetime?: number; - }): Promise<{ update: boolean }> { + install(params: TScriptInstallParam): Promise { if (!params.upsertBy) params.upsertBy = "user"; - return this.doThrow("install", { ...params }); + return this.doThrow("install", { ...params } satisfies TScriptInstallParam); } // delete(uuid: string) { diff --git a/src/app/service/service_worker/script.ts b/src/app/service/service_worker/script.ts index 227be28d1..e2cd92c01 100644 --- a/src/app/service/service_worker/script.ts +++ b/src/app/service/service_worker/script.ts @@ -54,6 +54,19 @@ export type TCheckScriptUpdateOption = Partial< export type TOpenBatchUpdatePageOption = { q: string; dontCheckNow: boolean }; +export type TScriptInstallParam = { + script: Script; // 脚本信息(包含脚本的基础元数据) + code: string; // 脚本源码内容 + upsertBy?: InstallSource; // 安装/更新来源(用于标识脚本来源渠道) + createtime?: number; // 导入时指定的创建时间(时间戳,毫秒) + updatetime?: number; // 导入时指定的最后更新时间(时间戳,毫秒) +}; + +export type TScriptInstallReturn = { + update: boolean; // 是否为更新操作(true 表示更新,false 表示新增) + updatetime: number | undefined; // 实际生效的更新时间(时间戳,毫秒) +}; + export class ScriptService { logger: Logger; scriptCodeDAO: ScriptCodeDAO = new ScriptCodeDAO(); @@ -369,14 +382,8 @@ export class ScriptService { return this.mq.publish("installScript", { script, ...options }); } - // 安装脚本 / 更新腳本 - async installScript(param: { - script: Script; - code: string; - upsertBy?: InstallSource; - createtime?: number; - updatetime?: number; - }) { + // 安装脚本 / 更新脚本 + async installScript(param: TScriptInstallParam): Promise { param.upsertBy = param.upsertBy || "user"; const { script, upsertBy, createtime, updatetime } = param; // 删 storage cache @@ -427,10 +434,11 @@ export class ScriptService { ]); // 广播一下 - // Runtime 會負責更新 CompiledResource + // Runtime 会负责更新 CompiledResource this.publishInstallScript(script, { update, upsertBy }); - return { update }; + // 传回(由后台控制的)实际更新时间,让 editor 中的script能保持正确的更新时间 + return { update, updatetime: script.updatetime }; }) .catch((e: any) => { logger.error("install error", Logger.E(e)); @@ -1144,7 +1152,7 @@ export class ScriptService { } isInstalled({ name, namespace }: { name: string; namespace: string }): Promise { - // 用於 window.external + // 用于 window.external return this.scriptDAO.findByNameAndNamespace(name, namespace).then((script) => { if (script) { return { diff --git a/src/pages/options/routes/script/ScriptEditor.tsx b/src/pages/options/routes/script/ScriptEditor.tsx index 08120b226..bb0cf32e5 100644 --- a/src/pages/options/routes/script/ScriptEditor.tsx +++ b/src/pages/options/routes/script/ScriptEditor.tsx @@ -1,8 +1,8 @@ import type { Script } from "@App/app/repo/scripts"; import { SCRIPT_TYPE_NORMAL, ScriptCodeDAO, ScriptDAO } from "@App/app/repo/scripts"; import CodeEditor from "@App/pages/components/CodeEditor"; -import React, { useCallback, useEffect, useMemo, useState } from "react"; -import { useParams, useSearchParams } from "react-router-dom"; +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useNavigate, useParams, useSearchParams } from "react-router-dom"; import type { editor } from "monaco-editor"; import { KeyCode, KeyMod } from "monaco-editor"; import { Button, Dropdown, Grid, Input, Menu, Message, Modal, Tabs, Tooltip } from "@arco-design/web-react"; @@ -31,18 +31,18 @@ type HotKey = { id: string; title: string; hotKey: number; - action: (script: Script, codeEditor: editor.IStandaloneCodeEditor) => void; + action: (script: Script, codeEditor: editor.ICodeEditor) => void; }; const Editor: React.FC<{ id: string; - script: Script; + getScript: (uuid: string) => Script | undefined; code: string; hotKeys: HotKey[]; - callbackEditor: (e: editor.IStandaloneCodeEditor) => void; + callbackEditor: (e: editor.ICodeEditor) => void; onChange: (code: string) => void; className: string; -}> = ({ id, script, code, hotKeys, callbackEditor, onChange, className }) => { +}> = ({ id, getScript, code, hotKeys, callbackEditor, onChange, className }) => { const [node, setNode] = useState<{ editor: editor.IStandaloneCodeEditor }>(); const ref = useCallback<(node: { editor: editor.IStandaloneCodeEditor }) => void>( (inlineNode) => { @@ -59,7 +59,7 @@ const Editor: React.FC<{ // @ts-ignore if (!node.editor.uuid) { // @ts-ignore - node.editor.uuid = script.uuid; + node.editor.uuid = id; } hotKeys.forEach((item) => { node.editor.addAction({ @@ -67,8 +67,10 @@ const Editor: React.FC<{ label: item.title, keybindings: [item.hotKey], run(editor) { - // @ts-ignore - item.action(script, editor); + const script = getScript(id); + if (script) { + item.action(script, editor); + } }, }); }); @@ -83,20 +85,20 @@ const Editor: React.FC<{ }; const WarpEditor = React.memo(Editor, (prev, next) => { - return prev.script.uuid === next.script.uuid; + return prev.id === next.id; }); type EditorMenu = { title: string; tooltip?: string; - action?: (script: Script, e: editor.IStandaloneCodeEditor) => void; + action?: (script: Script, e: editor.ICodeEditor) => void; items?: { id: string; title: string; tooltip?: string; hotKey?: number; hotKeyString?: string; - action: (script: Script, e: editor.IStandaloneCodeEditor) => void; + action: (script: Script, e: editor.ICodeEditor) => void; }[]; }; @@ -170,6 +172,8 @@ const emptyScript = async (template: string, hotKeys: any, target?: string) => { type visibleItem = "scriptStorage" | "scriptSetting" | "scriptResource"; +let cid: ReturnType; + const popstate = () => { if (confirm(i18n.t("script_modified_leave_confirm"))) { window.history.back(); @@ -180,39 +184,95 @@ const popstate = () => { return false; }; +type EditorState = { + script: Script; + code: string; + active: boolean; + hotKeys: HotKey[]; + editor?: editor.ICodeEditor; + isChanged: boolean; +}; + +const scriptDAO = new ScriptDAO(); +const scriptCodeDAO = new ScriptCodeDAO(); + function ScriptEditor() { const [visible, setVisible] = useState<{ [key: string]: boolean }>({}); const [searchKeyword, setSearchKeyword] = useState(""); const [showSearchInput, setShowSearchInput] = useState(false); const [modal, contextHolder] = Modal.useModal(); - const [editors, setEditors] = useState< - { - script: Script; - code: string; - active: boolean; - hotKeys: HotKey[]; - editor?: editor.IStandaloneCodeEditor; - isChanged: boolean; - }[] - >([]); + const [editors, setEditors] = useState([]); + const editorsRef = useRef(editors); // 取出资料用 + // Sync during render (no useEffect needed) + editorsRef.current = editors; + // The function identity is now permanent (empty dependency array) + const getScript = useCallback((uuid: string) => { + return editorsRef.current!.find((e) => e.script.uuid === uuid)?.script; + }, []); const [scriptList, setScriptList] = useState([]); const [currentScript, setCurrentScript] = useState