Skip to content

Commit d9b0eee

Browse files
committed
✨ 安装本地脚本时可以进行监听 #275
1 parent 06a7a01 commit d9b0eee

6 files changed

Lines changed: 169 additions & 69 deletions

File tree

src/locales/zh-CN/translation.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -418,5 +418,8 @@
418418
"sync_status": "同步状态",
419419
"search_scripts": "搜索脚本",
420420
"ext_update_notification": "脚本猫扩展已更新",
421-
"ext_update_notification_desc": "当前版本:{{version}},详情请查看更新日志"
421+
"ext_update_notification_desc": "当前版本:{{version}},详情请查看更新日志",
422+
"watch_file_description": "监听文件变动,自动更新脚本,使用时请确保脚本文件路径不变且不能关闭页面",
423+
"watch_file": "监听文件",
424+
"stop_watch_file": "停止监听"
422425
}

src/pages/components/layout/MainLayout.tsx

Lines changed: 61 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -34,29 +34,8 @@ import { systemConfig } from "@App/pages/store/global";
3434
import i18n, { matchLanguage } from "@App/locales/locales";
3535
import "./index.css";
3636
import { arcoLocale } from "@App/locales/arco";
37-
38-
const readFile = (file: File): Promise<string> => {
39-
return new Promise((resolve) => {
40-
// 实例化 FileReader对象
41-
const reader = new FileReader();
42-
reader.onload = async (processEvent) => {
43-
// 创建blob url
44-
const blob = new Blob([processEvent.target!.result!], {
45-
type: "application/javascript",
46-
});
47-
const url = URL.createObjectURL(blob);
48-
resolve(url);
49-
};
50-
// 调用readerAsText方法读取文本
51-
reader.readAsText(file);
52-
});
53-
};
54-
55-
const uploadFiles = async (files: File[], importByUrlsFunc: (urls: string[]) => Promise<void>) => {
56-
// const filterFiles = files.filter((f) => f.name.endsWith(".js"));
57-
const urls = await Promise.all(files.map((file) => readFile(file)));
58-
importByUrlsFunc(urls);
59-
};
37+
import { prepareScriptByCode } from "@App/pkg/utils/script";
38+
import type { ScriptClient } from "@App/app/service/service_worker/client";
6039

6140
const MainLayout: React.FC<{
6241
children: ReactNode;
@@ -70,38 +49,72 @@ const MainLayout: React.FC<{
7049
const [showLanguage, setShowLanguage] = useState(false);
7150
const { t } = useTranslation();
7251

52+
const showImportResult = (stat: Awaited<ReturnType<ScriptClient["importByUrls"]>>) => {
53+
if (!stat) return;
54+
Modal.info({
55+
title: t("script_import_result"),
56+
content: (
57+
<Space direction="vertical" style={{ width: "100%" }}>
58+
<div style={{ textAlign: "center" }}>
59+
<Space size="small" style={{ fontSize: 18 }}>
60+
<IconCheckCircle style={{ color: "green" }} />
61+
{stat.success}
62+
{""}
63+
<IconCloseCircle style={{ color: "red" }} />
64+
{stat.fail}
65+
</Space>
66+
</div>
67+
{stat.msg.length > 0 && (
68+
<>
69+
<b>{t("failure_info")}:</b>
70+
{stat.msg}
71+
</>
72+
)}
73+
</Space>
74+
),
75+
});
76+
};
77+
7378
const importByUrlsLocal = async (urls: string[]) => {
7479
const stat = await scriptClient.importByUrls(urls);
75-
stat &&
76-
Modal.info({
77-
title: t("script_import_result"),
78-
content: (
79-
<Space direction="vertical" style={{ width: "100%" }}>
80-
<div style={{ textAlign: "center" }}>
81-
<Space size="small" style={{ fontSize: 18 }}>
82-
<IconCheckCircle style={{ color: "green" }} />
83-
{stat.success}
84-
{""}
85-
<IconCloseCircle style={{ color: "red" }} />
86-
{stat.fail}
87-
</Space>
88-
</div>
89-
{stat.msg.length > 0 && (
90-
<>
91-
<b>{t("failure_info")}:</b>
92-
{stat.msg}
93-
</>
94-
)}
95-
</Space>
96-
),
97-
});
80+
stat && showImportResult(stat);
9881
};
9982

10083
const { getRootProps, getInputProps, isDragActive } = useDropzone({
10184
accept: { "application/javascript": [".js"] },
10285
onDrop: (acceptedFiles) => {
103-
console.log(acceptedFiles);
104-
uploadFiles(acceptedFiles, importByUrlsLocal);
86+
// 本地的文件在当前页面处理,打开安装页面,将FileSystemFileHandle传递过去
87+
// 实现本地文件的监听
88+
const stat: Awaited<ReturnType<ScriptClient["importByUrls"]>> = { success: 0, fail: 0, msg: [] };
89+
Promise.all(
90+
acceptedFiles.map(async (file) => {
91+
// 解析看看是不是一个标准的script文件
92+
// 如果是,则打开安装页面
93+
const code = await file.text();
94+
try {
95+
const resp = await prepareScriptByCode(code, "file://-/" + file.name);
96+
if (resp) {
97+
// 打开安装页面
98+
const installWindow = window.open(`/src/install.html?local=true`, "_blank");
99+
if (!installWindow) {
100+
throw new Error(t("install_page_open_failed"));
101+
}
102+
installWindow.onload = () =>
103+
//@ts-ignore
104+
installWindow.postMessage({ type: "file", file, fileHandle: file.handle }, "*");
105+
stat.success++;
106+
} else {
107+
stat.fail++;
108+
stat.msg.push(t("script_import_failed"));
109+
}
110+
} catch (e: any) {
111+
stat.fail++;
112+
stat.msg.push(e.message);
113+
}
114+
})
115+
).then(() => {
116+
showImportResult(stat);
117+
});
105118
},
106119
});
107120

@@ -164,7 +177,6 @@ const MainLayout: React.FC<{
164177
placeholder={t("import_script_placeholder")}
165178
defaultValue=""
166179
onKeyDown={(e) => {
167-
console.log(e);
168180
if (e.ctrlKey && e.key === "Enter") {
169181
e.preventDefault();
170182
handleImport();

src/pages/install/App.tsx

Lines changed: 73 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ import {
1010
Tag,
1111
Tooltip,
1212
Typography,
13+
Popover,
1314
} from "@arco-design/web-react";
1415
import { IconDown } from "@arco-design/web-react/icon";
16+
import { v4 as uuidv4 } from "uuid";
1517
import CodeEditor from "../components/CodeEditor";
1618
import { useCallback, useEffect, useMemo, useState } from "react";
1719
import type { Metadata, Script } from "@App/app/repo/scripts";
@@ -20,7 +22,7 @@ import type { Subscribe } from "@App/app/repo/subscribe";
2022
import { i18nDescription, i18nName } from "@App/locales/locales";
2123
import { useTranslation } from "react-i18next";
2224
import type { ScriptInfo } from "@App/pkg/utils/script";
23-
import { prepareScriptByCode, prepareSubscribeByCode } from "@App/pkg/utils/script";
25+
import { prepareScriptByCode, prepareSubscribeByCode, scriptInfoByCode } from "@App/pkg/utils/script";
2426
import { nextTime } from "@App/pkg/utils/cron";
2527
import { scriptClient, subscribeClient } from "../store/features/script";
2628

@@ -45,20 +47,34 @@ const useScriptInstall = () => {
4547
const [diffCode, setDiffCode] = useState<string>();
4648
const [oldScript, setOldScript] = useState<Script | Subscribe>();
4749
const [isUpdate, setIsUpdate] = useState<boolean>(false);
50+
const [localFile, setLocalFile] = useState<File | null>(null);
51+
const [localFileHandle, setLocalFileHandle] = useState<FileSystemFileHandle | null>(null);
4852
const { t } = useTranslation();
4953

5054
useEffect(() => {
51-
const url = new URL(window.location.href);
52-
const uuid = url.searchParams.get("uuid");
53-
if (!uuid) {
54-
return;
55-
}
56-
57-
const loadScriptInfo = async () => {
55+
const handle = async () => {
5856
try {
59-
const info: ScriptInfo = await scriptClient.getInstallInfo(uuid);
60-
if (!info) {
61-
throw new Error("fetch script info failed");
57+
const url = new URL(window.location.href);
58+
const uuid = url.searchParams.get("uuid");
59+
let info: ScriptInfo | undefined;
60+
if (!uuid) {
61+
// 检查是不是本地文件安装
62+
const local = url.searchParams.get("local") === "true";
63+
if (!local || !window.localFile) {
64+
return;
65+
}
66+
// 处理本地文件的安装流程
67+
// 处理成info对象
68+
const file = window.localFile;
69+
setLocalFile(file);
70+
setLocalFileHandle(window.localFileHandle!);
71+
const code = await file.text();
72+
info = scriptInfoByCode(code, "file:///*from-local*/" + file.name, "user", false, uuidv4());
73+
} else {
74+
info = await scriptClient.getInstallInfo(uuid);
75+
if (!info) {
76+
throw new Error("fetch script info failed");
77+
}
6278
}
6379

6480
let prepare:
@@ -96,8 +112,7 @@ const useScriptInstall = () => {
96112
Message.error(t("script_info_load_failed") + " " + e.message);
97113
}
98114
};
99-
100-
loadScriptInfo();
115+
handle();
101116
}, [t]);
102117

103118
return {
@@ -108,6 +123,8 @@ const useScriptInstall = () => {
108123
diffCode,
109124
oldScript,
110125
isUpdate,
126+
localFile,
127+
localFileHandle,
111128
};
112129
};
113130

@@ -226,9 +243,13 @@ const useAntiFeatures = () => {
226243
function App() {
227244
const [enable, setEnable] = useState<boolean>(false);
228245
const [btnText, setBtnText] = useState<string>("");
246+
const [scriptCode, setScriptCode] = useState<string>("");
229247
const { t } = useTranslation();
230248

231-
const { scriptInfo, upsertScript, setUpsertScript, code, diffCode, oldScript, isUpdate } = useScriptInstall();
249+
const { scriptInfo, upsertScript, setUpsertScript, code, diffCode, oldScript, isUpdate, localFile, localFileHandle } =
250+
useScriptInstall();
251+
252+
const [watchFile, setWatchFile] = useState(false);
232253

233254
const metadata: Metadata = scriptInfo?.metadata || {};
234255
const permissions = usePermissions(scriptInfo, metadata);
@@ -242,7 +263,7 @@ function App() {
242263
} else {
243264
setBtnText(isUpdate ? t("update_script")! : t("install_script"));
244265
}
245-
266+
setScriptCode(code || "");
246267
if (upsertScript) {
247268
document.title = `${!isUpdate ? t("install_script") : t("update_script")} - ${i18nName(upsertScript)} - ScriptCat`;
248269
}
@@ -324,12 +345,34 @@ function App() {
324345
[scriptInfo]
325346
);
326347

348+
useEffect(() => {
349+
if (!watchFile) {
350+
return;
351+
}
352+
if (!upsertScript) {
353+
return;
354+
}
355+
// @ts-ignore
356+
const observer = new FileSystemObserver(async (records) => {
357+
// 调用安装
358+
const code = await (await records[0].root.getFile()).text();
359+
setScriptCode(code);
360+
scriptClient.install(upsertScript as Script, code).catch((e) => {
361+
Message.error(t("install_failed") + ": " + e);
362+
});
363+
});
364+
observer.observe(localFileHandle);
365+
return () => {
366+
observer.disconnect();
367+
};
368+
}, [watchFile]);
369+
327370
return (
328371
<div className="h-full">
329372
<div className="h-full">
330373
<Grid.Row className="mb-2" gutter={8}>
331374
<Grid.Col flex={1} className="flex-col p-8px">
332-
<Space direction="vertical">
375+
<Space direction="vertical" className="w-full">
333376
<div>
334377
{upsertScript?.metadata.icon && (
335378
<Avatar size={32} shape="square" style={{ marginRight: "8px" }}>
@@ -391,6 +434,19 @@ function App() {
391434
<Button type="primary" size="small" icon={<IconDown />} />
392435
</Dropdown>
393436
</Button.Group>
437+
{localFile && (
438+
<Popover content={t("watch_file_description")}>
439+
<Button
440+
type="secondary"
441+
size="small"
442+
onClick={() => {
443+
setWatchFile(!watchFile);
444+
}}
445+
>
446+
{watchFile ? t("stop_watch_file") : t("watch_file")}
447+
</Button>
448+
</Popover>
449+
)}
394450
{isUpdate ? (
395451
<Button.Group>
396452
<Button type="primary" status="danger" size="small" onClick={() => handleClose()}>
@@ -498,7 +554,7 @@ function App() {
498554
</Grid.Row>
499555
</Grid.Col>
500556
</Grid.Row>
501-
<CodeEditor id="show-code" code={code || undefined} diffCode={diffCode || ""} />
557+
<CodeEditor id="show-code" code={scriptCode || undefined} diffCode={diffCode || ""} />
502558
</div>
503559
</div>
504560
);

src/pages/install/main.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,19 @@ const loggerCore = new LoggerCore({
1919

2020
loggerCore.logger().debug("install page start");
2121

22+
// 接收FileSystemType
23+
window.addEventListener(
24+
"message",
25+
(event) => {
26+
if (event.data && event.data.type === "file") {
27+
// 将FileSystemType存储到全局变量中
28+
window.localFile = event.data.file;
29+
window.localFileHandle = event.data.fileHandle;
30+
}
31+
},
32+
false
33+
);
34+
2235
const Root = (
2336
<Provider store={store}>
2437
<MainLayout className="!flex-col !px-4 box-border">

src/pkg/utils/script.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,13 +85,24 @@ export async function fetchScriptInfo(
8585
}
8686

8787
const body = await resp.text();
88-
const parse = parseMetadata(body);
88+
return scriptInfoByCode(body, url, source, update, uuid);
89+
}
90+
91+
// 通过脚本代码处理成脚本info
92+
export function scriptInfoByCode(
93+
code: string,
94+
url: string,
95+
source: InstallSource,
96+
update: boolean,
97+
uuid: string
98+
): ScriptInfo {
99+
const parse = parseMetadata(code);
89100
if (!parse) {
90101
throw new Error("parse script info failed");
91102
}
92103
const ret: ScriptInfo = {
93104
url,
94-
code: body,
105+
code,
95106
source,
96107
update,
97108
uuid,

src/types/main.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ declare const sandbox: Window;
77

88
declare const self: ServiceWorkerGlobalScope;
99

10+
declare interface Window {
11+
localFile: File | undefined;
12+
localFileHandle: FileSystemFileHandle | undefined;
13+
}
14+
1015
declare const MessageFlag: string;
1116

1217
// 可以让content与inject环境交换携带dom的对象

0 commit comments

Comments
 (0)