消息通讯
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 {
|
||||
constructor() {}
|
||||
|
||||
// 订阅
|
||||
subscribe(topic: string, handler: (message: any) => void) {
|
||||
const con = chrome.runtime.connect({ name: topic });
|
||||
con.postMessage({ action: "subscribe", topic });
|
||||
async subscribe(topic: string, handler: (message: any) => void) {
|
||||
const con = await connect("messageQueue", { action: "subscribe", topic });
|
||||
con.onMessage.addListener((msg: { action: string; topic: string; message: any }) => {
|
||||
if (msg.action === "message") {
|
||||
handler(msg.message);
|
||||
@ -24,6 +24,10 @@ export class Broker {
|
||||
export class MessageQueue {
|
||||
topicConMap: Map<string, { name: string; con: chrome.runtime.Port }[]> = new Map();
|
||||
|
||||
constructor(api: Server) {
|
||||
api.on("messageQueue", this.handler());
|
||||
}
|
||||
|
||||
handler(): ApiFunction {
|
||||
return ({ action, topic, message }: { action: string; topic: string; message: any }, con) => {
|
||||
if (!con) {
|
||||
@ -53,7 +57,10 @@ export class MessageQueue {
|
||||
}
|
||||
list.push({ name: topic, con });
|
||||
con.onDisconnect.addListener(() => {
|
||||
let list = this.topicConMap.get(topic);
|
||||
// 移除断开连接的con
|
||||
list = list!.filter((item) => item.con !== con);
|
||||
this.topicConMap.set(topic, list);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -5,18 +5,22 @@ export class Server {
|
||||
|
||||
constructor(private env: string) {
|
||||
chrome.runtime.onConnect.addListener((port) => {
|
||||
const handler = (msg: { action: string }) => {
|
||||
const handler = (msg: { action: string; data: any }) => {
|
||||
port.onMessage.removeListener(handler);
|
||||
this.connectHandle(msg.action, msg, port);
|
||||
this.connectHandle(msg.action, msg.data, port);
|
||||
};
|
||||
port.onMessage.addListener(handler);
|
||||
});
|
||||
|
||||
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) {
|
||||
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`,
|
||||
popup: `${src}/pages/popup/main.tsx`,
|
||||
install: `${src}/pages/install/main.tsx`,
|
||||
options: `${src}/pages/options/main.tsx`,
|
||||
},
|
||||
output: {
|
||||
path: `${dist}/ext/src`,
|
||||
@ -130,12 +131,20 @@ export default defineConfig({
|
||||
}),
|
||||
new rspack.HtmlRspackPlugin({
|
||||
filename: `${dist}/ext/src/install.html`,
|
||||
template: `${src}/pages/install/index.html`,
|
||||
template: `${src}/pages/template.html`,
|
||||
inject: "head",
|
||||
title: "Install - ScriptCat",
|
||||
minify: true,
|
||||
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({
|
||||
filename: `${dist}/ext/src/popup.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 {
|
||||
// 缓存触发器
|
||||
static Trigger(): (target: unknown, propertyName: string, descriptor: PropertyDescriptor) => void {
|
||||
return (target, propertyName, descriptor) => {
|
||||
descriptor.value();
|
||||
};
|
||||
}
|
||||
|
||||
// 脚本缓存
|
||||
static script(id: number): string {
|
||||
return `script:${id.toString()}`;
|
||||
}
|
||||
|
||||
// 加载脚本信息时的缓存,已处理删除
|
||||
static scriptInfo(uuid: string): string {
|
||||
// 加载脚本信息时的缓存
|
||||
static scriptInstallInfo(uuid: string): string {
|
||||
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";
|
||||
|
||||
export type LogLevel = "debug" | "info" | "warn" | "error";
|
||||
@ -20,7 +19,7 @@ export default class LoggerCore {
|
||||
return LoggerCore.instance;
|
||||
}
|
||||
|
||||
static getLogger(...label: LogLabel[]) {
|
||||
static logger(...label: LogLabel[]) {
|
||||
return LoggerCore.getInstance().logger(...label);
|
||||
}
|
||||
|
||||
@ -45,6 +44,4 @@ export default class LoggerCore {
|
||||
logger(...label: LogLabel[]) {
|
||||
return new Logger(this, this.labels, ...label);
|
||||
}
|
||||
|
||||
static EE = new EventEmitter();
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
/* eslint-disable no-console */
|
||||
import dayjs from "dayjs";
|
||||
import LoggerCore, { LogLabel, LogLevel } from "./core";
|
||||
|
||||
@ -54,7 +53,6 @@ export default class Logger {
|
||||
break;
|
||||
}
|
||||
}
|
||||
LoggerCore.EE.emit("log", { level, message, label });
|
||||
}
|
||||
|
||||
with(...label: LogLabel[]) {
|
||||
|
@ -32,6 +32,10 @@ function renameField(): void {
|
||||
// export是0.10.x时的兼容性处理
|
||||
export: "++id,&scriptId",
|
||||
});
|
||||
// 将脚本数据迁移到chrome.storage
|
||||
// db.version(18)
|
||||
// .stores({})
|
||||
// .upgrade((tx) => {});
|
||||
}
|
||||
|
||||
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 { 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 ConfigType =
|
||||
| "text"
|
||||
| "checkbox"
|
||||
| "select"
|
||||
| "mult-select"
|
||||
| "number"
|
||||
| "textarea"
|
||||
| "time";
|
||||
export type ConfigType = "text" | "checkbox" | "select" | "mult-select" | "number" | "textarea" | "time";
|
||||
|
||||
export interface Config {
|
||||
[key: string]: any;
|
||||
@ -88,34 +81,40 @@ export interface ScriptRunResouce extends Script {
|
||||
sourceCode: string;
|
||||
}
|
||||
|
||||
export class ScriptDAO extends DAO<Script> {
|
||||
public tableName = "scripts";
|
||||
|
||||
export class ScriptDAO extends Repo<Script> {
|
||||
constructor() {
|
||||
super();
|
||||
this.table = db.table(this.tableName);
|
||||
super("script");
|
||||
}
|
||||
|
||||
public save(val: Script) {
|
||||
return super._save(val.uuid, val);
|
||||
}
|
||||
|
||||
public findByName(name: string) {
|
||||
return this.findOne({ name });
|
||||
return this.findOne((key, value) => {
|
||||
return value.name === name;
|
||||
});
|
||||
}
|
||||
|
||||
public findByNameAndNamespace(name: string, namespace?: string) {
|
||||
if (namespace) {
|
||||
return this.findOne({ name, namespace });
|
||||
}
|
||||
return this.findOne({ name });
|
||||
return this.findOne((key, value) => {
|
||||
return value.name === name && (!namespace || value.namespace === namespace);
|
||||
});
|
||||
}
|
||||
|
||||
public findByUUID(uuid: string) {
|
||||
return this.findOne({ uuid });
|
||||
return this.get(uuid);
|
||||
}
|
||||
|
||||
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) {
|
||||
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[] };
|
||||
|
||||
@ -25,15 +25,12 @@ export interface Subscribe {
|
||||
checktime: number;
|
||||
}
|
||||
|
||||
export class SubscribeDAO extends DAO<Subscribe> {
|
||||
public tableName = "subscribe";
|
||||
|
||||
export class SubscribeDAO extends Repo<Subscribe> {
|
||||
constructor() {
|
||||
super();
|
||||
this.table = db.table(this.tableName);
|
||||
super("subscribe");
|
||||
}
|
||||
|
||||
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 { MessageQueue } from "@Packages/message/message_queue";
|
||||
import { ScriptService } from "./script";
|
||||
|
||||
export type InstallSource = "user" | "system" | "sync" | "subscribe" | "vscode";
|
||||
|
||||
@ -13,128 +8,13 @@ export type InstallSource = "user" | "system" | "sync" | "subscribe" | "vscode";
|
||||
export default class ServiceWorkerManager {
|
||||
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 mq: MessageQueue = new MessageQueue();
|
||||
|
||||
// 获取安装信息
|
||||
getInstallInfo(params: { uuid: string }) {
|
||||
const info = Cache.getInstance().get(CacheKey.scriptInfo(params.uuid));
|
||||
return info;
|
||||
}
|
||||
private mq: MessageQueue = new MessageQueue(this.api);
|
||||
|
||||
initManager() {
|
||||
// 监听消息
|
||||
this.api.on("getInstallInfo", this.getInstallInfo);
|
||||
this.api.on("messageQueue", this.mq.handler());
|
||||
|
||||
this.listenerScriptInstall();
|
||||
const group = this.api.group("serviceWorker");
|
||||
const script = new ScriptService(group.group("script"), this.mq);
|
||||
script.init();
|
||||
}
|
||||
}
|
||||
|
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",
|
||||
"permissions": [
|
||||
"storage",
|
||||
"offscreen",
|
||||
"scripting",
|
||||
"activeTab",
|
||||
|
@ -5,7 +5,9 @@ import { Metadata, Script, SCRIPT_STATUS_DISABLE, SCRIPT_STATUS_ENABLE } from "@
|
||||
import { Subscribe } from "@App/app/repo/subscribe";
|
||||
import { i18nDescription, i18nName } from "@App/locales/locales";
|
||||
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[] }[];
|
||||
|
||||
@ -14,28 +16,74 @@ const closeWindow = () => {
|
||||
};
|
||||
|
||||
function App() {
|
||||
const [permission, setPermission] = useState<Permission>([]);
|
||||
const [metadata, setMetadata] = useState<Metadata>({});
|
||||
// 脚本信息包括脚本代码、下载url,但是不包括解析代码后得到的metadata,通过background的缓存获取
|
||||
const [info, setInfo] = useState<ScriptInfo>();
|
||||
// 对脚本详细的描述
|
||||
const [description, setDescription] = useState<any>();
|
||||
// 脚本信息包括脚本代码、下载url、metadata等信息,通过service_worker的缓存获取
|
||||
const [scriptInfo, setScriptInfo] = useState<ScriptInfo>();
|
||||
// 是系统检测到脚本更新时打开的窗口会有一个倒计时
|
||||
const [countdown, setCountdown] = useState<number>(-1);
|
||||
// 是否为更新
|
||||
const [isUpdate, setIsUpdate] = useState<boolean>(false);
|
||||
// 脚本信息
|
||||
const [upsertScript, setUpsertScript] = useState<Script | Subscribe>();
|
||||
// 更新的情况下会有老版本的脚本信息
|
||||
const [oldScript, setOldScript] = useState<Script | Subscribe>();
|
||||
// 脚本开启状态
|
||||
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 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: {
|
||||
[key: string]: { color: string; title: string; description: string };
|
||||
@ -78,12 +126,46 @@ function App() {
|
||||
if (!uuid) {
|
||||
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 (
|
||||
<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">
|
||||
<Space direction="vertical">
|
||||
<div>
|
||||
@ -94,7 +176,9 @@ function App() {
|
||||
)}
|
||||
<Typography.Text bold className="text-size-lg">
|
||||
{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
|
||||
style={{ marginLeft: "8px" }}
|
||||
checked={enable}
|
||||
@ -131,7 +215,7 @@ function App() {
|
||||
overflowY: "auto",
|
||||
}}
|
||||
>
|
||||
{t("source")}: {info?.url}
|
||||
{t("source")}: {scriptInfo?.url}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className="text-end">
|
||||
@ -144,7 +228,7 @@ function App() {
|
||||
Message.error(t("script_info_load_failed")!);
|
||||
return;
|
||||
}
|
||||
if (isSub) {
|
||||
if (scriptInfo?.userSubscribe) {
|
||||
// subscribeCtrl
|
||||
// .upsert(upsertScript as Subscribe)
|
||||
// .then(() => {
|
||||
@ -159,23 +243,25 @@ function App() {
|
||||
// });
|
||||
return;
|
||||
}
|
||||
// scriptCtrl
|
||||
// .upsert(upsertScript as Script)
|
||||
// .then(() => {
|
||||
// if (isUpdate) {
|
||||
// Message.success(t("install.update_success")!);
|
||||
// setBtnText(t("install.update_success")!);
|
||||
// } else {
|
||||
// Message.success(t("install_success")!);
|
||||
// setBtnText(t("install_success")!);
|
||||
// }
|
||||
// setTimeout(() => {
|
||||
// closeWindow();
|
||||
// }, 200);
|
||||
// })
|
||||
// .catch((e) => {
|
||||
// Message.error(`${t("install_failed")}: ${e}`);
|
||||
// });
|
||||
new ScriptClient()
|
||||
.installScript(upsertScript as Script)
|
||||
.then(() => {
|
||||
if (isUpdate) {
|
||||
Message.success(t("install.update_success")!);
|
||||
setBtnText(t("install.update_success")!);
|
||||
} else {
|
||||
Message.success(t("install_success")!);
|
||||
setBtnText(t("install_success")!);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
!isDebug() &&
|
||||
setTimeout(() => {
|
||||
closeWindow();
|
||||
}, 200);
|
||||
})
|
||||
.catch((e) => {
|
||||
Message.error(`${t("install_failed")}: ${e}`);
|
||||
});
|
||||
}}
|
||||
>
|
||||
{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,
|
||||
UserConfig,
|
||||
} from "@App/app/repo/scripts";
|
||||
import { InstallSource } from "@App/app/service/service_worker";
|
||||
import YAML from "yaml";
|
||||
import { Subscribe, SUBSCRIBE_STATUS_ENABLE, SubscribeDAO } from "@App/app/repo/subscribe";
|
||||
import { nextTime } from "./utils";
|
||||
import { InstallSource } from "@App/app/service/service_worker";
|
||||
|
||||
export function getMetadataStr(code: string): string | null {
|
||||
const start = code.indexOf("==UserScript==");
|
||||
@ -95,16 +95,16 @@ export type ScriptInfo = {
|
||||
url: string;
|
||||
code: string;
|
||||
uuid: string;
|
||||
isSubscribe: boolean;
|
||||
isUpdate: boolean;
|
||||
userSubscribe: boolean;
|
||||
metadata: Metadata;
|
||||
update: boolean;
|
||||
source: InstallSource;
|
||||
};
|
||||
|
||||
export async function fetchScriptInfo(
|
||||
url: string,
|
||||
source: InstallSource,
|
||||
isUpdate: boolean,
|
||||
update: boolean,
|
||||
uuid: string
|
||||
): Promise<ScriptInfo> {
|
||||
const resp = await fetch(url, {
|
||||
@ -127,15 +127,12 @@ export async function fetchScriptInfo(
|
||||
const ret: ScriptInfo = {
|
||||
url,
|
||||
code: body,
|
||||
uuid,
|
||||
isSubscribe: false,
|
||||
isUpdate,
|
||||
metadata: parse,
|
||||
source,
|
||||
update,
|
||||
uuid,
|
||||
userSubscribe: parse.usersubscribe !== undefined,
|
||||
metadata: parse,
|
||||
};
|
||||
if (parse.usersubscribe) {
|
||||
ret.isSubscribe = true;
|
||||
}
|
||||
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