消息通讯
This commit is contained in:
parent
eeb343bc5f
commit
78152222f3
32
packages/message/client.ts
Normal file
32
packages/message/client.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
export function sendMessage(action: string, params?: any): Promise<any> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
chrome.runtime.sendMessage({ action, data: params }, (res) => {
|
||||||
|
if (res.code) {
|
||||||
|
console.error(res);
|
||||||
|
reject(res.message);
|
||||||
|
} else {
|
||||||
|
resolve(res.data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function connect(action: string, params?: any): Promise<chrome.runtime.Port> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const port = chrome.runtime.connect();
|
||||||
|
port.postMessage({ action, data: params });
|
||||||
|
resolve(port);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Client {
|
||||||
|
constructor(private prefix: string) {
|
||||||
|
if (!this.prefix.endsWith("/")) {
|
||||||
|
this.prefix += "/";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
do(action: string, params?: any): Promise<any> {
|
||||||
|
return sendMessage(this.prefix + action, params);
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +1,12 @@
|
|||||||
import { ApiFunction } from "./server";
|
import { connect } from "./client";
|
||||||
|
import { ApiFunction, Server } from "./server";
|
||||||
|
|
||||||
export class Broker {
|
export class Broker {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
// 订阅
|
// 订阅
|
||||||
subscribe(topic: string, handler: (message: any) => void) {
|
async subscribe(topic: string, handler: (message: any) => void) {
|
||||||
const con = chrome.runtime.connect({ name: topic });
|
const con = await connect("messageQueue", { action: "subscribe", topic });
|
||||||
con.postMessage({ action: "subscribe", topic });
|
|
||||||
con.onMessage.addListener((msg: { action: string; topic: string; message: any }) => {
|
con.onMessage.addListener((msg: { action: string; topic: string; message: any }) => {
|
||||||
if (msg.action === "message") {
|
if (msg.action === "message") {
|
||||||
handler(msg.message);
|
handler(msg.message);
|
||||||
@ -24,6 +24,10 @@ export class Broker {
|
|||||||
export class MessageQueue {
|
export class MessageQueue {
|
||||||
topicConMap: Map<string, { name: string; con: chrome.runtime.Port }[]> = new Map();
|
topicConMap: Map<string, { name: string; con: chrome.runtime.Port }[]> = new Map();
|
||||||
|
|
||||||
|
constructor(api: Server) {
|
||||||
|
api.on("messageQueue", this.handler());
|
||||||
|
}
|
||||||
|
|
||||||
handler(): ApiFunction {
|
handler(): ApiFunction {
|
||||||
return ({ action, topic, message }: { action: string; topic: string; message: any }, con) => {
|
return ({ action, topic, message }: { action: string; topic: string; message: any }, con) => {
|
||||||
if (!con) {
|
if (!con) {
|
||||||
@ -53,7 +57,10 @@ export class MessageQueue {
|
|||||||
}
|
}
|
||||||
list.push({ name: topic, con });
|
list.push({ name: topic, con });
|
||||||
con.onDisconnect.addListener(() => {
|
con.onDisconnect.addListener(() => {
|
||||||
|
let list = this.topicConMap.get(topic);
|
||||||
|
// 移除断开连接的con
|
||||||
list = list!.filter((item) => item.con !== con);
|
list = list!.filter((item) => item.con !== con);
|
||||||
|
this.topicConMap.set(topic, list);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,18 +5,22 @@ export class Server {
|
|||||||
|
|
||||||
constructor(private env: string) {
|
constructor(private env: string) {
|
||||||
chrome.runtime.onConnect.addListener((port) => {
|
chrome.runtime.onConnect.addListener((port) => {
|
||||||
const handler = (msg: { action: string }) => {
|
const handler = (msg: { action: string; data: any }) => {
|
||||||
port.onMessage.removeListener(handler);
|
port.onMessage.removeListener(handler);
|
||||||
this.connectHandle(msg.action, msg, port);
|
this.connectHandle(msg.action, msg.data, port);
|
||||||
};
|
};
|
||||||
port.onMessage.addListener(handler);
|
port.onMessage.addListener(handler);
|
||||||
});
|
});
|
||||||
|
|
||||||
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||||
this.messageHandle(msg.action, msg, sender, sendResponse);
|
this.messageHandle(msg.action, msg.data, sender, sendResponse);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
group(name: string) {
|
||||||
|
return new Group(this, name);
|
||||||
|
}
|
||||||
|
|
||||||
on(name: string, func: ApiFunction) {
|
on(name: string, func: ApiFunction) {
|
||||||
this.apiFunctionMap.set(name, func);
|
this.apiFunctionMap.set(name, func);
|
||||||
}
|
}
|
||||||
@ -45,3 +49,22 @@ export class Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class Group {
|
||||||
|
constructor(
|
||||||
|
private server: Server,
|
||||||
|
private name: string
|
||||||
|
) {
|
||||||
|
if (!name.endsWith("/")) {
|
||||||
|
this.name += "/";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
group(name: string) {
|
||||||
|
return new Group(this.server, `${this.name}${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
on(name: string, func: ApiFunction) {
|
||||||
|
this.server.on(`${this.name}${name}`, func);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -28,6 +28,7 @@ export default defineConfig({
|
|||||||
sandbox: `${src}/sandbox.ts`,
|
sandbox: `${src}/sandbox.ts`,
|
||||||
popup: `${src}/pages/popup/main.tsx`,
|
popup: `${src}/pages/popup/main.tsx`,
|
||||||
install: `${src}/pages/install/main.tsx`,
|
install: `${src}/pages/install/main.tsx`,
|
||||||
|
options: `${src}/pages/options/main.tsx`,
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
path: `${dist}/ext/src`,
|
path: `${dist}/ext/src`,
|
||||||
@ -130,12 +131,20 @@ export default defineConfig({
|
|||||||
}),
|
}),
|
||||||
new rspack.HtmlRspackPlugin({
|
new rspack.HtmlRspackPlugin({
|
||||||
filename: `${dist}/ext/src/install.html`,
|
filename: `${dist}/ext/src/install.html`,
|
||||||
template: `${src}/pages/install/index.html`,
|
template: `${src}/pages/template.html`,
|
||||||
inject: "head",
|
inject: "head",
|
||||||
title: "Install - ScriptCat",
|
title: "Install - ScriptCat",
|
||||||
minify: true,
|
minify: true,
|
||||||
chunks: ["install"],
|
chunks: ["install"],
|
||||||
}),
|
}),
|
||||||
|
new rspack.HtmlRspackPlugin({
|
||||||
|
filename: `${dist}/ext/src/options.html`,
|
||||||
|
template: `${src}/pages/template.html`,
|
||||||
|
inject: "head",
|
||||||
|
title: "Home - ScriptCat",
|
||||||
|
minify: true,
|
||||||
|
chunks: ["options"],
|
||||||
|
}),
|
||||||
new rspack.HtmlRspackPlugin({
|
new rspack.HtmlRspackPlugin({
|
||||||
filename: `${dist}/ext/src/popup.html`,
|
filename: `${dist}/ext/src/popup.html`,
|
||||||
template: `${src}/pages/popup/index.html`,
|
template: `${src}/pages/popup/index.html`,
|
||||||
|
@ -1,43 +1,6 @@
|
|||||||
// 缓存key,所有缓存相关的key都需要定义在此
|
|
||||||
// 使用装饰器维护缓存值
|
|
||||||
import { ConfirmParam } from "@App/runtime/background/permission_verify";
|
|
||||||
|
|
||||||
export default class CacheKey {
|
export default class CacheKey {
|
||||||
// 缓存触发器
|
// 加载脚本信息时的缓存
|
||||||
static Trigger(): (target: unknown, propertyName: string, descriptor: PropertyDescriptor) => void {
|
static scriptInstallInfo(uuid: string): string {
|
||||||
return (target, propertyName, descriptor) => {
|
|
||||||
descriptor.value();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 脚本缓存
|
|
||||||
static script(id: number): string {
|
|
||||||
return `script:${id.toString()}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载脚本信息时的缓存,已处理删除
|
|
||||||
static scriptInfo(uuid: string): string {
|
|
||||||
return `scriptInfo:${uuid}`;
|
return `scriptInfo:${uuid}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 脚本资源url缓存,可能存在泄漏
|
|
||||||
static resourceByUrl(url: string): string {
|
|
||||||
return `resource:${url}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 脚本value缓存,可能存在泄漏
|
|
||||||
static scriptValue(id: number, storagename?: string[]): string {
|
|
||||||
if (storagename) {
|
|
||||||
return `value:storagename:${storagename[0]}`;
|
|
||||||
}
|
|
||||||
return `value:id:${id.toString()}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static permissionConfirm(scriptId: number, confirm: ConfirmParam): string {
|
|
||||||
return `permission:${scriptId.toString()}:${confirm.permissionValue || ""}:${confirm.permission || ""}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static importInfo(uuid: string): string {
|
|
||||||
return `import:${uuid}`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import EventEmitter from "eventemitter3";
|
|
||||||
import Logger from "./logger";
|
import Logger from "./logger";
|
||||||
|
|
||||||
export type LogLevel = "debug" | "info" | "warn" | "error";
|
export type LogLevel = "debug" | "info" | "warn" | "error";
|
||||||
@ -20,7 +19,7 @@ export default class LoggerCore {
|
|||||||
return LoggerCore.instance;
|
return LoggerCore.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
static getLogger(...label: LogLabel[]) {
|
static logger(...label: LogLabel[]) {
|
||||||
return LoggerCore.getInstance().logger(...label);
|
return LoggerCore.getInstance().logger(...label);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,6 +44,4 @@ export default class LoggerCore {
|
|||||||
logger(...label: LogLabel[]) {
|
logger(...label: LogLabel[]) {
|
||||||
return new Logger(this, this.labels, ...label);
|
return new Logger(this, this.labels, ...label);
|
||||||
}
|
}
|
||||||
|
|
||||||
static EE = new EventEmitter();
|
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
/* eslint-disable no-console */
|
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import LoggerCore, { LogLabel, LogLevel } from "./core";
|
import LoggerCore, { LogLabel, LogLevel } from "./core";
|
||||||
|
|
||||||
@ -54,7 +53,6 @@ export default class Logger {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LoggerCore.EE.emit("log", { level, message, label });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
with(...label: LogLabel[]) {
|
with(...label: LogLabel[]) {
|
||||||
|
@ -32,6 +32,10 @@ function renameField(): void {
|
|||||||
// export是0.10.x时的兼容性处理
|
// export是0.10.x时的兼容性处理
|
||||||
export: "++id,&scriptId",
|
export: "++id,&scriptId",
|
||||||
});
|
});
|
||||||
|
// 将脚本数据迁移到chrome.storage
|
||||||
|
// db.version(18)
|
||||||
|
// .stores({})
|
||||||
|
// .upgrade((tx) => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function migrate() {
|
export default function migrate() {
|
||||||
|
58
src/app/repo/repo.ts
Normal file
58
src/app/repo/repo.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
export abstract class Repo<T> {
|
||||||
|
constructor(private prefix: string) {
|
||||||
|
if (!prefix.endsWith(":")) {
|
||||||
|
prefix += ":";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private joinKey(key: string) {
|
||||||
|
return this.prefix + key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async _save(key: string, val: T) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const data = {
|
||||||
|
[this.joinKey(key)]: val,
|
||||||
|
};
|
||||||
|
chrome.storage.local.set(data, () => {
|
||||||
|
resolve(val);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public get(key: string): Promise<T | undefined> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
key = this.joinKey(key);
|
||||||
|
chrome.storage.local.get(key, (result) => {
|
||||||
|
resolve(result[key]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public find(filters?: (key: string, value: T) => boolean): Promise<T[]> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
chrome.storage.local.get((result) => {
|
||||||
|
const ret = [];
|
||||||
|
for (const key in result) {
|
||||||
|
if (key.startsWith(this.prefix) && (!filters || filters(key, result[key]))) {
|
||||||
|
ret.push(result[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolve(ret);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
findOne(filters?: (key: string, value: T) => boolean): Promise<T | undefined> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
chrome.storage.local.get((result) => {
|
||||||
|
for (const key in result) {
|
||||||
|
if (key.startsWith(this.prefix) && (!filters || filters(key, result[key]))) {
|
||||||
|
return resolve(result[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolve(undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import { DAO, db } from "./dao";
|
import { Repo } from "./repo";
|
||||||
import { Resource } from "./resource";
|
import { Resource } from "./resource";
|
||||||
import { Value } from "./value";
|
import { Value } from "./value";
|
||||||
|
|
||||||
@ -26,14 +26,7 @@ export const SCRIPT_RUN_STATUS_RETRY: SCRIPT_RUN_STATUS = "retry";
|
|||||||
|
|
||||||
export type Metadata = { [key: string]: string[] };
|
export type Metadata = { [key: string]: string[] };
|
||||||
|
|
||||||
export type ConfigType =
|
export type ConfigType = "text" | "checkbox" | "select" | "mult-select" | "number" | "textarea" | "time";
|
||||||
| "text"
|
|
||||||
| "checkbox"
|
|
||||||
| "select"
|
|
||||||
| "mult-select"
|
|
||||||
| "number"
|
|
||||||
| "textarea"
|
|
||||||
| "time";
|
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
@ -88,34 +81,40 @@ export interface ScriptRunResouce extends Script {
|
|||||||
sourceCode: string;
|
sourceCode: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ScriptDAO extends DAO<Script> {
|
export class ScriptDAO extends Repo<Script> {
|
||||||
public tableName = "scripts";
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super("script");
|
||||||
this.table = db.table(this.tableName);
|
}
|
||||||
|
|
||||||
|
public save(val: Script) {
|
||||||
|
return super._save(val.uuid, val);
|
||||||
}
|
}
|
||||||
|
|
||||||
public findByName(name: string) {
|
public findByName(name: string) {
|
||||||
return this.findOne({ name });
|
return this.findOne((key, value) => {
|
||||||
|
return value.name === name;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public findByNameAndNamespace(name: string, namespace?: string) {
|
public findByNameAndNamespace(name: string, namespace?: string) {
|
||||||
if (namespace) {
|
return this.findOne((key, value) => {
|
||||||
return this.findOne({ name, namespace });
|
return value.name === name && (!namespace || value.namespace === namespace);
|
||||||
}
|
});
|
||||||
return this.findOne({ name });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public findByUUID(uuid: string) {
|
public findByUUID(uuid: string) {
|
||||||
return this.findOne({ uuid });
|
return this.get(uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
public findByUUIDAndSubscribeUrl(uuid: string, suburl: string) {
|
public findByUUIDAndSubscribeUrl(uuid: string, suburl: string) {
|
||||||
return this.findOne({ subscribeUrl: suburl, uuid });
|
return this.findOne((key, value) => {
|
||||||
|
return value.uuid === uuid && value.subscribeUrl === suburl;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public findByOriginAndSubscribeUrl(origin: string, suburl: string) {
|
public findByOriginAndSubscribeUrl(origin: string, suburl: string) {
|
||||||
return this.findOne({ subscribeUrl: suburl, origin });
|
return this.findOne((key, value) => {
|
||||||
|
return value.origin === origin && value.subscribeUrl === suburl;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { DAO, db } from "./dao";
|
import { Repo } from "./repo";
|
||||||
|
|
||||||
export type Metadata = { [key: string]: string[] };
|
export type Metadata = { [key: string]: string[] };
|
||||||
|
|
||||||
@ -25,15 +25,12 @@ export interface Subscribe {
|
|||||||
checktime: number;
|
checktime: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SubscribeDAO extends DAO<Subscribe> {
|
export class SubscribeDAO extends Repo<Subscribe> {
|
||||||
public tableName = "subscribe";
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super("subscribe");
|
||||||
this.table = db.table(this.tableName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public findByUrl(url: string) {
|
public findByUrl(url: string) {
|
||||||
return this.findOne({ url });
|
return this.findOne((key, value) => value.url === url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
17
src/app/service/service_worker/client.ts
Normal file
17
src/app/service/service_worker/client.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { Script } from "@App/app/repo/scripts";
|
||||||
|
import { Client } from "@Packages/message/client";
|
||||||
|
|
||||||
|
export class ScriptClient extends Client {
|
||||||
|
constructor() {
|
||||||
|
super("serviceWorker/script");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取安装信息
|
||||||
|
getInstallInfo(uuid: string) {
|
||||||
|
return this.do("getInstallInfo", uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
installScript(script: Script) {
|
||||||
|
return this.do("installScript", script);
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,6 @@
|
|||||||
import { fetchScriptInfo } from "@App/pkg/utils/script";
|
|
||||||
import { v4 as uuidv4 } from "uuid";
|
|
||||||
import Cache from "@App/app/cache";
|
|
||||||
import CacheKey from "@App/app/cache_key";
|
|
||||||
import { openInCurrentTab } from "@App/pkg/utils/utils";
|
|
||||||
import LoggerCore from "@App/app/logger/core";
|
|
||||||
import { Server } from "@Packages/message/server";
|
import { Server } from "@Packages/message/server";
|
||||||
import { MessageQueue } from "@Packages/message/message_queue";
|
import { MessageQueue } from "@Packages/message/message_queue";
|
||||||
|
import { ScriptService } from "./script";
|
||||||
|
|
||||||
export type InstallSource = "user" | "system" | "sync" | "subscribe" | "vscode";
|
export type InstallSource = "user" | "system" | "sync" | "subscribe" | "vscode";
|
||||||
|
|
||||||
@ -13,128 +8,13 @@ export type InstallSource = "user" | "system" | "sync" | "subscribe" | "vscode";
|
|||||||
export default class ServiceWorkerManager {
|
export default class ServiceWorkerManager {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
listenerScriptInstall() {
|
|
||||||
// 初始化脚本安装监听
|
|
||||||
chrome.webRequest.onCompleted.addListener(
|
|
||||||
(req: chrome.webRequest.WebResponseCacheDetails) => {
|
|
||||||
// 处理url, 实现安装脚本
|
|
||||||
if (req.method !== "GET") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const url = new URL(req.url);
|
|
||||||
// 判断是否有hash
|
|
||||||
if (!url.hash) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 判断是否有url参数
|
|
||||||
if (!url.hash.includes("url=")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 获取url参数
|
|
||||||
const targetUrl = url.hash.split("url=")[1];
|
|
||||||
// 读取脚本url内容, 进行安装
|
|
||||||
LoggerCore.getInstance().logger().debug("install script", { url: targetUrl });
|
|
||||||
this.openInstallPageByUrl(targetUrl).catch(() => {
|
|
||||||
// 如果打开失败, 则重定向到安装页
|
|
||||||
chrome.scripting.executeScript({
|
|
||||||
target: { tabId: req.tabId },
|
|
||||||
func: function () {
|
|
||||||
history.back();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
// 并不再重定向当前url
|
|
||||||
chrome.declarativeNetRequest.updateDynamicRules(
|
|
||||||
{
|
|
||||||
removeRuleIds: [2],
|
|
||||||
addRules: [
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
priority: 1,
|
|
||||||
action: {
|
|
||||||
type: chrome.declarativeNetRequest.RuleActionType.ALLOW,
|
|
||||||
},
|
|
||||||
condition: {
|
|
||||||
regexFilter: targetUrl,
|
|
||||||
resourceTypes: [chrome.declarativeNetRequest.ResourceType.MAIN_FRAME],
|
|
||||||
requestMethods: [chrome.declarativeNetRequest.RequestMethod.GET],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
if (chrome.runtime.lastError) {
|
|
||||||
console.error(chrome.runtime.lastError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{
|
|
||||||
urls: [
|
|
||||||
"https://docs.scriptcat.org/docs/script_installation",
|
|
||||||
"https://www.tampermonkey.net/script_installation.php",
|
|
||||||
],
|
|
||||||
types: ["main_frame"],
|
|
||||||
}
|
|
||||||
);
|
|
||||||
// 重定向到脚本安装页
|
|
||||||
chrome.declarativeNetRequest.updateDynamicRules(
|
|
||||||
{
|
|
||||||
removeRuleIds: [1],
|
|
||||||
addRules: [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
priority: 1,
|
|
||||||
action: {
|
|
||||||
type: chrome.declarativeNetRequest.RuleActionType.REDIRECT,
|
|
||||||
redirect: {
|
|
||||||
regexSubstitution: "https://docs.scriptcat.org/docs/script_installation#url=\\0",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
condition: {
|
|
||||||
regexFilter: "^([^#]+?)\\.user(\\.bg|\\.sub)?\\.js((\\?).*|$)",
|
|
||||||
resourceTypes: [chrome.declarativeNetRequest.ResourceType.MAIN_FRAME],
|
|
||||||
requestMethods: [chrome.declarativeNetRequest.RequestMethod.GET],
|
|
||||||
// 排除常见的复合上述条件的域名
|
|
||||||
excludedRequestDomains: ["github.com"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
if (chrome.runtime.lastError) {
|
|
||||||
console.error(chrome.runtime.lastError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public openInstallPageByUrl(url: string) {
|
|
||||||
return fetchScriptInfo(url, "user", false, uuidv4()).then((info) => {
|
|
||||||
Cache.getInstance().set(CacheKey.scriptInfo(info.uuid), info);
|
|
||||||
setTimeout(() => {
|
|
||||||
// 清理缓存
|
|
||||||
Cache.getInstance().del(CacheKey.scriptInfo(info.uuid));
|
|
||||||
}, 60 * 1000);
|
|
||||||
openInCurrentTab(`/src/install.html?uuid=${info.uuid}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private api: Server = new Server("service_worker");
|
private api: Server = new Server("service_worker");
|
||||||
|
|
||||||
private mq: MessageQueue = new MessageQueue();
|
private mq: MessageQueue = new MessageQueue(this.api);
|
||||||
|
|
||||||
// 获取安装信息
|
|
||||||
getInstallInfo(params: { uuid: string }) {
|
|
||||||
const info = Cache.getInstance().get(CacheKey.scriptInfo(params.uuid));
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
|
|
||||||
initManager() {
|
initManager() {
|
||||||
// 监听消息
|
const group = this.api.group("serviceWorker");
|
||||||
this.api.on("getInstallInfo", this.getInstallInfo);
|
const script = new ScriptService(group.group("script"), this.mq);
|
||||||
this.api.on("messageQueue", this.mq.handler());
|
script.init();
|
||||||
|
|
||||||
this.listenerScriptInstall();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
157
src/app/service/service_worker/script.ts
Normal file
157
src/app/service/service_worker/script.ts
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
import { fetchScriptInfo } from "@App/pkg/utils/script";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import { Group } from "@Packages/message/server";
|
||||||
|
import Logger from "@App/app/logger/logger";
|
||||||
|
import LoggerCore from "@App/app/logger/core";
|
||||||
|
import Cache from "@App/app/cache";
|
||||||
|
import CacheKey from "@App/app/cache_key";
|
||||||
|
import { openInCurrentTab } from "@App/pkg/utils/utils";
|
||||||
|
import { Script, ScriptDAO } from "@App/app/repo/scripts";
|
||||||
|
import { MessageQueue } from "@Packages/message/message_queue";
|
||||||
|
|
||||||
|
export class ScriptService {
|
||||||
|
logger: Logger;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private group: Group,
|
||||||
|
private mq: MessageQueue
|
||||||
|
) {
|
||||||
|
this.logger = LoggerCore.logger().with({ service: "script" });
|
||||||
|
}
|
||||||
|
|
||||||
|
listenerScriptInstall() {
|
||||||
|
// 初始化脚本安装监听
|
||||||
|
chrome.webRequest.onBeforeRequest.addListener(
|
||||||
|
(req: chrome.webRequest.WebRequestBodyDetails) => {
|
||||||
|
// 处理url, 实现安装脚本
|
||||||
|
if (req.method !== "GET") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = new URL(req.url);
|
||||||
|
// 判断是否有hash
|
||||||
|
if (!url.hash) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 判断是否有url参数
|
||||||
|
if (!url.hash.includes("url=")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 获取url参数
|
||||||
|
const targetUrl = url.hash.split("url=")[1];
|
||||||
|
// 读取脚本url内容, 进行安装
|
||||||
|
const logger = this.logger.with({ url: targetUrl });
|
||||||
|
logger.debug("install script");
|
||||||
|
this.openInstallPageByUrl(targetUrl).catch((e) => {
|
||||||
|
logger.error("install script error", Logger.E(e));
|
||||||
|
// 如果打开失败, 则重定向到安装页
|
||||||
|
chrome.scripting.executeScript({
|
||||||
|
target: { tabId: req.tabId },
|
||||||
|
func: function () {
|
||||||
|
history.back();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// 并不再重定向当前url
|
||||||
|
chrome.declarativeNetRequest.updateDynamicRules(
|
||||||
|
{
|
||||||
|
removeRuleIds: [2],
|
||||||
|
addRules: [
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
priority: 1,
|
||||||
|
action: {
|
||||||
|
type: chrome.declarativeNetRequest.RuleActionType.ALLOW,
|
||||||
|
},
|
||||||
|
condition: {
|
||||||
|
regexFilter: targetUrl,
|
||||||
|
resourceTypes: [chrome.declarativeNetRequest.ResourceType.MAIN_FRAME],
|
||||||
|
requestMethods: [chrome.declarativeNetRequest.RequestMethod.GET],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
console.error(chrome.runtime.lastError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
urls: [
|
||||||
|
"https://docs.scriptcat.org/docs/script_installation",
|
||||||
|
"https://www.tampermonkey.net/script_installation.php",
|
||||||
|
],
|
||||||
|
types: ["main_frame"],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// 重定向到脚本安装页
|
||||||
|
chrome.declarativeNetRequest.updateDynamicRules(
|
||||||
|
{
|
||||||
|
removeRuleIds: [1, 2],
|
||||||
|
addRules: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
priority: 1,
|
||||||
|
action: {
|
||||||
|
type: chrome.declarativeNetRequest.RuleActionType.REDIRECT,
|
||||||
|
redirect: {
|
||||||
|
regexSubstitution: "https://docs.scriptcat.org/docs/script_installation#url=\\0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
condition: {
|
||||||
|
regexFilter: "^([^#]+?)\\.user(\\.bg|\\.sub)?\\.js((\\?).*|$)",
|
||||||
|
resourceTypes: [chrome.declarativeNetRequest.ResourceType.MAIN_FRAME],
|
||||||
|
requestMethods: [chrome.declarativeNetRequest.RequestMethod.GET],
|
||||||
|
// 排除常见的符合上述条件的域名
|
||||||
|
excludedRequestDomains: ["github.com"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
console.error(chrome.runtime.lastError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public openInstallPageByUrl(url: string) {
|
||||||
|
const uuid = uuidv4();
|
||||||
|
return fetchScriptInfo(url, "user", false, uuidv4()).then((info) => {
|
||||||
|
Cache.getInstance().set(CacheKey.scriptInstallInfo(uuid), info);
|
||||||
|
setTimeout(() => {
|
||||||
|
// 清理缓存
|
||||||
|
Cache.getInstance().del(CacheKey.scriptInstallInfo(uuid));
|
||||||
|
}, 60 * 1000);
|
||||||
|
openInCurrentTab(`/src/install.html?uuid=${uuid}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取安装信息
|
||||||
|
getInstallInfo(uuid: string) {
|
||||||
|
return Cache.getInstance().get(CacheKey.scriptInstallInfo(uuid));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 安装脚本
|
||||||
|
async installScript(script: Script) {
|
||||||
|
const dao = new ScriptDAO();
|
||||||
|
// 判断是否已经安装
|
||||||
|
const oldScript = await dao.findByUUID(script.uuid);
|
||||||
|
if (!oldScript) {
|
||||||
|
// 执行安装逻辑
|
||||||
|
} else {
|
||||||
|
// 执行更新逻辑
|
||||||
|
}
|
||||||
|
// 广播一下
|
||||||
|
this.mq.publish("installScript", script);
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.listenerScriptInstall();
|
||||||
|
|
||||||
|
this.group.on("getInstallInfo", this.getInstallInfo);
|
||||||
|
this.group.on("installScript", this.installScript.bind(this));
|
||||||
|
}
|
||||||
|
}
|
@ -18,6 +18,7 @@
|
|||||||
},
|
},
|
||||||
"default_locale": "zh_CN",
|
"default_locale": "zh_CN",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
|
"storage",
|
||||||
"offscreen",
|
"offscreen",
|
||||||
"scripting",
|
"scripting",
|
||||||
"activeTab",
|
"activeTab",
|
||||||
|
@ -5,7 +5,9 @@ import { Metadata, Script, SCRIPT_STATUS_DISABLE, SCRIPT_STATUS_ENABLE } from "@
|
|||||||
import { Subscribe } from "@App/app/repo/subscribe";
|
import { Subscribe } from "@App/app/repo/subscribe";
|
||||||
import { i18nDescription, i18nName } from "@App/locales/locales";
|
import { i18nDescription, i18nName } from "@App/locales/locales";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { ScriptInfo } from "@App/pkg/utils/script";
|
import { prepareScriptByCode, prepareSubscribeByCode, ScriptInfo } from "@App/pkg/utils/script";
|
||||||
|
import { isDebug, nextTime } from "@App/pkg/utils/utils";
|
||||||
|
import { ScriptClient } from "@App/app/service/service_worker/client";
|
||||||
|
|
||||||
type Permission = { label: string; color?: string; value: string[] }[];
|
type Permission = { label: string; color?: string; value: string[] }[];
|
||||||
|
|
||||||
@ -14,28 +16,74 @@ const closeWindow = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [permission, setPermission] = useState<Permission>([]);
|
// 脚本信息包括脚本代码、下载url、metadata等信息,通过service_worker的缓存获取
|
||||||
const [metadata, setMetadata] = useState<Metadata>({});
|
const [scriptInfo, setScriptInfo] = useState<ScriptInfo>();
|
||||||
// 脚本信息包括脚本代码、下载url,但是不包括解析代码后得到的metadata,通过background的缓存获取
|
|
||||||
const [info, setInfo] = useState<ScriptInfo>();
|
|
||||||
// 对脚本详细的描述
|
|
||||||
const [description, setDescription] = useState<any>();
|
|
||||||
// 是系统检测到脚本更新时打开的窗口会有一个倒计时
|
// 是系统检测到脚本更新时打开的窗口会有一个倒计时
|
||||||
const [countdown, setCountdown] = useState<number>(-1);
|
const [countdown, setCountdown] = useState<number>(-1);
|
||||||
// 是否为更新
|
|
||||||
const [isUpdate, setIsUpdate] = useState<boolean>(false);
|
|
||||||
// 脚本信息
|
// 脚本信息
|
||||||
const [upsertScript, setUpsertScript] = useState<Script | Subscribe>();
|
const [upsertScript, setUpsertScript] = useState<Script | Subscribe>();
|
||||||
// 更新的情况下会有老版本的脚本信息
|
// 更新的情况下会有老版本的脚本信息
|
||||||
const [oldScript, setOldScript] = useState<Script | Subscribe>();
|
const [oldScript, setOldScript] = useState<Script | Subscribe>();
|
||||||
// 脚本开启状态
|
// 脚本开启状态
|
||||||
const [enable, setEnable] = useState<boolean>(false);
|
const [enable, setEnable] = useState<boolean>(false);
|
||||||
// 是否是订阅脚本
|
|
||||||
const [isSub, setIsSub] = useState<boolean>(false);
|
|
||||||
// 按钮文案
|
// 按钮文案
|
||||||
const [btnText, setBtnText] = useState<string>();
|
const [btnText, setBtnText] = useState<string>("");
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const metadata: Metadata = scriptInfo?.metadata || {};
|
||||||
|
const permission: Permission = [];
|
||||||
|
const isUpdate = scriptInfo?.update;
|
||||||
|
const description = [];
|
||||||
|
if (scriptInfo) {
|
||||||
|
if (scriptInfo.userSubscribe) {
|
||||||
|
permission.push({
|
||||||
|
label: t("subscribe_install_label"),
|
||||||
|
color: "#ff0000",
|
||||||
|
value: metadata.scripturl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (metadata.match) {
|
||||||
|
permission.push({ label: t("script_runs_in"), value: metadata.match });
|
||||||
|
}
|
||||||
|
if (metadata.connect) {
|
||||||
|
permission.push({
|
||||||
|
label: t("script_has_full_access_to"),
|
||||||
|
color: "#F9925A",
|
||||||
|
value: metadata.connect,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (metadata.require) {
|
||||||
|
permission.push({ label: t("script_requires"), value: metadata.require });
|
||||||
|
}
|
||||||
|
|
||||||
|
let isCookie = false;
|
||||||
|
metadata.grant?.forEach((val) => {
|
||||||
|
if (val === "GM_cookie") {
|
||||||
|
isCookie = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (isCookie) {
|
||||||
|
description.push(
|
||||||
|
<Typography.Text type="error" key="cookie">
|
||||||
|
{t("cookie_warning")}
|
||||||
|
</Typography.Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (metadata.crontab) {
|
||||||
|
description.push(<Typography.Text key="crontab">{t("scheduled_script_description_1")}</Typography.Text>);
|
||||||
|
description.push(
|
||||||
|
<Typography.Text key="cronta-nexttime">
|
||||||
|
{t("scheduled_script_description_2", {
|
||||||
|
expression: metadata.crontab[0],
|
||||||
|
time: nextTime(metadata.crontab[0]),
|
||||||
|
})}
|
||||||
|
</Typography.Text>
|
||||||
|
);
|
||||||
|
} else if (metadata.background) {
|
||||||
|
description.push(<Typography.Text key="background">{t("background_script_description")}</Typography.Text>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 不推荐的内容标签与描述
|
// 不推荐的内容标签与描述
|
||||||
const antifeatures: {
|
const antifeatures: {
|
||||||
[key: string]: { color: string; title: string; description: string };
|
[key: string]: { color: string; title: string; description: string };
|
||||||
@ -78,12 +126,46 @@ function App() {
|
|||||||
if (!uuid) {
|
if (!uuid) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
});
|
new ScriptClient()
|
||||||
|
.getInstallInfo(uuid)
|
||||||
|
.then(async (info: ScriptInfo) => {
|
||||||
|
if (!info) {
|
||||||
|
throw new Error("fetch script info failed");
|
||||||
|
}
|
||||||
|
// 如果是更新的情况下, 获取老版本的脚本信息
|
||||||
|
let prepare: { script: Script; oldScript?: Script } | { subscribe: Subscribe; oldSubscribe?: Subscribe };
|
||||||
|
let action: Script | Subscribe;
|
||||||
|
if (info.userSubscribe) {
|
||||||
|
prepare = await prepareSubscribeByCode(info.code, info.url);
|
||||||
|
action = prepare.subscribe;
|
||||||
|
setOldScript(prepare.oldSubscribe);
|
||||||
|
} else {
|
||||||
|
if (info.update) {
|
||||||
|
prepare = await prepareScriptByCode(info.code, info.url, info.uuid);
|
||||||
|
} else {
|
||||||
|
prepare = await prepareScriptByCode(info.code, info.url);
|
||||||
|
}
|
||||||
|
action = prepare.script;
|
||||||
|
setOldScript(prepare.oldScript);
|
||||||
|
}
|
||||||
|
if (info.userSubscribe) {
|
||||||
|
setBtnText(isUpdate ? t("update_subscribe")! : t("install_subscribe"));
|
||||||
|
} else {
|
||||||
|
setBtnText(isUpdate ? t("update_script")! : t("install_script"));
|
||||||
|
}
|
||||||
|
setScriptInfo(info);
|
||||||
|
setEnable(action.status === SCRIPT_STATUS_ENABLE);
|
||||||
|
setUpsertScript(action);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
Message.error(t("script_info_load_failed"));
|
||||||
|
});
|
||||||
|
}, [t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full">
|
<div className="h-full">
|
||||||
<div className="h-full">
|
<div className="h-full">
|
||||||
<Grid.Row gutter={8}>
|
<Grid.Row className="mb-2" gutter={8}>
|
||||||
<Grid.Col flex={1} className="flex-col p-8px">
|
<Grid.Col flex={1} className="flex-col p-8px">
|
||||||
<Space direction="vertical">
|
<Space direction="vertical">
|
||||||
<div>
|
<div>
|
||||||
@ -94,7 +176,9 @@ function App() {
|
|||||||
)}
|
)}
|
||||||
<Typography.Text bold className="text-size-lg">
|
<Typography.Text bold className="text-size-lg">
|
||||||
{upsertScript && i18nName(upsertScript)}
|
{upsertScript && i18nName(upsertScript)}
|
||||||
<Tooltip content={isSub ? t("subscribe_source_tooltip") : t("script_status_tooltip")}>
|
<Tooltip
|
||||||
|
content={scriptInfo?.userSubscribe ? t("subscribe_source_tooltip") : t("script_status_tooltip")}
|
||||||
|
>
|
||||||
<Switch
|
<Switch
|
||||||
style={{ marginLeft: "8px" }}
|
style={{ marginLeft: "8px" }}
|
||||||
checked={enable}
|
checked={enable}
|
||||||
@ -131,7 +215,7 @@ function App() {
|
|||||||
overflowY: "auto",
|
overflowY: "auto",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("source")}: {info?.url}
|
{t("source")}: {scriptInfo?.url}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-end">
|
<div className="text-end">
|
||||||
@ -144,7 +228,7 @@ function App() {
|
|||||||
Message.error(t("script_info_load_failed")!);
|
Message.error(t("script_info_load_failed")!);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isSub) {
|
if (scriptInfo?.userSubscribe) {
|
||||||
// subscribeCtrl
|
// subscribeCtrl
|
||||||
// .upsert(upsertScript as Subscribe)
|
// .upsert(upsertScript as Subscribe)
|
||||||
// .then(() => {
|
// .then(() => {
|
||||||
@ -159,23 +243,25 @@ function App() {
|
|||||||
// });
|
// });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// scriptCtrl
|
new ScriptClient()
|
||||||
// .upsert(upsertScript as Script)
|
.installScript(upsertScript as Script)
|
||||||
// .then(() => {
|
.then(() => {
|
||||||
// if (isUpdate) {
|
if (isUpdate) {
|
||||||
// Message.success(t("install.update_success")!);
|
Message.success(t("install.update_success")!);
|
||||||
// setBtnText(t("install.update_success")!);
|
setBtnText(t("install.update_success")!);
|
||||||
// } else {
|
} else {
|
||||||
// Message.success(t("install_success")!);
|
Message.success(t("install_success")!);
|
||||||
// setBtnText(t("install_success")!);
|
setBtnText(t("install_success")!);
|
||||||
// }
|
}
|
||||||
// setTimeout(() => {
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||||
// closeWindow();
|
!isDebug() &&
|
||||||
// }, 200);
|
setTimeout(() => {
|
||||||
// })
|
closeWindow();
|
||||||
// .catch((e) => {
|
}, 200);
|
||||||
// Message.error(`${t("install_failed")}: ${e}`);
|
})
|
||||||
// });
|
.catch((e) => {
|
||||||
|
Message.error(`${t("install_failed")}: ${e}`);
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{btnText}
|
{btnText}
|
||||||
|
41
src/pages/options/index.css
Normal file
41
src/pages/options/index.css
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
.show-log-card .arco-list-item {
|
||||||
|
border-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1.arco-typography,
|
||||||
|
h2.arco-typography,
|
||||||
|
h3.arco-typography,
|
||||||
|
h4.arco-typography,
|
||||||
|
h5.arco-typography,
|
||||||
|
h6.arco-typography {
|
||||||
|
margin-top: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.script-list .arco-card-body {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.max-table-cell .arco-table-cell {
|
||||||
|
display: block;
|
||||||
|
max-height: 100px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* error、wran图标直接用的油猴CodeMirror编辑器图标 待优化*/
|
||||||
|
.icon-error{
|
||||||
|
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII=);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
left: 10px!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-warn{
|
||||||
|
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAANlBMVEX/uwDvrwD/uwD/uwD/uwD/uwD/uwD/uwD/uwD6twD/uwAAAADurwD2tQD7uAD+ugAAAAD/uwDhmeTRAAAADHRSTlMJ8mN1EYcbmiixgACm7WbuAAAAVklEQVR42n3PUQqAIBBFUU1LLc3u/jdbOJoW1P08DA9Gba8+YWJ6gNJoNYIBzAA2chBth5kLmG9YUoG0NHAUwFXwO9LuBQL1giCQb8gC9Oro2vp5rncCIY8L8uEx5ZkAAAAASUVORK5CYII=);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
left: 10px!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionList{
|
||||||
|
height: auto !important;
|
||||||
|
}
|
25
src/pages/options/main.tsx
Normal file
25
src/pages/options/main.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import MainLayout from "../components/layout/MainLayout.tsx";
|
||||||
|
import "@arco-design/web-react/dist/css/arco.css";
|
||||||
|
import "@App/locales/locales";
|
||||||
|
import "@App/index.css";
|
||||||
|
import { Provider } from "react-redux";
|
||||||
|
import { store } from "@App/store/store.ts";
|
||||||
|
import { Broker } from "@Packages/message/message_queue.ts";
|
||||||
|
|
||||||
|
// 测试监听广播
|
||||||
|
|
||||||
|
const border = new Broker();
|
||||||
|
|
||||||
|
border.subscribe("installScript", (message) => {
|
||||||
|
console.log(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<Provider store={store}>
|
||||||
|
<MainLayout className="!flex-col !px-4 box-border">p</MainLayout>
|
||||||
|
</Provider>
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
362
src/pages/options/routes/Logger.tsx
Normal file
362
src/pages/options/routes/Logger.tsx
Normal file
@ -0,0 +1,362 @@
|
|||||||
|
import React, { useEffect } from "react";
|
||||||
|
import {
|
||||||
|
BackTop,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
DatePicker,
|
||||||
|
Input,
|
||||||
|
List,
|
||||||
|
Message,
|
||||||
|
Space,
|
||||||
|
} from "@arco-design/web-react";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import Text from "@arco-design/web-react/es/Typography/text";
|
||||||
|
import { Logger, LoggerDAO } from "@App/app/repo/logger";
|
||||||
|
import LogLabel, { Labels, Query } from "@App/pages/components/LogLabel";
|
||||||
|
import { IconPlus } from "@arco-design/web-react/icon";
|
||||||
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
import { formatUnixTime } from "@App/pkg/utils/utils";
|
||||||
|
import { SystemConfig } from "@App/pkg/config/config";
|
||||||
|
import IoC from "@App/app/ioc";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
function LoggerPage() {
|
||||||
|
const [labels, setLabels] = React.useState<Labels>({});
|
||||||
|
const defaultQuery = JSON.parse(useSearchParams()[0].get("query") || "[{}]");
|
||||||
|
const [init, setInit] = React.useState(0);
|
||||||
|
const [querys, setQuerys] = React.useState<Query[]>(defaultQuery);
|
||||||
|
const [logs, setLogs] = React.useState<Logger[]>([]);
|
||||||
|
const [queryLogs, setQueryLogs] = React.useState<Logger[]>([]);
|
||||||
|
const [search, setSearch] = React.useState<string>("");
|
||||||
|
const [startTime, setStartTime] = React.useState(
|
||||||
|
dayjs().subtract(24, "hour").unix()
|
||||||
|
);
|
||||||
|
const [endTime, setEndTime] = React.useState(dayjs().unix());
|
||||||
|
const loggerDAO = new LoggerDAO();
|
||||||
|
const systemConfig = IoC.instance(SystemConfig) as SystemConfig;
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const onQueryLog = () => {
|
||||||
|
const newQueryLogs: Logger[] = [];
|
||||||
|
const regex = search && new RegExp(search);
|
||||||
|
logs.forEach((log) => {
|
||||||
|
for (let i = 0; i < querys.length; i += 1) {
|
||||||
|
const query = querys[i];
|
||||||
|
if (query.key) {
|
||||||
|
const value = log.label[query.key];
|
||||||
|
switch (query.condition) {
|
||||||
|
case "=":
|
||||||
|
// eslint-disable-next-line eqeqeq
|
||||||
|
if (value != query.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "=~":
|
||||||
|
if (
|
||||||
|
typeof value === "string" &&
|
||||||
|
value.indexOf(query.value) === -1
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "!=":
|
||||||
|
// eslint-disable-next-line eqeqeq
|
||||||
|
if (value == query.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "!~":
|
||||||
|
if (
|
||||||
|
typeof value === "string" &&
|
||||||
|
value.indexOf(query.value) === -1
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// eslint-disable-next-line eqeqeq
|
||||||
|
if (value != query.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (regex && !regex.test(log.message)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
newQueryLogs.push(log);
|
||||||
|
});
|
||||||
|
setInit(4);
|
||||||
|
setQueryLogs(newQueryLogs);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (init === 1 && defaultQuery.length && defaultQuery[0].key) {
|
||||||
|
onQueryLog();
|
||||||
|
setInit(2);
|
||||||
|
}
|
||||||
|
}, [init]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loggerDAO.queryLogs(startTime * 1000, endTime * 1000).then((l) => {
|
||||||
|
setLogs(l);
|
||||||
|
// 计算标签
|
||||||
|
const newLabels = labels;
|
||||||
|
l.forEach((log) => {
|
||||||
|
Object.keys(log.label).forEach((key) => {
|
||||||
|
if (!newLabels[key]) {
|
||||||
|
newLabels[key] = {};
|
||||||
|
}
|
||||||
|
const value = log.label[key];
|
||||||
|
switch (typeof value) {
|
||||||
|
case "string":
|
||||||
|
case "number":
|
||||||
|
newLabels[key][value] = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
setLabels(newLabels);
|
||||||
|
setQueryLogs([]);
|
||||||
|
if (init === 0) {
|
||||||
|
setInit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [startTime, endTime]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<BackTop
|
||||||
|
visibleHeight={30}
|
||||||
|
style={{ position: "absolute" }}
|
||||||
|
target={() => document.getElementById("backtop")!}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
id="backtop"
|
||||||
|
style={{
|
||||||
|
height: "100%",
|
||||||
|
overflow: "auto",
|
||||||
|
position: "relative",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Space
|
||||||
|
direction="vertical"
|
||||||
|
className="log-space"
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
bordered={false}
|
||||||
|
title={t("log_title")}
|
||||||
|
extra={
|
||||||
|
<Space size="large">
|
||||||
|
<DatePicker.RangePicker
|
||||||
|
style={{ width: 400 }}
|
||||||
|
showTime
|
||||||
|
shortcutsPlacementLeft
|
||||||
|
value={[startTime * 1000, endTime * 1000]}
|
||||||
|
onChange={(_, time) => {
|
||||||
|
setStartTime(time[0].unix());
|
||||||
|
setEndTime(time[1].unix());
|
||||||
|
}}
|
||||||
|
shortcuts={[
|
||||||
|
{
|
||||||
|
text: t("last_5_minutes"),
|
||||||
|
value: () => [dayjs(), dayjs().add(-5, "minute")],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t("last_15_minutes"),
|
||||||
|
value: () => [dayjs(), dayjs().add(-15, "minute")],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t("last_30_minutes"),
|
||||||
|
value: () => [dayjs(), dayjs().add(-30, "minute")],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t("last_1_hour"),
|
||||||
|
value: () => [dayjs(), dayjs().add(-1, "hour")],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t("last_3_hours"),
|
||||||
|
value: () => [dayjs(), dayjs().add(-3, "hour")],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t("last_6_hours"),
|
||||||
|
value: () => [dayjs(), dayjs().add(-6, "hour")],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t("last_12_hours"),
|
||||||
|
value: () => [dayjs(), dayjs().add(-12, "hour")],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t("last_24_hours"),
|
||||||
|
value: () => [dayjs(), dayjs().add(-24, "hour")],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t("last_7_days"),
|
||||||
|
value: () => [dayjs(), dayjs().add(-7, "day")],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Button type="primary" onClick={onQueryLog}>
|
||||||
|
{t("query")}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Space direction="vertical">
|
||||||
|
<Space
|
||||||
|
style={{
|
||||||
|
background: "var(--color-neutral-1)",
|
||||||
|
padding: 8,
|
||||||
|
}}
|
||||||
|
direction="vertical"
|
||||||
|
>
|
||||||
|
<div className="text-sm font-medium">{t("labels")}</div>
|
||||||
|
<Space>
|
||||||
|
{querys.map((query, index) => (
|
||||||
|
<LogLabel
|
||||||
|
key={query.key + query.value}
|
||||||
|
value={query}
|
||||||
|
labels={labels}
|
||||||
|
onChange={(v) => {
|
||||||
|
querys[index] = v;
|
||||||
|
setQuerys([...querys]);
|
||||||
|
}}
|
||||||
|
onClose={() => {
|
||||||
|
querys.splice(index, 1);
|
||||||
|
setQuerys([...querys]);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
iconOnly
|
||||||
|
onClick={() => {
|
||||||
|
setQuerys([
|
||||||
|
...querys,
|
||||||
|
{
|
||||||
|
key: "",
|
||||||
|
condition: "=",
|
||||||
|
value: "",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}}
|
||||||
|
icon={<IconPlus />}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</Space>
|
||||||
|
<Space
|
||||||
|
style={{
|
||||||
|
background: "var(--color-neutral-1)",
|
||||||
|
padding: 8,
|
||||||
|
}}
|
||||||
|
direction="vertical"
|
||||||
|
>
|
||||||
|
<div className="text-sm font-medium">{t("search_regex")}</div>
|
||||||
|
<Input value={search} onChange={(e) => setSearch(e)} />
|
||||||
|
</Space>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
<Card
|
||||||
|
className="show-log-card"
|
||||||
|
bordered={false}
|
||||||
|
title={t("logs")}
|
||||||
|
extra={
|
||||||
|
<Space>
|
||||||
|
<Space>
|
||||||
|
<span>{t("clean_schedule")}</span>
|
||||||
|
<Input
|
||||||
|
defaultValue={systemConfig.logCleanCycle.toString()}
|
||||||
|
style={{
|
||||||
|
width: "60px",
|
||||||
|
}}
|
||||||
|
type="number"
|
||||||
|
onChange={(e) => {
|
||||||
|
systemConfig.logCleanCycle = parseInt(e, 10);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span>{t("days_ago_logs")}</span>
|
||||||
|
</Space>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
status="warning"
|
||||||
|
onClick={() => {
|
||||||
|
queryLogs.forEach((log) => {
|
||||||
|
loggerDAO.delete(log.id);
|
||||||
|
});
|
||||||
|
setQueryLogs([]);
|
||||||
|
setLogs([]);
|
||||||
|
Message.info(t("delete_completed")!);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("delete_current_logs")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
status="danger"
|
||||||
|
onClick={() => {
|
||||||
|
loggerDAO.clear();
|
||||||
|
setQueryLogs([]);
|
||||||
|
setLogs([]);
|
||||||
|
Message.info(t("clear_completed")!);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("clear_logs")}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
padding: 8,
|
||||||
|
height: "100%",
|
||||||
|
boxSizing: "border-box",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text>
|
||||||
|
{formatUnixTime(startTime)} {t("to")} {formatUnixTime(endTime)}{" "}
|
||||||
|
{t("total_logs", { length: logs.length })}
|
||||||
|
{init === 4
|
||||||
|
? `, ${t("filtered_logs", { length: queryLogs.length })}`
|
||||||
|
: `, ${t("enter_filter_conditions")}`}
|
||||||
|
</Text>
|
||||||
|
<List
|
||||||
|
style={{
|
||||||
|
height: "100%",
|
||||||
|
overflow: "auto",
|
||||||
|
}}
|
||||||
|
size="small"
|
||||||
|
dataSource={queryLogs}
|
||||||
|
render={(item: Logger, index) => (
|
||||||
|
<List.Item
|
||||||
|
key={index}
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
// eslint-disable-next-line no-nested-ternary
|
||||||
|
item.level === "error"
|
||||||
|
? "var(--color-danger-light-2)" // eslint-disable-next-line no-nested-ternary
|
||||||
|
: item.level === "warn"
|
||||||
|
? "var(--color-warning-light-2)"
|
||||||
|
: item.level === "info"
|
||||||
|
? "var(--color-success-light-2)"
|
||||||
|
: "var(--color-primary-light-1)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{formatUnixTime(item.createtime / 1000)}{" "}
|
||||||
|
{typeof item.message === "object"
|
||||||
|
? JSON.stringify(item.message)
|
||||||
|
: item.message}{" "}
|
||||||
|
{JSON.stringify(item.label)}
|
||||||
|
</List.Item>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoggerPage;
|
1051
src/pages/options/routes/ScriptList.tsx
Normal file
1051
src/pages/options/routes/ScriptList.tsx
Normal file
File diff suppressed because it is too large
Load Diff
269
src/pages/options/routes/Setting.tsx
Normal file
269
src/pages/options/routes/Setting.tsx
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Checkbox,
|
||||||
|
Input,
|
||||||
|
Message,
|
||||||
|
Select,
|
||||||
|
Space,
|
||||||
|
} from "@arco-design/web-react";
|
||||||
|
import FileSystemParams from "@App/pages/components/FileSystemParams";
|
||||||
|
import { SystemConfig } from "@App/pkg/config/config";
|
||||||
|
import IoC from "@App/app/ioc";
|
||||||
|
import FileSystemFactory, { FileSystemType } from "@Pkg/filesystem/factory";
|
||||||
|
import Title from "@arco-design/web-react/es/Typography/title";
|
||||||
|
import { IconQuestionCircleFill } from "@arco-design/web-react/icon";
|
||||||
|
// eslint-disable-next-line import/no-extraneous-dependencies, import/no-import-module-exports
|
||||||
|
import { format } from "prettier";
|
||||||
|
// eslint-disable-next-line import/no-extraneous-dependencies, import/no-import-module-exports
|
||||||
|
import babel from "prettier/parser-babel";
|
||||||
|
import GMApiSetting from "@App/pages/components/GMApiSetting";
|
||||||
|
import i18n from "@App/locales/locales";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import Logger from "@App/app/logger/logger";
|
||||||
|
|
||||||
|
function Setting() {
|
||||||
|
const systemConfig = IoC.instance(SystemConfig) as SystemConfig;
|
||||||
|
const [syncDelete, setSyncDelete] = useState<boolean>(
|
||||||
|
systemConfig.cloudSync.syncDelete
|
||||||
|
);
|
||||||
|
const [enableCloudSync, setEnableCloudSync] = useState(
|
||||||
|
systemConfig.cloudSync.enable
|
||||||
|
);
|
||||||
|
const [fileSystemType, setFilesystemType] = useState<FileSystemType>(
|
||||||
|
systemConfig.cloudSync.filesystem
|
||||||
|
);
|
||||||
|
const [fileSystemParams, setFilesystemParam] = useState<{
|
||||||
|
[key: string]: any;
|
||||||
|
}>(systemConfig.cloudSync.params[fileSystemType] || {});
|
||||||
|
const [language, setLanguage] = useState(i18n.language);
|
||||||
|
const languageList: { key: string; title: string }[] = [];
|
||||||
|
const { t } = useTranslation();
|
||||||
|
Object.keys(i18n.store.data).forEach((key) => {
|
||||||
|
if (key === "ach-UG") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
languageList.push({
|
||||||
|
key,
|
||||||
|
title: i18n.store.data[key].title as string,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
languageList.push({
|
||||||
|
key: "help",
|
||||||
|
title: t("help_translate"),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Space
|
||||||
|
className="setting"
|
||||||
|
direction="vertical"
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
overflow: "auto",
|
||||||
|
position: "relative",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Card title={t("general")} bordered={false}>
|
||||||
|
<Space direction="vertical">
|
||||||
|
<Space>
|
||||||
|
<span>{t("language")}:</span>
|
||||||
|
<Select
|
||||||
|
value={language}
|
||||||
|
className="w-24"
|
||||||
|
onChange={(value) => {
|
||||||
|
if (value === "help") {
|
||||||
|
window.open(
|
||||||
|
"https://crowdin.com/project/scriptcat",
|
||||||
|
"_blank"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLanguage(value);
|
||||||
|
i18n.changeLanguage(value);
|
||||||
|
dayjs.locale(value.toLocaleLowerCase());
|
||||||
|
localStorage.language = value;
|
||||||
|
Message.success(t("language_change_tip")!);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{languageList.map((item) => (
|
||||||
|
<Select.Option key={item.key} value={item.key}>
|
||||||
|
{item.title}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Space>
|
||||||
|
<Space>
|
||||||
|
{t("menu_expand_num_before")}:
|
||||||
|
<Input
|
||||||
|
style={{ width: "64px" }}
|
||||||
|
type="number"
|
||||||
|
defaultValue={systemConfig.menuExpandNum.toString()}
|
||||||
|
onChange={(val) => {
|
||||||
|
systemConfig.menuExpandNum = parseInt(val, 10);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{t("menu_expand_num_after")}
|
||||||
|
</Space>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
<Card className="sync" title={t("script_sync")} bordered={false}>
|
||||||
|
<Space direction="vertical">
|
||||||
|
<Checkbox
|
||||||
|
checked={syncDelete}
|
||||||
|
onChange={(checked) => {
|
||||||
|
setSyncDelete(checked);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("sync_delete")}
|
||||||
|
</Checkbox>
|
||||||
|
<FileSystemParams
|
||||||
|
preNode={
|
||||||
|
<Checkbox
|
||||||
|
checked={enableCloudSync}
|
||||||
|
onChange={(checked) => {
|
||||||
|
setEnableCloudSync(checked);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("enable_script_sync_to")}
|
||||||
|
</Checkbox>
|
||||||
|
}
|
||||||
|
actionButton={[
|
||||||
|
<Button
|
||||||
|
key="save"
|
||||||
|
type="primary"
|
||||||
|
onClick={async () => {
|
||||||
|
// Save to the configuration
|
||||||
|
// Perform validation if enabled
|
||||||
|
if (enableCloudSync) {
|
||||||
|
Message.info(t("cloud_sync_account_verification")!);
|
||||||
|
try {
|
||||||
|
await FileSystemFactory.create(
|
||||||
|
fileSystemType,
|
||||||
|
fileSystemParams
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
Message.error(
|
||||||
|
`${t(
|
||||||
|
"cloud_sync_verification_failed"
|
||||||
|
)}: ${JSON.stringify(Logger.E(e))}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const params = { ...systemConfig.backup.params };
|
||||||
|
params[fileSystemType] = fileSystemParams;
|
||||||
|
systemConfig.cloudSync = {
|
||||||
|
enable: enableCloudSync,
|
||||||
|
syncDelete,
|
||||||
|
filesystem: fileSystemType,
|
||||||
|
params,
|
||||||
|
};
|
||||||
|
Message.success(t("save_success")!);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("save")}
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
fileSystemType={fileSystemType}
|
||||||
|
fileSystemParams={fileSystemParams}
|
||||||
|
onChangeFileSystemType={(type) => {
|
||||||
|
setFilesystemType(type);
|
||||||
|
}}
|
||||||
|
onChangeFileSystemParams={(params) => {
|
||||||
|
setFilesystemParam(params);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
<Card title={t("update")} bordered={false}>
|
||||||
|
<Space direction="vertical">
|
||||||
|
<Space>
|
||||||
|
<span>{t("script_subscription_check_interval")}:</span>
|
||||||
|
<Select
|
||||||
|
defaultValue={systemConfig.checkScriptUpdateCycle.toString()}
|
||||||
|
style={{
|
||||||
|
width: 120,
|
||||||
|
}}
|
||||||
|
onChange={(value) => {
|
||||||
|
systemConfig.checkScriptUpdateCycle = parseInt(value, 10);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Select.Option value="0">{t("never")}</Select.Option>
|
||||||
|
<Select.Option value="21600">{t("6_hours")}</Select.Option>
|
||||||
|
<Select.Option value="43200">{t("12_hours")}</Select.Option>
|
||||||
|
<Select.Option value="86400">{t("every_day")}</Select.Option>
|
||||||
|
<Select.Option value="604800">{t("every_week")}</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</Space>
|
||||||
|
<Checkbox
|
||||||
|
onChange={(checked) => {
|
||||||
|
systemConfig.updateDisableScript = checked;
|
||||||
|
}}
|
||||||
|
defaultChecked={systemConfig.updateDisableScript}
|
||||||
|
>
|
||||||
|
{t("update_disabled_scripts")}
|
||||||
|
</Checkbox>
|
||||||
|
<Checkbox
|
||||||
|
onChange={(checked) => {
|
||||||
|
systemConfig.silenceUpdateScript = checked;
|
||||||
|
}}
|
||||||
|
defaultChecked={systemConfig.silenceUpdateScript}
|
||||||
|
>
|
||||||
|
{t("silent_update_non_critical_changes")}
|
||||||
|
</Checkbox>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
<GMApiSetting />
|
||||||
|
<Card title="ESLint" bordered={false}>
|
||||||
|
<Space direction="vertical" className="w-full">
|
||||||
|
<Checkbox
|
||||||
|
onChange={(checked) => {
|
||||||
|
systemConfig.enableEslint = checked;
|
||||||
|
}}
|
||||||
|
defaultChecked={systemConfig.enableEslint}
|
||||||
|
>
|
||||||
|
{t("enable_eslint")}
|
||||||
|
</Checkbox>
|
||||||
|
<Title heading={5}>
|
||||||
|
{t("eslint_rules")}{" "}
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
style={{
|
||||||
|
height: 24,
|
||||||
|
}}
|
||||||
|
icon={
|
||||||
|
<IconQuestionCircleFill
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
href="https://eslint.org/play/"
|
||||||
|
target="_blank"
|
||||||
|
iconOnly
|
||||||
|
/>
|
||||||
|
</Title>
|
||||||
|
<Input.TextArea
|
||||||
|
placeholder={t("enter_eslint_rules")!}
|
||||||
|
autoSize={{
|
||||||
|
minRows: 4,
|
||||||
|
maxRows: 8,
|
||||||
|
}}
|
||||||
|
defaultValue={format(systemConfig.eslintConfig, {
|
||||||
|
parser: "json",
|
||||||
|
plugins: [babel],
|
||||||
|
})}
|
||||||
|
onBlur={(v) => {
|
||||||
|
systemConfig.eslintConfig = v.target.value;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Setting;
|
320
src/pages/options/routes/SubscribeList.tsx
Normal file
320
src/pages/options/routes/SubscribeList.tsx
Normal file
@ -0,0 +1,320 @@
|
|||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
import Text from "@arco-design/web-react/es/Typography/text";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Input,
|
||||||
|
Message,
|
||||||
|
Popconfirm,
|
||||||
|
Switch,
|
||||||
|
Table,
|
||||||
|
Tag,
|
||||||
|
Tooltip,
|
||||||
|
} from "@arco-design/web-react";
|
||||||
|
import {
|
||||||
|
Subscribe,
|
||||||
|
SUBSCRIBE_STATUS_DISABLE,
|
||||||
|
SUBSCRIBE_STATUS_ENABLE,
|
||||||
|
SubscribeDAO,
|
||||||
|
} from "@App/app/repo/subscribe";
|
||||||
|
import { ColumnProps } from "@arco-design/web-react/es/Table";
|
||||||
|
import IoC from "@App/app/ioc";
|
||||||
|
import SubscribeController from "@App/app/service/subscribe/controller";
|
||||||
|
import { IconSearch, IconUserAdd } from "@arco-design/web-react/icon";
|
||||||
|
import { RefInputType } from "@arco-design/web-react/es/Input/interface";
|
||||||
|
import { semTime } from "@App/pkg/utils/utils";
|
||||||
|
import { RiDeleteBin5Fill } from "react-icons/ri";
|
||||||
|
import { useTranslation } from "react-i18next"; // 添加了 react-i18next 的引用
|
||||||
|
|
||||||
|
type ListType = Subscribe & { loading?: boolean };
|
||||||
|
|
||||||
|
function SubscribeList() {
|
||||||
|
const dao = new SubscribeDAO();
|
||||||
|
const subscribeCtrl = IoC.instance(
|
||||||
|
SubscribeController
|
||||||
|
) as SubscribeController;
|
||||||
|
const [list, setList] = useState<ListType[]>([]);
|
||||||
|
const inputRef = useRef<RefInputType>(null);
|
||||||
|
const { t } = useTranslation(); // 使用 useTranslation hook
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dao.table
|
||||||
|
.orderBy("id")
|
||||||
|
.toArray()
|
||||||
|
.then((subscribes) => {
|
||||||
|
setList(subscribes);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const columns: ColumnProps[] = [
|
||||||
|
{
|
||||||
|
title: "#",
|
||||||
|
dataIndex: "id",
|
||||||
|
width: 70,
|
||||||
|
key: "#",
|
||||||
|
sorter: (a, b) => a.id - b.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("enable"),
|
||||||
|
width: t("subscribe_list_enable_width"),
|
||||||
|
key: "enable",
|
||||||
|
sorter(a, b) {
|
||||||
|
return a.status - b.status;
|
||||||
|
},
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
text: t("enable"),
|
||||||
|
value: SUBSCRIBE_STATUS_ENABLE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t("disable"),
|
||||||
|
value: SUBSCRIBE_STATUS_DISABLE,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onFilter: (value, row) => row.status === value,
|
||||||
|
render: (col, item: ListType, index) => {
|
||||||
|
return (
|
||||||
|
<Switch
|
||||||
|
checked={item.status === SUBSCRIBE_STATUS_ENABLE}
|
||||||
|
loading={item.loading}
|
||||||
|
disabled={item.loading}
|
||||||
|
onChange={(checked) => {
|
||||||
|
list[index].loading = true;
|
||||||
|
setList([...list]);
|
||||||
|
let p: Promise<any>;
|
||||||
|
if (checked) {
|
||||||
|
p = subscribeCtrl.enable(item.id).then(() => {
|
||||||
|
list[index].status = SUBSCRIBE_STATUS_ENABLE;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
p = subscribeCtrl.disable(item.id).then(() => {
|
||||||
|
list[index].status = SUBSCRIBE_STATUS_DISABLE;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
p.catch((err) => {
|
||||||
|
Message.error(err);
|
||||||
|
}).finally(() => {
|
||||||
|
list[index].loading = false;
|
||||||
|
setList([...list]);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("name"),
|
||||||
|
dataIndex: "name",
|
||||||
|
sorter: (a, b) => a.name.localeCompare(b.name),
|
||||||
|
filterIcon: <IconSearch />,
|
||||||
|
key: "name",
|
||||||
|
// eslint-disable-next-line react/no-unstable-nested-components
|
||||||
|
filterDropdown: ({ filterKeys, setFilterKeys, confirm }: any) => {
|
||||||
|
return (
|
||||||
|
<div className="arco-table-custom-filter">
|
||||||
|
<Input.Search
|
||||||
|
ref={inputRef}
|
||||||
|
searchButton
|
||||||
|
placeholder={t("enter_subscribe_name")!}
|
||||||
|
value={filterKeys[0] || ""}
|
||||||
|
onChange={(value) => {
|
||||||
|
setFilterKeys(value ? [value] : []);
|
||||||
|
}}
|
||||||
|
onSearch={() => {
|
||||||
|
confirm();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onFilter: (value, row) => (value ? row.name.indexOf(value) !== -1 : true),
|
||||||
|
onFilterDropdownVisibleChange: (visible) => {
|
||||||
|
if (visible) {
|
||||||
|
setTimeout(() => inputRef.current!.focus(), 150);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
className: "max-w-[240px]",
|
||||||
|
render: (col) => {
|
||||||
|
return (
|
||||||
|
<Tooltip content={col} position="tl">
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
display: "block",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{col}
|
||||||
|
</Text>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("version"),
|
||||||
|
dataIndex: "version",
|
||||||
|
width: 120,
|
||||||
|
align: "center",
|
||||||
|
key: "version",
|
||||||
|
render(col, item: Subscribe) {
|
||||||
|
return item.metadata.version && item.metadata.version[0];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("permission"),
|
||||||
|
width: 120,
|
||||||
|
align: "center",
|
||||||
|
key: "permission",
|
||||||
|
render(_, item: Subscribe) {
|
||||||
|
if (item.metadata.connect) {
|
||||||
|
return <div />;
|
||||||
|
}
|
||||||
|
return (item.metadata.connect as string[]).map((val) => {
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
src={`https://${val}/favicon.ico`}
|
||||||
|
alt={val}
|
||||||
|
height={16}
|
||||||
|
width={16}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("source"),
|
||||||
|
width: 100,
|
||||||
|
align: "center",
|
||||||
|
key: "source",
|
||||||
|
render(_, item: Subscribe) {
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
content={
|
||||||
|
<p style={{ margin: 0, padding: 0 }}>
|
||||||
|
{t("subscribe_url")}: {decodeURIComponent(item.url)}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Tag
|
||||||
|
icon={<IconUserAdd color="" />}
|
||||||
|
color="green"
|
||||||
|
bordered
|
||||||
|
style={{
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("subscribe_url")}
|
||||||
|
</Tag>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("last_updated"),
|
||||||
|
dataIndex: "updatetime",
|
||||||
|
align: "center",
|
||||||
|
key: "updatetime",
|
||||||
|
width: t("script_list_last_updated_width"),
|
||||||
|
sorter: (a, b) => a.updatetime - b.updatetime,
|
||||||
|
render(col, subscribe: Subscribe) {
|
||||||
|
return (
|
||||||
|
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
Message.info({
|
||||||
|
id: "checkupdate",
|
||||||
|
content: t("checking_for_updates"),
|
||||||
|
});
|
||||||
|
subscribeCtrl
|
||||||
|
.checkUpdate(subscribe.id)
|
||||||
|
.then((res) => {
|
||||||
|
if (res) {
|
||||||
|
Message.warning({
|
||||||
|
id: "checkupdate",
|
||||||
|
content: t("new_version_available"),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Message.success({
|
||||||
|
id: "checkupdate",
|
||||||
|
content: t("latest_version"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
Message.error({
|
||||||
|
id: "checkupdate",
|
||||||
|
content: `${t("check_update_failed")}: ${e.message}`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{semTime(new Date(col))}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("action"),
|
||||||
|
width: 120,
|
||||||
|
align: "center",
|
||||||
|
key: "action",
|
||||||
|
render(_, item: Subscribe) {
|
||||||
|
return (
|
||||||
|
<Button.Group>
|
||||||
|
<Popconfirm
|
||||||
|
title={t("confirm_delete_subscription")}
|
||||||
|
icon={<RiDeleteBin5Fill />}
|
||||||
|
onOk={() => {
|
||||||
|
setList(list.filter((val) => val.id !== item.id));
|
||||||
|
subscribeCtrl.delete(item.id).catch((e) => {
|
||||||
|
Message.error(`${t("delete_failed")}: ${e}`);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<RiDeleteBin5Fill />}
|
||||||
|
onClick={() => {}}
|
||||||
|
style={{
|
||||||
|
color: "var(--color-text-2)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Popconfirm>
|
||||||
|
</Button.Group>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
className="script-list subscribe-list"
|
||||||
|
style={{
|
||||||
|
height: "100%",
|
||||||
|
overflowY: "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Table
|
||||||
|
className="arco-drag-table-container"
|
||||||
|
rowKey="id"
|
||||||
|
tableLayoutFixed
|
||||||
|
columns={columns}
|
||||||
|
data={list}
|
||||||
|
pagination={{
|
||||||
|
total: list.length,
|
||||||
|
pageSize: list.length,
|
||||||
|
hideOnSinglePage: true,
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
minWidth: "1100px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SubscribeList;
|
346
src/pages/options/routes/Tools.tsx
Normal file
346
src/pages/options/routes/Tools.tsx
Normal file
@ -0,0 +1,346 @@
|
|||||||
|
import React, { useRef, useState } from "react";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Checkbox,
|
||||||
|
Drawer,
|
||||||
|
Empty,
|
||||||
|
Input,
|
||||||
|
List,
|
||||||
|
Message,
|
||||||
|
Modal,
|
||||||
|
Space,
|
||||||
|
} from "@arco-design/web-react";
|
||||||
|
import Title from "@arco-design/web-react/es/Typography/title";
|
||||||
|
import IoC from "@App/app/ioc";
|
||||||
|
import SynchronizeController from "@App/app/service/synchronize/controller";
|
||||||
|
import FileSystemFactory, { FileSystemType } from "@Pkg/filesystem/factory";
|
||||||
|
import { SystemConfig } from "@App/pkg/config/config";
|
||||||
|
import { File, FileReader } from "@Pkg/filesystem/filesystem";
|
||||||
|
import { formatUnixTime } from "@App/pkg/utils/utils";
|
||||||
|
import FileSystemParams from "@App/pages/components/FileSystemParams";
|
||||||
|
import { IconQuestionCircleFill } from "@arco-design/web-react/icon";
|
||||||
|
import { RefInputType } from "@arco-design/web-react/es/Input/interface";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import SystemController from "@App/app/service/system/controller";
|
||||||
|
|
||||||
|
function Tools() {
|
||||||
|
const [loading, setLoading] = useState<{ [key: string]: boolean }>({});
|
||||||
|
const syncCtrl = IoC.instance(SynchronizeController) as SynchronizeController;
|
||||||
|
const fileRef = useRef<HTMLInputElement>(null);
|
||||||
|
const systemConfig = IoC.instance(SystemConfig) as SystemConfig;
|
||||||
|
const [fileSystemType, setFilesystemType] = useState<FileSystemType>(
|
||||||
|
systemConfig.backup.filesystem
|
||||||
|
);
|
||||||
|
const [fileSystemParams, setFilesystemParam] = useState<{
|
||||||
|
[key: string]: any;
|
||||||
|
}>(systemConfig.backup.params[fileSystemType] || {});
|
||||||
|
const [backupFileList, setBackupFileList] = useState<File[]>([]);
|
||||||
|
const vscodeRef = useRef<RefInputType>(null);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Space
|
||||||
|
className="tools"
|
||||||
|
direction="vertical"
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
overflow: "auto",
|
||||||
|
position: "relative",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Card className="backup" title={t("backup")} bordered={false}>
|
||||||
|
<Space direction="vertical">
|
||||||
|
<Title heading={6}>{t("local")}</Title>
|
||||||
|
<Space>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={fileRef}
|
||||||
|
style={{ display: "none" }}
|
||||||
|
accept=".zip"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
loading={loading.local}
|
||||||
|
onClick={async () => {
|
||||||
|
setLoading((prev) => ({ ...prev, local: true }));
|
||||||
|
await syncCtrl.backup();
|
||||||
|
setLoading((prev) => ({ ...prev, local: false }));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("export_file")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={() => {
|
||||||
|
syncCtrl
|
||||||
|
.openImportFile(fileRef.current!)
|
||||||
|
.then(() => {
|
||||||
|
Message.success(t("select_import_script")!);
|
||||||
|
})
|
||||||
|
.then((e) => {
|
||||||
|
Message.error(`${t("import_error")}${e}`);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("import_file")}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
<Title heading={6}>{t("cloud")}</Title>
|
||||||
|
<FileSystemParams
|
||||||
|
preNode={t("backup_to")}
|
||||||
|
onChangeFileSystemType={(type) => {
|
||||||
|
setFilesystemType(type);
|
||||||
|
}}
|
||||||
|
onChangeFileSystemParams={(params) => {
|
||||||
|
setFilesystemParam(params);
|
||||||
|
}}
|
||||||
|
actionButton={[
|
||||||
|
<Button
|
||||||
|
key="backup"
|
||||||
|
type="primary"
|
||||||
|
loading={loading.cloud}
|
||||||
|
onClick={() => {
|
||||||
|
// Store parameters
|
||||||
|
const params = { ...systemConfig.backup.params };
|
||||||
|
params[fileSystemType] = fileSystemParams;
|
||||||
|
systemConfig.backup = {
|
||||||
|
filesystem: fileSystemType,
|
||||||
|
params,
|
||||||
|
};
|
||||||
|
setLoading((prev) => ({ ...prev, cloud: true }));
|
||||||
|
Message.info(t("preparing_backup")!);
|
||||||
|
syncCtrl
|
||||||
|
.backupToCloud(fileSystemType, fileSystemParams)
|
||||||
|
.then(() => {
|
||||||
|
Message.success(t("backup_success")!);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
Message.error(`${t("backup_failed")}: ${e}`);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading((prev) => ({ ...prev, cloud: false }));
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("backup")}
|
||||||
|
</Button>,
|
||||||
|
<Button
|
||||||
|
key="list"
|
||||||
|
type="primary"
|
||||||
|
onClick={async () => {
|
||||||
|
let fs = await FileSystemFactory.create(
|
||||||
|
fileSystemType,
|
||||||
|
fileSystemParams
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
fs = await fs.openDir("ScriptCat");
|
||||||
|
let list = await fs.list();
|
||||||
|
list.sort((a, b) => b.updatetime - a.updatetime);
|
||||||
|
// Filter non-zip files
|
||||||
|
list = list.filter((file) => file.name.endsWith(".zip"));
|
||||||
|
if (list.length === 0) {
|
||||||
|
Message.info(t("no_backup_files")!);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setBackupFileList(list);
|
||||||
|
} catch (e) {
|
||||||
|
Message.error(`${t("get_backup_files_failed")}: ${e}`);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("backup_list")}
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
fileSystemType={fileSystemType}
|
||||||
|
fileSystemParams={fileSystemParams}
|
||||||
|
/>
|
||||||
|
<Drawer
|
||||||
|
width={400}
|
||||||
|
title={
|
||||||
|
<div className="flex flex-row justify-between w-full gap-10">
|
||||||
|
<span>{t("backup_list")}</span>
|
||||||
|
<Button
|
||||||
|
type="secondary"
|
||||||
|
size="mini"
|
||||||
|
onClick={async () => {
|
||||||
|
let fs = await FileSystemFactory.create(
|
||||||
|
fileSystemType,
|
||||||
|
fileSystemParams
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
fs = await fs.openDir("ScriptCat");
|
||||||
|
const url = await fs.getDirUrl();
|
||||||
|
if (url) {
|
||||||
|
window.open(url, "_black");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Message.error(`${t("get_backup_dir_url_failed")}: ${e}`);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("open_backup_dir")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
visible={backupFileList.length !== 0}
|
||||||
|
onOk={() => {
|
||||||
|
setBackupFileList([]);
|
||||||
|
}}
|
||||||
|
onCancel={() => {
|
||||||
|
setBackupFileList([]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<List
|
||||||
|
bordered={false}
|
||||||
|
dataSource={backupFileList}
|
||||||
|
render={(item: File) => (
|
||||||
|
<List.Item key={item.name}>
|
||||||
|
<List.Item.Meta
|
||||||
|
title={item.name}
|
||||||
|
description={formatUnixTime(item.updatetime / 1000)}
|
||||||
|
/>
|
||||||
|
<Space className="w-full justify-end">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
onClick={async () => {
|
||||||
|
Message.info(t("pulling_data_from_cloud")!);
|
||||||
|
let fs = await FileSystemFactory.create(
|
||||||
|
fileSystemType,
|
||||||
|
fileSystemParams
|
||||||
|
);
|
||||||
|
let file: FileReader;
|
||||||
|
let data: Blob;
|
||||||
|
try {
|
||||||
|
fs = await fs.openDir("ScriptCat");
|
||||||
|
file = await fs.open(item);
|
||||||
|
data = (await file.read("blob")) as Blob;
|
||||||
|
} catch (e) {
|
||||||
|
Message.error(`${t("pull_failed")}: ${e}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = URL.createObjectURL(data);
|
||||||
|
setTimeout(() => {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}, 60 * 100000);
|
||||||
|
syncCtrl
|
||||||
|
.openImportWindow(item.name, url)
|
||||||
|
.then(() => {
|
||||||
|
Message.success(t("select_import_script")!);
|
||||||
|
})
|
||||||
|
.then((e) => {
|
||||||
|
Message.error(`${t("import_error")}${e}`);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("restore")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
status="danger"
|
||||||
|
size="small"
|
||||||
|
onClick={() => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: t("confirm_delete"),
|
||||||
|
content: `${t("confirm_delete_backup_file")}${
|
||||||
|
item.name
|
||||||
|
}?`,
|
||||||
|
onOk: async () => {
|
||||||
|
let fs = await FileSystemFactory.create(
|
||||||
|
fileSystemType,
|
||||||
|
fileSystemParams
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
fs = await fs.openDir("ScriptCat");
|
||||||
|
await fs.delete(item.name);
|
||||||
|
setBackupFileList(
|
||||||
|
backupFileList.filter(
|
||||||
|
(i) => i.name !== item.name
|
||||||
|
)
|
||||||
|
);
|
||||||
|
Message.success(t("delete_success")!);
|
||||||
|
} catch (e) {
|
||||||
|
Message.error(`${t("delete_failed")}${e}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("delete")}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</List.Item>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Drawer>
|
||||||
|
<Title heading={6}>{t("backup_strategy")}</Title>
|
||||||
|
<Empty description={t("under_construction")} />
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
<span>{t("development_debugging")}</span>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
style={{
|
||||||
|
height: 24,
|
||||||
|
}}
|
||||||
|
icon={
|
||||||
|
<IconQuestionCircleFill
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
href="https://www.bilibili.com/video/BV16q4y157CP"
|
||||||
|
target="_blank"
|
||||||
|
iconOnly
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
bordered={false}
|
||||||
|
>
|
||||||
|
<Space direction="vertical">
|
||||||
|
<Title heading={6}>{t("vscode_url")}</Title>
|
||||||
|
<Input
|
||||||
|
ref={vscodeRef}
|
||||||
|
defaultValue={systemConfig.vscodeUrl}
|
||||||
|
onChange={(value) => {
|
||||||
|
systemConfig.vscodeUrl = value;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
onChange={(checked) => {
|
||||||
|
systemConfig.vscodeReconnect = checked;
|
||||||
|
}}
|
||||||
|
defaultChecked={systemConfig.vscodeReconnect}
|
||||||
|
>
|
||||||
|
{t("auto_connect_vscode_service")}
|
||||||
|
</Checkbox>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={() => {
|
||||||
|
const ctrl = IoC.instance(SystemController) as SystemController;
|
||||||
|
ctrl
|
||||||
|
.connectVSCode()
|
||||||
|
.then(() => {
|
||||||
|
Message.success(t("connection_success")!);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
Message.error(`${t("connection_failed")}: ${e}`);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("connect")}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Tools;
|
927
src/pages/options/routes/script/ScriptEditor.tsx
Normal file
927
src/pages/options/routes/script/ScriptEditor.tsx
Normal file
@ -0,0 +1,927 @@
|
|||||||
|
import { Script, ScriptDAO } from "@App/app/repo/scripts";
|
||||||
|
import CodeEditor from "@App/pages/components/CodeEditor";
|
||||||
|
import React, { 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";
|
||||||
|
import TabPane from "@arco-design/web-react/es/Tabs/tab-pane";
|
||||||
|
import ScriptController from "@App/app/service/script/controller";
|
||||||
|
import normalTpl from "@App/template/normal.tpl";
|
||||||
|
import crontabTpl from "@App/template/crontab.tpl";
|
||||||
|
import backgroundTpl from "@App/template/background.tpl";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import "./index.css";
|
||||||
|
import IoC from "@App/app/ioc";
|
||||||
|
import LoggerCore from "@App/app/logger/core";
|
||||||
|
import Logger from "@App/app/logger/logger";
|
||||||
|
import { prepareScriptByCode } from "@App/pkg/utils/script";
|
||||||
|
import RuntimeController from "@App/runtime/content/runtime";
|
||||||
|
import ScriptStorage from "@App/pages/components/ScriptStorage";
|
||||||
|
import ScriptResource from "@App/pages/components/ScriptResource";
|
||||||
|
import ScriptSetting from "@App/pages/components/ScriptSetting";
|
||||||
|
|
||||||
|
const { Row } = Grid;
|
||||||
|
const { Col } = Grid;
|
||||||
|
|
||||||
|
// 声明一个Map存储Script
|
||||||
|
const ScriptMap = new Map();
|
||||||
|
|
||||||
|
type HotKey = {
|
||||||
|
hotKey: number;
|
||||||
|
action: (script: Script, codeEditor: editor.IStandaloneCodeEditor) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Editor: React.FC<{
|
||||||
|
id: string;
|
||||||
|
script: Script;
|
||||||
|
hotKeys: HotKey[];
|
||||||
|
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为key,Script为value,储存Script
|
||||||
|
ScriptMap.has(script.uuid) || ScriptMap.set(script.uuid, script);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!codeEditor.current || !codeEditor.current.editor) {
|
||||||
|
setTimeout(() => {
|
||||||
|
setInit(true);
|
||||||
|
}, 200);
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
// 初始化editor时将Script的uuid绑定到editor上
|
||||||
|
// @ts-ignore
|
||||||
|
if (!codeEditor.current.editor.uuid) {
|
||||||
|
// @ts-ignore
|
||||||
|
codeEditor.current.editor.uuid = script.uuid;
|
||||||
|
}
|
||||||
|
hotKeys.forEach((item) => {
|
||||||
|
codeEditor.current?.editor.addCommand(item.hotKey, () => {
|
||||||
|
// 获取当前激活的editor(通过editor._focusTracker._hasFocus判断editor激活状态 可能有更好的方法)
|
||||||
|
const activeEditor = editor
|
||||||
|
.getEditors()
|
||||||
|
// @ts-ignore
|
||||||
|
// eslint-disable-next-line no-underscore-dangle
|
||||||
|
.find((i) => i._focusTracker._hasFocus);
|
||||||
|
|
||||||
|
// 仅在获取到激活的editor时,通过editor上绑定的uuid获取Script,并指定激活的editor执行快捷键action
|
||||||
|
activeEditor &&
|
||||||
|
// @ts-ignore
|
||||||
|
item.action(ScriptMap.get(activeEditor.uuid), activeEditor);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
codeEditor.current.editor.onKeyUp(() => {
|
||||||
|
onChange(codeEditor.current?.editor.getValue() || "");
|
||||||
|
});
|
||||||
|
callbackEditor(codeEditor.current.editor);
|
||||||
|
return () => {};
|
||||||
|
}, [init]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CodeEditor
|
||||||
|
id={id}
|
||||||
|
ref={codeEditor}
|
||||||
|
code={script.code}
|
||||||
|
diffCode=""
|
||||||
|
editable
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type EditorMenu = {
|
||||||
|
title: string;
|
||||||
|
tooltip?: string;
|
||||||
|
action?: (script: Script, e: editor.IStandaloneCodeEditor) => void;
|
||||||
|
items?: {
|
||||||
|
title: string;
|
||||||
|
tooltip?: string;
|
||||||
|
hotKey?: number;
|
||||||
|
hotKeyString?: string;
|
||||||
|
action: (script: Script, e: editor.IStandaloneCodeEditor) => void;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const emptyScript = async (template: string, hotKeys: any, target?: string) => {
|
||||||
|
let code = "";
|
||||||
|
switch (template) {
|
||||||
|
case "background":
|
||||||
|
code = backgroundTpl;
|
||||||
|
break;
|
||||||
|
case "crontab":
|
||||||
|
code = crontabTpl;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
code = normalTpl;
|
||||||
|
if (target === "initial") {
|
||||||
|
const url = await new Promise<string>((resolve) => {
|
||||||
|
chrome.storage.local.get(["activeTabUrl"], (result) => {
|
||||||
|
chrome.storage.local.remove(["activeTabUrl"]);
|
||||||
|
if (result.activeTabUrl) {
|
||||||
|
resolve(result.activeTabUrl.url);
|
||||||
|
} else {
|
||||||
|
resolve("undefind");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
code = code.replace("{{match}}", url);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const prepareScript = await prepareScriptByCode(code, "", uuidv4());
|
||||||
|
const { script } = prepareScript;
|
||||||
|
|
||||||
|
return Promise.resolve({
|
||||||
|
script,
|
||||||
|
code: script.code,
|
||||||
|
active: true,
|
||||||
|
hotKeys,
|
||||||
|
isChanged: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
type visibleItem = "scriptStorage" | "scriptSetting" | "scriptResource";
|
||||||
|
|
||||||
|
const popstate = () => {
|
||||||
|
// eslint-disable-next-line no-restricted-globals, no-alert
|
||||||
|
if (confirm("脚本已修改, 离开后会丢失修改, 是否继续?")) {
|
||||||
|
window.history.back();
|
||||||
|
window.removeEventListener("popstate", popstate);
|
||||||
|
} else {
|
||||||
|
window.history.pushState(null, "", window.location.href);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
function ScriptEditor() {
|
||||||
|
const scriptDAO = new ScriptDAO();
|
||||||
|
const scriptCtrl = IoC.instance(ScriptController) as ScriptController;
|
||||||
|
const runtimeCtrl = IoC.instance(RuntimeController) as RuntimeController;
|
||||||
|
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<
|
||||||
|
{
|
||||||
|
script: Script;
|
||||||
|
code: string;
|
||||||
|
active: boolean;
|
||||||
|
hotKeys: HotKey[];
|
||||||
|
editor?: editor.IStandaloneCodeEditor;
|
||||||
|
isChanged: boolean;
|
||||||
|
}[]
|
||||||
|
>([]);
|
||||||
|
const [scriptList, setScriptList] = useState<Script[]>([]);
|
||||||
|
const [currentScript, setCurrentScript] = useState<Script>();
|
||||||
|
const [selectSciptButtonAndTab, setSelectSciptButtonAndTab] =
|
||||||
|
useState<string>("");
|
||||||
|
const [rightOperationTab, setRightOperationTab] = useState<{
|
||||||
|
key: string;
|
||||||
|
uuid: string;
|
||||||
|
selectSciptButtonAndTab: string;
|
||||||
|
}>();
|
||||||
|
const setShow = (key: visibleItem, show: boolean) => {
|
||||||
|
Object.keys(visible).forEach((k) => {
|
||||||
|
visible[k] = false;
|
||||||
|
});
|
||||||
|
visible[key] = show;
|
||||||
|
setVisible({ ...visible });
|
||||||
|
};
|
||||||
|
|
||||||
|
const { id } = useParams();
|
||||||
|
const save = (
|
||||||
|
script: Script,
|
||||||
|
e: editor.IStandaloneCodeEditor
|
||||||
|
): Promise<Script> => {
|
||||||
|
// 解析code生成新的script并更新
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
prepareScriptByCode(e.getValue(), script.origin || "", script.uuid)
|
||||||
|
.then((prepareScript) => {
|
||||||
|
const newScript = prepareScript.script;
|
||||||
|
scriptCtrl.upsert(newScript).then(
|
||||||
|
() => {
|
||||||
|
if (!newScript.name) {
|
||||||
|
Message.warning("脚本name不可以设置为空");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (newScript.id === 0) {
|
||||||
|
Message.success("新建成功,请注意后台脚本不会默认开启");
|
||||||
|
// 保存的时候如何左侧没有脚本即新建
|
||||||
|
setScriptList((prev) => {
|
||||||
|
setSelectSciptButtonAndTab(newScript.uuid);
|
||||||
|
return [newScript, ...prev];
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setScriptList((prev) => {
|
||||||
|
// eslint-disable-next-line no-shadow, array-callback-return
|
||||||
|
prev.map((script: Script) => {
|
||||||
|
if (script.uuid === newScript.uuid) {
|
||||||
|
script.name = newScript.name;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return [...prev];
|
||||||
|
});
|
||||||
|
Message.success("保存成功");
|
||||||
|
}
|
||||||
|
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].isChanged = false;
|
||||||
|
prev[i].script.name = newScript.name;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolve(newScript);
|
||||||
|
return [...prev];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
Message.error(`保存失败: ${err}`);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
Message.error(`错误的脚本代码: ${err}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const saveAs = (script: Script, e: editor.IStandaloneCodeEditor) => {
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
chrome.downloads.download(
|
||||||
|
{
|
||||||
|
url: URL.createObjectURL(
|
||||||
|
new Blob([e.getValue()], { type: "text/javascript" })
|
||||||
|
),
|
||||||
|
saveAs: true, // true直接弹出对话框;false弹出下载选项
|
||||||
|
filename: `${script.name}.user.js`,
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
/*
|
||||||
|
chrome扩展api发生错误无法通过try/catch捕获,必须在api回调函数中访问chrome.runtime.lastError进行获取
|
||||||
|
var chrome.runtime.lastError: chrome.runtime.LastError | undefined
|
||||||
|
This will be defined during an API method callback if there was an error
|
||||||
|
*/
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log("另存为失败: ", chrome.runtime.lastError);
|
||||||
|
Message.error(`另存为失败: ${chrome.runtime.lastError.message}`);
|
||||||
|
} else {
|
||||||
|
Message.success("另存为成功");
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const menu: EditorMenu[] = [
|
||||||
|
{
|
||||||
|
title: "文件",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: "保存",
|
||||||
|
hotKey: KeyMod.CtrlCmd | KeyCode.KeyS,
|
||||||
|
hotKeyString: "Ctrl+S",
|
||||||
|
action: save,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "另存为",
|
||||||
|
hotKey: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyS,
|
||||||
|
hotKeyString: "Ctrl+Shift+S",
|
||||||
|
action: saveAs,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "运行",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: "调试",
|
||||||
|
hotKey: KeyMod.CtrlCmd | KeyCode.F5,
|
||||||
|
hotKeyString: "Ctrl+F5",
|
||||||
|
tooltip:
|
||||||
|
"只有后台脚本/定时脚本才能调试, 且调试模式下不对进行权限校验(例如@connect)",
|
||||||
|
action: async (script, e) => {
|
||||||
|
// 保存更新代码之后再调试
|
||||||
|
const newScript = await save(script, e);
|
||||||
|
Message.loading({
|
||||||
|
id: "debug_script",
|
||||||
|
content: "正在准备脚本资源...",
|
||||||
|
duration: 3000,
|
||||||
|
});
|
||||||
|
runtimeCtrl
|
||||||
|
.debugScript(newScript)
|
||||||
|
.then(() => {
|
||||||
|
Message.success({
|
||||||
|
id: "debug_script",
|
||||||
|
content: "构建成功, 可以打开开发者工具在控制台中查看输出",
|
||||||
|
duration: 3000,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
LoggerCore.getLogger(Logger.E(err)).debug("debug script error");
|
||||||
|
Message.error({
|
||||||
|
id: "debug_script",
|
||||||
|
content: `构建失败: ${err}`,
|
||||||
|
duration: 3000,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "工具",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: "脚本储存",
|
||||||
|
tooltip: "可以管理脚本GM_value的储存数据",
|
||||||
|
action(script) {
|
||||||
|
setShow("scriptStorage", true);
|
||||||
|
setCurrentScript(script);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "脚本资源",
|
||||||
|
tooltip: "管理@resource,@require下载的资源",
|
||||||
|
action(script) {
|
||||||
|
setShow("scriptResource", true);
|
||||||
|
setCurrentScript(script);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "设置",
|
||||||
|
tooltip: "对脚本进行一些自定义设置",
|
||||||
|
action(script) {
|
||||||
|
setShow("scriptSetting", true);
|
||||||
|
setCurrentScript(script);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 根据菜单生产快捷键
|
||||||
|
const hotKeys: HotKey[] = [];
|
||||||
|
let activeTab = "";
|
||||||
|
for (let i = 0; i < editors.length; i += 1) {
|
||||||
|
if (editors[i].active) {
|
||||||
|
activeTab = i.toString();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
menu.forEach((item) => {
|
||||||
|
item.items &&
|
||||||
|
item.items.forEach((menuItem) => {
|
||||||
|
if (menuItem.hotKey) {
|
||||||
|
hotKeys.push({
|
||||||
|
hotKey: menuItem.hotKey,
|
||||||
|
action: menuItem.action,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
useEffect(() => {
|
||||||
|
scriptDAO.table
|
||||||
|
.orderBy("sort")
|
||||||
|
.toArray()
|
||||||
|
.then((scripts) => {
|
||||||
|
setScriptList(scripts);
|
||||||
|
// 如果有id则打开对应的脚本
|
||||||
|
if (id) {
|
||||||
|
const iId = parseInt(id, 10);
|
||||||
|
for (let i = 0; i < scripts.length; i += 1) {
|
||||||
|
if (scripts[i].id === iId) {
|
||||||
|
editors.push({
|
||||||
|
script: scripts[i],
|
||||||
|
code: scripts[i].code,
|
||||||
|
active: true,
|
||||||
|
hotKeys,
|
||||||
|
isChanged: false,
|
||||||
|
});
|
||||||
|
setSelectSciptButtonAndTab(scripts[i].uuid);
|
||||||
|
setEditors([...editors]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!id) {
|
||||||
|
emptyScript(template || "", hotKeys, target).then((e) => {
|
||||||
|
editors.push(e);
|
||||||
|
setEditors([...editors]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 控制onbeforeunload
|
||||||
|
useEffect(() => {
|
||||||
|
let flag = false;
|
||||||
|
|
||||||
|
for (let i = 0; i < editors.length; i += 1) {
|
||||||
|
if (editors[i].isChanged) {
|
||||||
|
flag = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flag) {
|
||||||
|
const beforeunload = () => {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
window.onbeforeunload = beforeunload;
|
||||||
|
window.history.pushState(null, "", window.location.href);
|
||||||
|
window.addEventListener("popstate", popstate);
|
||||||
|
} else {
|
||||||
|
window.removeEventListener("popstate", popstate);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.onbeforeunload = null;
|
||||||
|
};
|
||||||
|
}, [editors]);
|
||||||
|
|
||||||
|
// 对tab点击右键进行的操作
|
||||||
|
useEffect(() => {
|
||||||
|
let newEditors = [];
|
||||||
|
let selectEditorIndex: number = 0;
|
||||||
|
// 1 关闭当前, 2关闭其它, 3关闭左侧, 4关闭右侧
|
||||||
|
if (rightOperationTab) {
|
||||||
|
// eslint-disable-next-line default-case
|
||||||
|
switch (rightOperationTab.key) {
|
||||||
|
case "1":
|
||||||
|
newEditors = editors.filter(
|
||||||
|
(item) => item.script.uuid !== rightOperationTab.uuid
|
||||||
|
);
|
||||||
|
if (newEditors.length > 0) {
|
||||||
|
// 还有的话,如果之前有选中的,那么我们还是选中之前的,如果没有选中的我们就选中第一个
|
||||||
|
if (
|
||||||
|
rightOperationTab.selectSciptButtonAndTab ===
|
||||||
|
rightOperationTab.uuid
|
||||||
|
) {
|
||||||
|
if (newEditors.length > 0) {
|
||||||
|
newEditors[0].active = true;
|
||||||
|
setSelectSciptButtonAndTab(newEditors[0].script.uuid);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setSelectSciptButtonAndTab(
|
||||||
|
rightOperationTab.selectSciptButtonAndTab
|
||||||
|
);
|
||||||
|
// 之前选中的tab
|
||||||
|
editors.filter((item) => {
|
||||||
|
if (
|
||||||
|
item.script.uuid === rightOperationTab.selectSciptButtonAndTab
|
||||||
|
) {
|
||||||
|
item.active = true;
|
||||||
|
} else {
|
||||||
|
item.active = false;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
item.script.uuid === rightOperationTab.selectSciptButtonAndTab
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setEditors([...newEditors]);
|
||||||
|
break;
|
||||||
|
case "2":
|
||||||
|
newEditors = editors.filter(
|
||||||
|
(item) => item.script.uuid === rightOperationTab.uuid
|
||||||
|
);
|
||||||
|
setSelectSciptButtonAndTab(rightOperationTab.uuid);
|
||||||
|
setEditors([...newEditors]);
|
||||||
|
break;
|
||||||
|
case "3":
|
||||||
|
editors.map((item, index) => {
|
||||||
|
if (item.script.uuid === rightOperationTab.uuid) {
|
||||||
|
selectEditorIndex = index;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
newEditors = editors.splice(selectEditorIndex);
|
||||||
|
setEditors([...newEditors]);
|
||||||
|
break;
|
||||||
|
case "4":
|
||||||
|
editors.map((item, index) => {
|
||||||
|
if (item.script.uuid === rightOperationTab.uuid) {
|
||||||
|
selectEditorIndex = index;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
newEditors = editors.splice(0, selectEditorIndex + 1);
|
||||||
|
setEditors([...newEditors]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [rightOperationTab]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="h-full flex flex-col"
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
left: -10,
|
||||||
|
top: -10,
|
||||||
|
width: "calc(100% + 20px)",
|
||||||
|
height: "calc(100% + 20px)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ScriptStorage
|
||||||
|
visible={visible.scriptStorage}
|
||||||
|
script={currentScript}
|
||||||
|
onOk={() => {
|
||||||
|
setShow("scriptStorage", false);
|
||||||
|
}}
|
||||||
|
onCancel={() => {
|
||||||
|
setShow("scriptStorage", false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ScriptResource
|
||||||
|
visible={visible.scriptResource}
|
||||||
|
script={currentScript}
|
||||||
|
onOk={() => {
|
||||||
|
setShow("scriptResource", false);
|
||||||
|
}}
|
||||||
|
onCancel={() => {
|
||||||
|
setShow("scriptResource", false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ScriptSetting
|
||||||
|
visible={visible.scriptSetting}
|
||||||
|
script={currentScript!}
|
||||||
|
onOk={() => {
|
||||||
|
setShow("scriptSetting", false);
|
||||||
|
}}
|
||||||
|
onCancel={() => {
|
||||||
|
setShow("scriptSetting", false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="h-6"
|
||||||
|
style={{
|
||||||
|
borderBottom: "1px solid var(--color-neutral-3)",
|
||||||
|
background: "var(--color-secondary)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex flex-row">
|
||||||
|
{menu.map((item, index) => {
|
||||||
|
if (!item.items) {
|
||||||
|
// 没有子菜单
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key={`m_${item.title}`}
|
||||||
|
size="mini"
|
||||||
|
onClick={() => {
|
||||||
|
setEditors((prev) => {
|
||||||
|
prev.forEach((e) => {
|
||||||
|
if (e.active) {
|
||||||
|
item.action && item.action(e.script, e.editor!);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return prev;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.title}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
key={`d_${index.toString()}`}
|
||||||
|
droplist={
|
||||||
|
<Menu
|
||||||
|
style={{
|
||||||
|
padding: "0",
|
||||||
|
margin: "0",
|
||||||
|
borderRadius: "0",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.items.map((menuItem, i) => {
|
||||||
|
const btn = (
|
||||||
|
<Button
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
textAlign: "left",
|
||||||
|
alignSelf: "center",
|
||||||
|
verticalAlign: "middle",
|
||||||
|
}}
|
||||||
|
key={`sm_${menuItem.title}`}
|
||||||
|
size="mini"
|
||||||
|
onClick={() => {
|
||||||
|
setEditors((prev) => {
|
||||||
|
prev.forEach((e) => {
|
||||||
|
if (e.active) {
|
||||||
|
menuItem.action(e.script, e.editor!);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return prev;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
minWidth: "70px",
|
||||||
|
float: "left",
|
||||||
|
fontSize: "14px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{menuItem.title}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
minWidth: "50px",
|
||||||
|
float: "left",
|
||||||
|
color: "rgb(165 165 165)",
|
||||||
|
fontSize: "12px",
|
||||||
|
lineHeight: "22px", // 不知道除此以外怎么垂直居中
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{menuItem.hotKeyString}
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Menu.Item
|
||||||
|
key={`m_${i.toString()}`}
|
||||||
|
style={{
|
||||||
|
height: "unset",
|
||||||
|
padding: "0",
|
||||||
|
lineHeight: "unset",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{menuItem.tooltip ? (
|
||||||
|
<Tooltip
|
||||||
|
key={`m${i.toString()}`}
|
||||||
|
position="right"
|
||||||
|
content={menuItem.tooltip}
|
||||||
|
>
|
||||||
|
{btn}
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
btn
|
||||||
|
)}
|
||||||
|
</Menu.Item>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Menu>
|
||||||
|
}
|
||||||
|
trigger="click"
|
||||||
|
position="bl"
|
||||||
|
>
|
||||||
|
<Button key={`m_${item.title}`} size="mini">
|
||||||
|
{item.title}
|
||||||
|
</Button>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Row
|
||||||
|
className="flex flex-grow flex-1"
|
||||||
|
style={{
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Col
|
||||||
|
span={4}
|
||||||
|
className="h-full"
|
||||||
|
style={{
|
||||||
|
overflow: "scroll",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="flex flex-col"
|
||||||
|
style={{
|
||||||
|
backgroundColor: "var(--color-secondary)",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
className="text-left"
|
||||||
|
size="mini"
|
||||||
|
disabled
|
||||||
|
style={{
|
||||||
|
color: "var(--color-text-2)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
已安装脚本
|
||||||
|
</Button>
|
||||||
|
{scriptList.map((script) => (
|
||||||
|
<Button
|
||||||
|
key={`s_${script.uuid}`}
|
||||||
|
size="mini"
|
||||||
|
className="text-left"
|
||||||
|
style={{
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
backgroundColor:
|
||||||
|
selectSciptButtonAndTab === script.uuid ? "gray" : "",
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectSciptButtonAndTab(script.uuid);
|
||||||
|
// 如果已经打开则激活
|
||||||
|
let flag = false;
|
||||||
|
for (let i = 0; i < editors.length; i += 1) {
|
||||||
|
if (editors[i].script.uuid === script.uuid) {
|
||||||
|
editors[i].active = true;
|
||||||
|
flag = true;
|
||||||
|
} else {
|
||||||
|
editors[i].active = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!flag) {
|
||||||
|
// 如果没有打开则打开
|
||||||
|
editors.push({
|
||||||
|
script,
|
||||||
|
code: script.code,
|
||||||
|
active: true,
|
||||||
|
hotKeys,
|
||||||
|
isChanged: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setEditors([...editors]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{script.name}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col span={20} className="flex! flex-col h-full">
|
||||||
|
<Tabs
|
||||||
|
editable
|
||||||
|
activeTab={activeTab}
|
||||||
|
className="edit-tabs"
|
||||||
|
type="card-gutter"
|
||||||
|
style={{
|
||||||
|
overflow: "inherit",
|
||||||
|
}}
|
||||||
|
onChange={(index: string) => {
|
||||||
|
editors.forEach((_, i) => {
|
||||||
|
if (i.toString() === index) {
|
||||||
|
setSelectSciptButtonAndTab(editors[i].script.uuid);
|
||||||
|
editors[i].active = true;
|
||||||
|
} else {
|
||||||
|
editors[i].active = false;
|
||||||
|
}
|
||||||
|
setEditors([...editors]);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onAddTab={() => {
|
||||||
|
emptyScript(template || "", hotKeys).then((e) => {
|
||||||
|
setEditors((prev) => {
|
||||||
|
prev.forEach((item) => {
|
||||||
|
item.active = false;
|
||||||
|
});
|
||||||
|
setSelectSciptButtonAndTab(e.script.uuid);
|
||||||
|
prev.push(e);
|
||||||
|
return [...prev];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onDeleteTab={(index: string) => {
|
||||||
|
// 处理删除
|
||||||
|
setEditors((prev) => {
|
||||||
|
const i = parseInt(index, 10);
|
||||||
|
if (prev[i].isChanged) {
|
||||||
|
// eslint-disable-next-line no-restricted-globals, no-alert
|
||||||
|
if (!confirm("脚本已修改, 关闭后会丢失修改, 是否继续?")) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (prev.length === 1) {
|
||||||
|
// 如果是id打开的回退到列表
|
||||||
|
if (id) {
|
||||||
|
navigate("/");
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
// 如果没有打开的了, 则打开一个空白的
|
||||||
|
emptyScript(template || "", hotKeys).then((e) => {
|
||||||
|
setEditors([e]);
|
||||||
|
});
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
if (prev[i].active) {
|
||||||
|
// 如果关闭的是当前激活的, 则激活下一个
|
||||||
|
if (i === prev.length - 1) {
|
||||||
|
prev[i - 1].active = true;
|
||||||
|
setSelectSciptButtonAndTab(prev[i - 1].script.uuid);
|
||||||
|
} else {
|
||||||
|
prev[i + 1].active = true;
|
||||||
|
setSelectSciptButtonAndTab(prev[i - 1].script.uuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prev.splice(i, 1);
|
||||||
|
return [...prev];
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{editors.map((e, index) => (
|
||||||
|
<TabPane
|
||||||
|
destroyOnHide
|
||||||
|
key={index!.toString()}
|
||||||
|
title={
|
||||||
|
<Dropdown
|
||||||
|
trigger="contextMenu"
|
||||||
|
position="bl"
|
||||||
|
droplist={
|
||||||
|
<Menu
|
||||||
|
// eslint-disable-next-line no-shadow
|
||||||
|
onClickMenuItem={(key) => {
|
||||||
|
setRightOperationTab({
|
||||||
|
...rightOperationTab,
|
||||||
|
key,
|
||||||
|
uuid: e.script.uuid,
|
||||||
|
selectSciptButtonAndTab,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Menu.Item key="1">关闭当前标签页</Menu.Item>
|
||||||
|
<Menu.Item key="2">关闭其他标签页</Menu.Item>
|
||||||
|
<Menu.Item key="3">关闭左侧标签页</Menu.Item>
|
||||||
|
<Menu.Item key="4">关闭右侧标签页</Menu.Item>
|
||||||
|
</Menu>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
// eslint-disable-next-line no-nested-ternary
|
||||||
|
color: e.isChanged
|
||||||
|
? "rgb(var(--orange-5))" // eslint-disable-next-line no-nested-ternary
|
||||||
|
: e.script.uuid === selectSciptButtonAndTab
|
||||||
|
? "rgb(var(--green-7))"
|
||||||
|
: e.active
|
||||||
|
? "rgb(var(--green-7))"
|
||||||
|
: "var(--color-text-1)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{e.script.name}
|
||||||
|
</span>
|
||||||
|
</Dropdown>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
<div className="flex flex-grow flex-1">
|
||||||
|
{editors.map((item) => {
|
||||||
|
// 先这样吧
|
||||||
|
setTimeout(() => {
|
||||||
|
if (item.active && item.editor) {
|
||||||
|
item.editor.focus();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="w-full"
|
||||||
|
key={`fe_${item.script.uuid}`}
|
||||||
|
style={{
|
||||||
|
display: item.active ? "block" : "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Editor
|
||||||
|
id={`e_${item.script.uuid}`}
|
||||||
|
script={item.script}
|
||||||
|
hotKeys={item.hotKeys}
|
||||||
|
callbackEditor={(e) => {
|
||||||
|
setEditors((prev) => {
|
||||||
|
prev.forEach((v) => {
|
||||||
|
if (v.script.uuid === item.script.uuid) {
|
||||||
|
v.editor = e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return [...prev];
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onChange={(code) => {
|
||||||
|
const isChanged = !(item.code === code);
|
||||||
|
if (isChanged !== item.isChanged) {
|
||||||
|
setEditors((prev) => {
|
||||||
|
prev.forEach((v) => {
|
||||||
|
if (v.script.uuid === item.script.uuid) {
|
||||||
|
v.isChanged = isChanged;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return [...prev];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ScriptEditor;
|
6
src/pages/options/routes/script/index.css
Normal file
6
src/pages/options/routes/script/index.css
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
.edit-tabs .arco-tabs-header-title-text {
|
||||||
|
max-width: 200px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
223
src/pages/options/routes/utils.tsx
Normal file
223
src/pages/options/routes/utils.tsx
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
/* eslint-disable import/prefer-default-export */
|
||||||
|
import React from "react";
|
||||||
|
import IoC from "@App/app/ioc";
|
||||||
|
import { Metadata, Script, ScriptDAO } from "@App/app/repo/scripts";
|
||||||
|
import ValueManager from "@App/app/service/value/manager";
|
||||||
|
import { Avatar, Button, Space, Tooltip } from "@arco-design/web-react";
|
||||||
|
import {
|
||||||
|
IconBug,
|
||||||
|
IconCode,
|
||||||
|
IconGithub,
|
||||||
|
IconHome,
|
||||||
|
} from "@arco-design/web-react/icon";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
// 较对脚本排序位置
|
||||||
|
export function scriptListSort(result: Script[]) {
|
||||||
|
const dao = new ScriptDAO();
|
||||||
|
for (let i = 0; i < result.length; i += 1) {
|
||||||
|
if (result[i].sort !== i) {
|
||||||
|
dao.update(result[i].id, { sort: i });
|
||||||
|
result[i].sort = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 安装url转home主页
|
||||||
|
export function installUrlToHome(installUrl: string) {
|
||||||
|
try {
|
||||||
|
// 解析scriptcat
|
||||||
|
if (installUrl.indexOf("scriptcat.org") !== -1) {
|
||||||
|
const id = installUrl.split("/")[5];
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
iconOnly
|
||||||
|
size="small"
|
||||||
|
target="_blank"
|
||||||
|
href={`https://scriptcat.org/script-show-page/${id}`}
|
||||||
|
>
|
||||||
|
<img width={16} height={16} src="/assets/logo.png" alt="" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (installUrl.indexOf("greasyfork.org") !== -1) {
|
||||||
|
const id = installUrl.split("/")[4];
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
iconOnly
|
||||||
|
size="small"
|
||||||
|
target="_blank"
|
||||||
|
href={`https://greasyfork.org/scripts/${id}`}
|
||||||
|
>
|
||||||
|
<img width={16} height={16} src="/assets/logo/gf.png" alt="" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (installUrl.indexOf("raw.githubusercontent.com") !== -1) {
|
||||||
|
const repo = `${installUrl.split("/")[3]}/${installUrl.split("/")[4]}`;
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
iconOnly
|
||||||
|
size="small"
|
||||||
|
target="_blank"
|
||||||
|
href={`https://github.com/${repo}`}
|
||||||
|
style={{
|
||||||
|
color: "var(--color-text-1)",
|
||||||
|
}}
|
||||||
|
icon={<IconGithub />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (installUrl.indexOf("github.com") !== -1) {
|
||||||
|
const repo = `${installUrl.split("/")[3]}/${installUrl.split("/")[4]}`;
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
iconOnly
|
||||||
|
size="small"
|
||||||
|
target="_blank"
|
||||||
|
href={`https://github.com/${repo}`}
|
||||||
|
style={{
|
||||||
|
color: "var(--color-text-1)",
|
||||||
|
}}
|
||||||
|
icon={<IconGithub />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ignore error
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ListHomeRender({ script }: { script: Script }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
let home;
|
||||||
|
if (!script.metadata.homepageurl) {
|
||||||
|
home = installUrlToHome(script.downloadUrl || "");
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Space size="mini">
|
||||||
|
{home && <Tooltip content={t("homepage")}>{home}</Tooltip>}
|
||||||
|
{script.metadata.homepage && (
|
||||||
|
<Tooltip content={t("homepage")}>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
iconOnly
|
||||||
|
icon={<IconHome />}
|
||||||
|
size="small"
|
||||||
|
href={script.metadata.homepage[0]}
|
||||||
|
target="_blank"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{script.metadata.homepageurl && (
|
||||||
|
<Tooltip content={t("homepage")}>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
iconOnly
|
||||||
|
icon={<IconHome />}
|
||||||
|
size="small"
|
||||||
|
href={script.metadata.homepageurl[0]}
|
||||||
|
target="_blank"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{script.metadata.website && (
|
||||||
|
<Tooltip content={t("script_website")}>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
iconOnly
|
||||||
|
icon={<IconHome />}
|
||||||
|
size="small"
|
||||||
|
href={script.metadata.website[0]}
|
||||||
|
target="_blank"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{script.metadata.source && (
|
||||||
|
<Tooltip content={t("script_source")}>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
iconOnly
|
||||||
|
icon={<IconCode />}
|
||||||
|
size="small"
|
||||||
|
href={script.metadata.source[0]}
|
||||||
|
target="_blank"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{script.metadata.supporturl && (
|
||||||
|
<Tooltip content={t("bug_feedback_script_support")}>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
iconOnly
|
||||||
|
icon={<IconBug />}
|
||||||
|
size="small"
|
||||||
|
href={script.metadata.supporturl[0]}
|
||||||
|
target="_blank"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getValues(script: Script) {
|
||||||
|
const { config } = script;
|
||||||
|
return (IoC.instance(ValueManager) as ValueManager)
|
||||||
|
.getValues(script)
|
||||||
|
.then((data) => {
|
||||||
|
const newValues: { [key: string]: any } = {};
|
||||||
|
Object.keys(config!).forEach((tabKey) => {
|
||||||
|
const tab = config![tabKey];
|
||||||
|
Object.keys(tab).forEach((key) => {
|
||||||
|
// 动态变量
|
||||||
|
if (tab[key].bind) {
|
||||||
|
const bindKey = tab[key].bind!.substring(1);
|
||||||
|
newValues[bindKey] =
|
||||||
|
data[bindKey] === undefined ? undefined : data[bindKey].value;
|
||||||
|
}
|
||||||
|
newValues[`${tabKey}.${key}`] =
|
||||||
|
data[`${tabKey}.${key}`] === undefined
|
||||||
|
? config![tabKey][key].default
|
||||||
|
: data[`${tabKey}.${key}`].value;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return newValues;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ScriptIconsProps = {
|
||||||
|
script: { name: string; metadata: Metadata };
|
||||||
|
size?: number;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ScriptIcons({ script, size = 32, style }: ScriptIconsProps) {
|
||||||
|
style = style || {};
|
||||||
|
style.display = style.display || "inline-block";
|
||||||
|
style.marginRight = style.marginRight || "8px";
|
||||||
|
let icon = "";
|
||||||
|
if (script.metadata.icon) {
|
||||||
|
[icon] = script.metadata.icon;
|
||||||
|
} else if (script.metadata.iconurl) {
|
||||||
|
[icon] = script.metadata.iconurl;
|
||||||
|
} else if (script.metadata.icon64) {
|
||||||
|
[icon] = script.metadata.icon64;
|
||||||
|
} else if (script.metadata.icon64url) {
|
||||||
|
[icon] = script.metadata.icon64url;
|
||||||
|
}
|
||||||
|
if (icon) {
|
||||||
|
return (
|
||||||
|
<Avatar size={size || 32} shape="square" style={style}>
|
||||||
|
<img src={icon} alt={script?.name} />
|
||||||
|
</Avatar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||||
|
return <></>;
|
||||||
|
}
|
@ -11,10 +11,10 @@ import {
|
|||||||
ScriptDAO,
|
ScriptDAO,
|
||||||
UserConfig,
|
UserConfig,
|
||||||
} from "@App/app/repo/scripts";
|
} from "@App/app/repo/scripts";
|
||||||
import { InstallSource } from "@App/app/service/service_worker";
|
|
||||||
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 } from "@App/app/repo/subscribe";
|
||||||
import { nextTime } from "./utils";
|
import { nextTime } from "./utils";
|
||||||
|
import { InstallSource } from "@App/app/service/service_worker";
|
||||||
|
|
||||||
export function getMetadataStr(code: string): string | null {
|
export function getMetadataStr(code: string): string | null {
|
||||||
const start = code.indexOf("==UserScript==");
|
const start = code.indexOf("==UserScript==");
|
||||||
@ -95,16 +95,16 @@ export type ScriptInfo = {
|
|||||||
url: string;
|
url: string;
|
||||||
code: string;
|
code: string;
|
||||||
uuid: string;
|
uuid: string;
|
||||||
isSubscribe: boolean;
|
userSubscribe: boolean;
|
||||||
isUpdate: boolean;
|
|
||||||
metadata: Metadata;
|
metadata: Metadata;
|
||||||
|
update: boolean;
|
||||||
source: InstallSource;
|
source: InstallSource;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function fetchScriptInfo(
|
export async function fetchScriptInfo(
|
||||||
url: string,
|
url: string,
|
||||||
source: InstallSource,
|
source: InstallSource,
|
||||||
isUpdate: boolean,
|
update: boolean,
|
||||||
uuid: string
|
uuid: string
|
||||||
): Promise<ScriptInfo> {
|
): Promise<ScriptInfo> {
|
||||||
const resp = await fetch(url, {
|
const resp = await fetch(url, {
|
||||||
@ -127,15 +127,12 @@ export async function fetchScriptInfo(
|
|||||||
const ret: ScriptInfo = {
|
const ret: ScriptInfo = {
|
||||||
url,
|
url,
|
||||||
code: body,
|
code: body,
|
||||||
uuid,
|
|
||||||
isSubscribe: false,
|
|
||||||
isUpdate,
|
|
||||||
metadata: parse,
|
|
||||||
source,
|
source,
|
||||||
|
update,
|
||||||
|
uuid,
|
||||||
|
userSubscribe: parse.usersubscribe !== undefined,
|
||||||
|
metadata: parse,
|
||||||
};
|
};
|
||||||
if (parse.usersubscribe) {
|
|
||||||
ret.isSubscribe = true;
|
|
||||||
}
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -182,3 +182,7 @@ export function openInCurrentTab(url: string) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isDebug() {
|
||||||
|
return process.env.NODE_ENV === "development";
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user