Compare commits
10 Commits
e1a890a400
...
main
Author | SHA1 | Date | |
---|---|---|---|
ec28795dbb | |||
44041f4735 | |||
ffabe268b1 | |||
ddd3219bae | |||
14baa176d9 | |||
3c1e30182f | |||
1aaf1bbd4a | |||
8a216933ca | |||
51fe2a89e1 | |||
a26f1c5014 |
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "scriptcat",
|
||||
"version": "0.17.0-alpha.2",
|
||||
"version": "0.17.0-alpha.4",
|
||||
"description": "脚本猫,一个可以执行用户脚本的浏览器扩展,万物皆可脚本化,让你的浏览器可以做更多的事情!",
|
||||
"author": "CodFrm",
|
||||
"license": "GPLv3",
|
||||
|
@ -208,6 +208,16 @@ export default defineConfig({
|
||||
minimizerOptions: { targets },
|
||||
}),
|
||||
],
|
||||
splitChunks: {
|
||||
chunks: (chunk) => {
|
||||
// 排除这些文件,不进行分离
|
||||
return !["editor.worker", "ts.worker", "linter.worker", "service_worker", "content", "inject"].includes(
|
||||
chunk.name || ""
|
||||
);
|
||||
},
|
||||
minSize: 307200,
|
||||
maxSize: 4194304,
|
||||
},
|
||||
},
|
||||
experiments: {
|
||||
css: true,
|
||||
|
@ -113,7 +113,6 @@ function renameField() {
|
||||
if (subscribe.length) {
|
||||
await Promise.all(
|
||||
subscribe.map((s: Subscribe) => {
|
||||
console.log("1234", s);
|
||||
const { url, name, code, author, scripts, metadata, status, createtime, updatetime, checktime } = s;
|
||||
return subscribeDAO.save({
|
||||
url,
|
||||
|
@ -23,6 +23,7 @@ export default class ContentRuntime {
|
||||
// 转发给inject
|
||||
return sendMessage(this.msg, "inject/runtime/valueUpdate", data);
|
||||
});
|
||||
forwardMessage("serviceWorker", "script/isInstalled", this.server, this.extSend);
|
||||
forwardMessage(
|
||||
"serviceWorker",
|
||||
"runtime/gmApi",
|
||||
|
@ -4,6 +4,8 @@ import ExecScript, { ValueUpdateData } from "./exec_script";
|
||||
import { addStyle, ScriptFunc } from "./utils";
|
||||
import { getStorageName } from "@App/pkg/utils/utils";
|
||||
import { EmitEventRequest } from "../service_worker/runtime";
|
||||
import { ExternalWhitelist } from "@App/app/const";
|
||||
import { sendMessage } from "@Packages/message/client";
|
||||
|
||||
export class InjectRuntime {
|
||||
execList: ExecScript[] = [];
|
||||
@ -44,6 +46,40 @@ export class InjectRuntime {
|
||||
val.valueUpdate(data);
|
||||
});
|
||||
});
|
||||
// 注入允许外部调用
|
||||
this.externalMessage();
|
||||
}
|
||||
|
||||
externalMessage() {
|
||||
// 对外接口白名单
|
||||
let msg = this.msg;
|
||||
for (let i = 0; i < ExternalWhitelist.length; i += 1) {
|
||||
if (window.location.host.endsWith(ExternalWhitelist[i])) {
|
||||
// 注入
|
||||
(<{ external: any }>(<unknown>window)).external = window.external || {};
|
||||
(<
|
||||
{
|
||||
external: {
|
||||
Scriptcat: {
|
||||
isInstalled: (name: string, namespace: string, callback: any) => void;
|
||||
};
|
||||
};
|
||||
}
|
||||
>(<unknown>window)).external.Scriptcat = {
|
||||
async isInstalled(name: string, namespace: string, callback: any) {
|
||||
const resp = await sendMessage(msg, "content/script/isInstalled", {
|
||||
name,
|
||||
namespace,
|
||||
});
|
||||
callback(resp);
|
||||
},
|
||||
};
|
||||
(<{ external: { Tampermonkey: any } }>(<unknown>window)).external.Tampermonkey = (<
|
||||
{ external: { Scriptcat: any } }
|
||||
>(<unknown>window)).external.Scriptcat;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
execScript(script: ScriptRunResouce, scriptFunc: ScriptFunc) {
|
||||
|
@ -9,6 +9,8 @@ import { PopupService } from "./popup";
|
||||
import { SystemConfig } from "@App/pkg/config/config";
|
||||
import { SynchronizeService } from "./synchronize";
|
||||
import { SubscribeService } from "./subscribe";
|
||||
import { ExtServer, ExtVersion } from "@App/app/const";
|
||||
import { systemConfig } from "@App/pages/store/global";
|
||||
|
||||
export type InstallSource = "user" | "system" | "sync" | "subscribe" | "vscode";
|
||||
|
||||
@ -78,8 +80,17 @@ export default class ServiceWorkerManager {
|
||||
case "checkSubscribeUpdate":
|
||||
subscribe.checkSubscribeUpdate();
|
||||
break;
|
||||
case "checkUpdate":
|
||||
// 检查扩展更新
|
||||
this.checkUpdate();
|
||||
break;
|
||||
}
|
||||
});
|
||||
// 8小时检查一次扩展更新
|
||||
chrome.alarms.create("checkUpdate", {
|
||||
delayInMinutes: 0,
|
||||
periodInMinutes: 8 * 60,
|
||||
});
|
||||
|
||||
// 监听配置变化
|
||||
this.mq.subscribe("systemConfigChange", (msg) => {
|
||||
@ -95,5 +106,31 @@ export default class ServiceWorkerManager {
|
||||
systemConfig.getCloudSync().then((config) => {
|
||||
synchronize.cloudSyncConfigChange(config);
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
chrome.runtime.onInstalled.addListener((details) => {
|
||||
if (details.reason === "install") {
|
||||
chrome.tabs.create({ url: "https://docs.scriptcat.org/" });
|
||||
} else if (details.reason === "update") {
|
||||
chrome.tabs.create({
|
||||
url: `https://docs.scriptcat.org/docs/change/#${ExtVersion}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
checkUpdate() {
|
||||
fetch(`${ExtServer}api/v1/system/version?version=${ExtVersion}`)
|
||||
.then((resp) => resp.json())
|
||||
.then((resp: { data: { notice: string; version: string } }) => {
|
||||
systemConfig.getCheckUpdate().then((items) => {
|
||||
if (items.notice !== resp.data.notice) {
|
||||
systemConfig.setCheckUpdate(Object.assign(resp.data, { isRead: false }));
|
||||
} else {
|
||||
systemConfig.setCheckUpdate(Object.assign(resp.data, { isRead: items.isRead }));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -257,7 +257,7 @@ export class ResourceService {
|
||||
return fetch(u.url)
|
||||
.then(async (resp) => {
|
||||
if (resp.status !== 200) {
|
||||
throw new Error(`resource response status not 200:${resp.status}`);
|
||||
throw new Error(`resource response status not 200: ${resp.status}`);
|
||||
}
|
||||
return {
|
||||
data: await resp.blob(),
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { MessageQueue } from "@Packages/message/message_queue";
|
||||
import { MessageQueue, Unsubscribe } from "@Packages/message/message_queue";
|
||||
import { ExtMessageSender, GetSender, Group, MessageSend } from "@Packages/message/server";
|
||||
import {
|
||||
Script,
|
||||
@ -15,18 +15,17 @@ import { subscribeScriptDelete, subscribeScriptEnable, subscribeScriptInstall }
|
||||
import { ScriptService } from "./script";
|
||||
import { runScript, stopScript } from "../offscreen/client";
|
||||
import { getRunAt } from "./utils";
|
||||
import { randomString } from "@App/pkg/utils/utils";
|
||||
import { isUserScriptsAvailable, randomString } from "@App/pkg/utils/utils";
|
||||
import Cache from "@App/app/cache";
|
||||
import { dealPatternMatches, UrlMatch } from "@App/pkg/utils/match";
|
||||
import { ExtensionContentMessageSend } from "@Packages/message/extension_message";
|
||||
import { sendMessage } from "@Packages/message/client";
|
||||
import { compileInjectScript } from "../content/utils";
|
||||
import { PopupService } from "./popup";
|
||||
import Logger from "@App/app/logger/logger";
|
||||
import LoggerCore from "@App/app/logger/core";
|
||||
import PermissionVerify from "./permission_verify";
|
||||
import { SystemConfig } from "@App/pkg/config/config";
|
||||
import { ResourceService } from "./resource";
|
||||
import { LocalStorageDAO } from "@App/app/repo/localStorage";
|
||||
|
||||
// 为了优化性能,存储到缓存时删除了code、value与resource
|
||||
export interface ScriptMatchInfo extends ScriptRunResouce {
|
||||
@ -49,6 +48,9 @@ export class RuntimeService {
|
||||
scriptCustomizeMatch: UrlMatch<string> = new UrlMatch<string>();
|
||||
scriptMatchCache: Map<string, ScriptMatchInfo> | null | undefined;
|
||||
|
||||
isEnableDeveloperMode = false;
|
||||
isEnableUserscribe = true;
|
||||
|
||||
constructor(
|
||||
private systemConfig: SystemConfig,
|
||||
private group: Group,
|
||||
@ -70,8 +72,35 @@ export class RuntimeService {
|
||||
this.group.on("runScript", this.runScript.bind(this));
|
||||
this.group.on("pageLoad", this.pageLoad.bind(this));
|
||||
|
||||
// 读取inject.js注入页面
|
||||
this.registerInjectScript();
|
||||
// 检查是否开启了开发者模式
|
||||
this.isEnableDeveloperMode = isUserScriptsAvailable();
|
||||
if (!this.isEnableDeveloperMode) {
|
||||
// 未开启加上警告引导
|
||||
// 判断是否首次
|
||||
const localStorage = new LocalStorageDAO();
|
||||
localStorage.get("firstShowDeveloperMode").then((res) => {
|
||||
if (!res) {
|
||||
localStorage.save({
|
||||
key: "firstShowDeveloperMode",
|
||||
value: true,
|
||||
});
|
||||
// 打开页面
|
||||
chrome.tabs.create({
|
||||
url: `https://docs.scriptcat.org/docs/use/open-dev/`,
|
||||
});
|
||||
}
|
||||
});
|
||||
chrome.action.setBadgeBackgroundColor({
|
||||
color: "#ff8c00",
|
||||
});
|
||||
chrome.action.setBadgeTextColor({
|
||||
color: "#ffffff",
|
||||
});
|
||||
chrome.action.setBadgeText({
|
||||
text: "!",
|
||||
});
|
||||
}
|
||||
|
||||
// 监听脚本开启
|
||||
subscribeScriptEnable(this.mq, async (data) => {
|
||||
const script = await this.scriptDAO.getAndCode(data.uuid);
|
||||
@ -82,6 +111,7 @@ export class RuntimeService {
|
||||
// 如果是后台脚本, 在offscreen中进行处理
|
||||
if (script.type === SCRIPT_TYPE_NORMAL) {
|
||||
// 加载页面脚本
|
||||
// 不管开没开启都要加载一次脚本信息
|
||||
await this.loadPageScript(script);
|
||||
if (!data.enable) {
|
||||
await this.unregistryPageScript(script.uuid);
|
||||
@ -104,6 +134,32 @@ export class RuntimeService {
|
||||
this.deleteScriptMatch(uuid);
|
||||
});
|
||||
|
||||
this.systemConfig.addListener("enable_script", (enable) => {
|
||||
this.isEnableUserscribe = enable;
|
||||
if (enable) {
|
||||
this.registerUserscripts();
|
||||
} else {
|
||||
this.unregisterUserscripts();
|
||||
}
|
||||
});
|
||||
// 检查是否开启
|
||||
this.isEnableUserscribe = await this.systemConfig.getEnableScript();
|
||||
if (this.isEnableUserscribe) {
|
||||
this.registerUserscripts();
|
||||
}
|
||||
}
|
||||
|
||||
unsubscribe: Unsubscribe[] = [];
|
||||
|
||||
// 取消脚本注册
|
||||
unregisterUserscripts() {
|
||||
chrome.userScripts.unregister();
|
||||
this.deleteMessageFlag();
|
||||
}
|
||||
|
||||
async registerUserscripts() {
|
||||
// 读取inject.js注入页面
|
||||
this.registerInjectScript();
|
||||
// 将开启的脚本发送一次enable消息
|
||||
const scriptDao = new ScriptDAO();
|
||||
const list = await scriptDao.all();
|
||||
@ -132,6 +188,14 @@ export class RuntimeService {
|
||||
});
|
||||
}
|
||||
|
||||
deleteMessageFlag() {
|
||||
return Cache.getInstance().del("scriptInjectMessageFlag");
|
||||
}
|
||||
|
||||
getMessageFlag() {
|
||||
return Cache.getInstance().get("scriptInjectMessageFlag");
|
||||
}
|
||||
|
||||
// 给指定tab发送消息
|
||||
sendMessageToTab(to: ExtMessageSender, action: string, data: any) {
|
||||
if (to.tabId === -1) {
|
||||
@ -205,7 +269,7 @@ export class RuntimeService {
|
||||
return undefined;
|
||||
}
|
||||
// 如果是iframe,判断是否允许在iframe里运行
|
||||
if (chromeSender.frameId !== undefined) {
|
||||
if (chromeSender.frameId) {
|
||||
if (scriptRes.metadata.noframes) {
|
||||
return undefined;
|
||||
}
|
||||
@ -254,40 +318,57 @@ export class RuntimeService {
|
||||
}
|
||||
|
||||
// 注册inject.js
|
||||
registerInjectScript() {
|
||||
chrome.userScripts.getScripts({ ids: ["scriptcat-inject"] }).then((res) => {
|
||||
if (res.length == 0) {
|
||||
chrome.userScripts.configureWorld({
|
||||
csp: "script-src 'self' 'unsafe-inline' 'unsafe-eval' *",
|
||||
messaging: true,
|
||||
async registerInjectScript() {
|
||||
// 如果没设置过, 则更新messageFlag
|
||||
let messageFlag = await this.getMessageFlag();
|
||||
if (!messageFlag) {
|
||||
messageFlag = await this.messageFlag();
|
||||
const injectJs = await fetch("inject.js").then((res) => res.text());
|
||||
// 替换ScriptFlag
|
||||
const code = `(function (MessageFlag) {\n${injectJs}\n})('${messageFlag}')`;
|
||||
chrome.userScripts.configureWorld({
|
||||
csp: "script-src 'self' 'unsafe-inline' 'unsafe-eval' *",
|
||||
messaging: true,
|
||||
});
|
||||
const scripts: chrome.userScripts.RegisteredUserScript[] = [
|
||||
{
|
||||
id: "scriptcat-inject",
|
||||
js: [{ code }],
|
||||
matches: ["<all_urls>"],
|
||||
allFrames: true,
|
||||
world: "MAIN",
|
||||
runAt: "document_start",
|
||||
},
|
||||
// 注册content
|
||||
{
|
||||
id: "scriptcat-content",
|
||||
js: [{ file: "src/content.js" }],
|
||||
matches: ["<all_urls>"],
|
||||
allFrames: true,
|
||||
runAt: "document_start",
|
||||
world: "USER_SCRIPT",
|
||||
},
|
||||
];
|
||||
try {
|
||||
// 如果使用getScripts来判断, 会出现找不到的问题
|
||||
// 另外如果使用
|
||||
await chrome.userScripts.register(scripts);
|
||||
} catch (e: any) {
|
||||
LoggerCore.logger().error("register inject.js error", {
|
||||
error: e,
|
||||
});
|
||||
fetch("inject.js")
|
||||
.then((res) => res.text())
|
||||
.then(async (injectJs) => {
|
||||
// 替换ScriptFlag
|
||||
const code = `(function (MessageFlag) {\n${injectJs}\n})('${await this.messageFlag()}')`;
|
||||
chrome.userScripts.register([
|
||||
{
|
||||
id: "scriptcat-inject",
|
||||
js: [{ code }],
|
||||
matches: ["<all_urls>"],
|
||||
allFrames: true,
|
||||
world: "MAIN",
|
||||
runAt: "document_start",
|
||||
},
|
||||
// 注册content
|
||||
{
|
||||
id: "scriptcat-content",
|
||||
js: [{ file: "src/content.js" }],
|
||||
matches: ["<all_urls>"],
|
||||
allFrames: true,
|
||||
runAt: "document_start",
|
||||
world: "USER_SCRIPT",
|
||||
},
|
||||
]);
|
||||
if (e.message?.indexOf("Duplicate script ID") !== -1) {
|
||||
// 如果是重复注册, 则更新
|
||||
chrome.userScripts.update(scripts, () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
LoggerCore.logger().error("update inject.js error", {
|
||||
error: chrome.runtime.lastError,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
loadingScript: Promise<void> | null | undefined;
|
||||
@ -367,8 +448,11 @@ export class RuntimeService {
|
||||
if (!this.scriptMatchCache) {
|
||||
await this.loadScriptMatchInfo();
|
||||
}
|
||||
this.scriptMatchCache!.get(uuid)!.status = status;
|
||||
this.saveScriptMatchInfo();
|
||||
const script = await this.scriptMatchCache!.get(uuid);
|
||||
if (script) {
|
||||
script.status = status;
|
||||
this.saveScriptMatchInfo();
|
||||
}
|
||||
}
|
||||
|
||||
async deleteScriptMatch(uuid: string) {
|
||||
@ -434,25 +518,36 @@ export class RuntimeService {
|
||||
this.addScriptMatch(scriptMatchInfo);
|
||||
|
||||
// 如果脚本开启, 则注册脚本
|
||||
if (script.status === SCRIPT_STATUS_ENABLE) {
|
||||
if (!scriptRes.metadata["noframes"]) {
|
||||
if (this.isEnableDeveloperMode && this.isEnableUserscribe && script.status === SCRIPT_STATUS_ENABLE) {
|
||||
if (scriptRes.metadata["noframes"]) {
|
||||
registerScript.allFrames = false;
|
||||
} else {
|
||||
registerScript.allFrames = true;
|
||||
}
|
||||
if (scriptRes.metadata["run-at"]) {
|
||||
registerScript.runAt = getRunAt(scriptRes.metadata["run-at"]);
|
||||
}
|
||||
if (await Cache.getInstance().get("registryScript:" + script.uuid)) {
|
||||
await chrome.userScripts.update([registerScript]);
|
||||
const res = await chrome.userScripts.getScripts({ ids: [script.uuid] });
|
||||
const logger = LoggerCore.logger({
|
||||
name: script.name,
|
||||
registerMatch: {
|
||||
matches: registerScript.matches,
|
||||
excludeMatches: registerScript.excludeMatches,
|
||||
},
|
||||
});
|
||||
if (res.length > 0) {
|
||||
await chrome.userScripts.update([registerScript], () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
logger.error("update registerScript error", {
|
||||
error: chrome.runtime.lastError,
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
await chrome.userScripts.register([registerScript], () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
LoggerCore.logger().error("registerScript error", {
|
||||
logger.error("registerScript error", {
|
||||
error: chrome.runtime.lastError,
|
||||
name: script.name,
|
||||
registerMatch: {
|
||||
matches: registerScript.matches,
|
||||
excludeMatches: registerScript.excludeMatches,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -462,19 +557,17 @@ export class RuntimeService {
|
||||
}
|
||||
|
||||
async unregistryPageScript(uuid: string) {
|
||||
if (!(await Cache.getInstance().get("registryScript:" + uuid))) {
|
||||
if (
|
||||
!this.isEnableDeveloperMode ||
|
||||
!this.isEnableUserscribe ||
|
||||
!(await Cache.getInstance().get("registryScript:" + uuid))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
chrome.userScripts.unregister(
|
||||
{
|
||||
ids: [uuid],
|
||||
},
|
||||
() => {
|
||||
// 删除缓存
|
||||
Cache.getInstance().del("registryScript:" + uuid);
|
||||
// 修改脚本状态为disable
|
||||
this.updateScriptStatus(uuid, SCRIPT_STATUS_DISABLE);
|
||||
}
|
||||
);
|
||||
// 删除缓存
|
||||
Cache.getInstance().del("registryScript:" + uuid);
|
||||
// 修改脚本状态为disable
|
||||
this.updateScriptStatus(uuid, SCRIPT_STATUS_DISABLE);
|
||||
chrome.userScripts.unregister({ ids: [uuid] });
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import { ResourceService } from "./resource";
|
||||
import { ValueService } from "./value";
|
||||
import { compileScriptCode } from "../content/utils";
|
||||
import { SystemConfig } from "@App/pkg/config/config";
|
||||
import i18n, { localePath } from "@App/locales/locales";
|
||||
|
||||
export class ScriptService {
|
||||
logger: Logger;
|
||||
@ -59,50 +60,55 @@ export class ScriptService {
|
||||
// 读取脚本url内容, 进行安装
|
||||
const logger = this.logger.with({ url: targetUrl });
|
||||
logger.debug("install script");
|
||||
this.openInstallPageByUrl(targetUrl, "user").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,
|
||||
this.openInstallPageByUrl(targetUrl, "user")
|
||||
.catch((e) => {
|
||||
logger.error("install script error", Logger.E(e));
|
||||
// 不再重定向当前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],
|
||||
},
|
||||
},
|
||||
condition: {
|
||||
regexFilter: targetUrl,
|
||||
resourceTypes: [chrome.declarativeNetRequest.ResourceType.MAIN_FRAME],
|
||||
requestMethods: [chrome.declarativeNetRequest.RequestMethod.GET],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
() => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error(chrome.runtime.lastError);
|
||||
],
|
||||
},
|
||||
() => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error(chrome.runtime.lastError);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
// 回退到到安装页
|
||||
chrome.scripting.executeScript({
|
||||
target: { tabId: req.tabId },
|
||||
func: function () {
|
||||
history.back();
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
{
|
||||
urls: [
|
||||
"https://docs.scriptcat.org/docs/script_installation",
|
||||
"https://docs.scriptcat.org/docs/script_installation/",
|
||||
"https://docs.scriptcat.org/en/docs/script_installation/",
|
||||
"https://www.tampermonkey.net/script_installation.php",
|
||||
],
|
||||
types: ["main_frame"],
|
||||
}
|
||||
);
|
||||
// 获取i18n
|
||||
// 重定向到脚本安装页
|
||||
chrome.declarativeNetRequest.updateDynamicRules(
|
||||
{
|
||||
@ -114,7 +120,7 @@ export class ScriptService {
|
||||
action: {
|
||||
type: chrome.declarativeNetRequest.RuleActionType.REDIRECT,
|
||||
redirect: {
|
||||
regexSubstitution: "https://docs.scriptcat.org/docs/script_installation#url=\\0",
|
||||
regexSubstitution: `https://docs.scriptcat.org${localePath}/docs/script_installation/#url=\\0`,
|
||||
},
|
||||
},
|
||||
condition: {
|
||||
@ -479,6 +485,15 @@ export class ScriptService {
|
||||
return this.checkUpdate(uuid, "user");
|
||||
}
|
||||
|
||||
isInstalled({ name, namespace }: { name: string; namespace: string }) {
|
||||
return this.scriptDAO.findByNameAndNamespace(name, namespace).then((script) => {
|
||||
if (script) {
|
||||
return { installed: true, version: script.metadata.version && script.metadata.version[0] };
|
||||
}
|
||||
return { installed: false };
|
||||
});
|
||||
}
|
||||
|
||||
init() {
|
||||
this.listenerScriptInstall();
|
||||
|
||||
@ -494,6 +509,7 @@ export class ScriptService {
|
||||
this.group.on("resetMatch", this.resetMatch.bind(this));
|
||||
this.group.on("resetExclude", this.resetExclude.bind(this));
|
||||
this.group.on("requestCheckUpdate", this.requestCheckUpdate.bind(this));
|
||||
this.group.on("isInstalled", this.isInstalled.bind(this));
|
||||
|
||||
// 定时检查更新, 每10分钟检查一次
|
||||
chrome.alarms.create("checkScriptUpdate", {
|
||||
|
@ -27,10 +27,15 @@ i18n.use(initReactI18next).init({
|
||||
},
|
||||
});
|
||||
|
||||
export let localePath = "";
|
||||
|
||||
chrome.i18n.getAcceptLanguages((lngs) => {
|
||||
systemConfig.getLanguage().then((lng) => {
|
||||
systemConfig.getLanguage(lngs).then((lng) => {
|
||||
i18n.changeLanguage(lng);
|
||||
dayjs.locale(lng.toLocaleLowerCase());
|
||||
if (lng !== "zh-CN") {
|
||||
localePath = "en";
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -368,5 +368,8 @@
|
||||
"eslint_config_format_error": "eslint配置格式错误",
|
||||
"export_success": "导出成功",
|
||||
"get_backup_dir_url_failed": "获取备份目录地址失败",
|
||||
"get_backup_files_failed": "获取备份文件失败"
|
||||
"get_backup_files_failed": "获取备份文件失败",
|
||||
"develop_mode_guide": "检测到当前未开启开发者模式,您的脚本无法正常使用,<a href=\"https://docs.scriptcat.org/docs/use/open-dev/\" target=\"black\" style=\"color: var(--color-text-1)\">👉点我了解如何开启</a>",
|
||||
"enable_script_failed": "脚本开启失败",
|
||||
"disable_script_failed": "脚本关闭失败"
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "__MSG_scriptcat__",
|
||||
"version": "0.17.0.1003",
|
||||
"version": "0.17.0.1005",
|
||||
"author": "CodFrm",
|
||||
"description": "__MSG_scriptcat_description__",
|
||||
"options_ui": {
|
||||
|
@ -39,11 +39,9 @@ const CodeEditor: React.ForwardRefRenderFunction<{ editor: editor.IStandaloneCod
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("1231", code);
|
||||
if (diffCode === undefined || code === undefined || !div.current) {
|
||||
return () => {};
|
||||
}
|
||||
console.log("1232");
|
||||
let edit: editor.IStandaloneDiffEditor | editor.IStandaloneCodeEditor;
|
||||
const inlineDiv = document.getElementById(id) as HTMLDivElement;
|
||||
// @ts-ignore
|
||||
|
@ -81,6 +81,8 @@ import {
|
||||
requestStopScript,
|
||||
requestRunScript,
|
||||
scriptClient,
|
||||
enableLoading,
|
||||
updateEnableStatus,
|
||||
} from "@App/pages/store/features/script";
|
||||
import { message, systemConfig } from "@App/pages/store/global";
|
||||
import { SynchronizeClient, ValueClient } from "@App/app/service/service_worker/client";
|
||||
@ -615,6 +617,7 @@ function ScriptList() {
|
||||
const dealColumns: ColumnProps[] = [];
|
||||
|
||||
newColumns.forEach((item) => {
|
||||
console.log(newColumns);
|
||||
switch (item.width) {
|
||||
case -1:
|
||||
break;
|
||||
@ -625,37 +628,39 @@ function ScriptList() {
|
||||
});
|
||||
|
||||
const sortIndex = dealColumns.findIndex((item) => item.key === "sort");
|
||||
let SortableItem;
|
||||
if (sortIndex !== -1) {
|
||||
SortableItem = (props: any) => {
|
||||
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: props!.record.uuid });
|
||||
|
||||
const SortableItem = (props: any) => {
|
||||
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: props!.record.uuid });
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
};
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
// 替换排序列,使其可以拖拽
|
||||
props.children[sortIndex + 1] = (
|
||||
<td
|
||||
className="arco-table-td"
|
||||
style={{
|
||||
textAlign: "center",
|
||||
}}
|
||||
key="drag"
|
||||
>
|
||||
<div className="arco-table-cell">
|
||||
<IconMenu
|
||||
style={{
|
||||
cursor: "move",
|
||||
}}
|
||||
{...listeners}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
|
||||
return <tr ref={setNodeRef} style={style} {...attributes} {...props} />;
|
||||
};
|
||||
|
||||
// 替换排序列,使其可以拖拽
|
||||
props.children[sortIndex + 1] = (
|
||||
<td
|
||||
className="arco-table-td"
|
||||
style={{
|
||||
textAlign: "center",
|
||||
}}
|
||||
key="drag"
|
||||
>
|
||||
<div className="arco-table-cell">
|
||||
<IconMenu
|
||||
style={{
|
||||
cursor: "move",
|
||||
}}
|
||||
{...listeners}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
|
||||
return <tr ref={setNodeRef} style={style} {...attributes} {...props} />;
|
||||
};
|
||||
}
|
||||
|
||||
const components: ComponentsProps = {
|
||||
table: React.forwardRef(SortableWrapper),
|
||||
@ -703,19 +708,23 @@ function ScriptList() {
|
||||
type="primary"
|
||||
size="mini"
|
||||
onClick={() => {
|
||||
const uuids: string[] = [];
|
||||
const enableAction = (enable: boolean) => {
|
||||
const uuids = select.map((item) => item.uuid);
|
||||
dispatch(enableLoading({ uuids: uuids, loading: true }));
|
||||
Promise.allSettled(uuids.map((uuid) => scriptClient.enable(uuid, enable))).finally(() => {
|
||||
dispatch(updateEnableStatus({ uuids: uuids, enable: enable }));
|
||||
dispatch(enableLoading({ uuids: uuids, loading: false }));
|
||||
});
|
||||
};
|
||||
switch (action) {
|
||||
case "enable":
|
||||
select.forEach((item) => {
|
||||
dispatch(requestEnableScript({ uuid: item.uuid, enable: true }));
|
||||
});
|
||||
enableAction(true);
|
||||
break;
|
||||
case "disable":
|
||||
select.forEach((item) => {
|
||||
dispatch(requestEnableScript({ uuid: item.uuid, enable: false }));
|
||||
});
|
||||
enableAction(false);
|
||||
break;
|
||||
case "export":
|
||||
const uuids: string[] = [];
|
||||
select.forEach((item) => {
|
||||
uuids.push(item.uuid);
|
||||
});
|
||||
|
@ -30,7 +30,7 @@ function Tools() {
|
||||
useEffect(() => {
|
||||
// 获取配置
|
||||
const loadConfig = async () => {
|
||||
const [backup, vscodeUrl] = await Promise.all([
|
||||
const [backup, vscodeUrl, vscodeReconnect] = await Promise.all([
|
||||
systemConfig.getBackup(),
|
||||
systemConfig.getVscodeUrl(),
|
||||
systemConfig.getVscodeReconnect(),
|
||||
@ -38,7 +38,7 @@ function Tools() {
|
||||
setFilesystemType(backup.filesystem);
|
||||
setFilesystemParam(backup.params[backup.filesystem] || {});
|
||||
setVscodeUrl(vscodeUrl);
|
||||
setVscodeReconnect(systemConfig.vscodeReconnect);
|
||||
setVscodeReconnect(vscodeReconnect);
|
||||
};
|
||||
loadConfig();
|
||||
}, []);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Script, ScriptAndCode, ScriptCodeDAO, ScriptDAO } from "@App/app/repo/scripts";
|
||||
import { Script, SCRIPT_TYPE_NORMAL, ScriptAndCode, ScriptCodeDAO, ScriptDAO } from "@App/app/repo/scripts";
|
||||
import CodeEditor from "@App/pages/components/CodeEditor";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
||||
@ -16,7 +16,7 @@ import { prepareScriptByCode } from "@App/pkg/utils/script";
|
||||
import ScriptStorage from "@App/pages/components/ScriptStorage";
|
||||
import ScriptResource from "@App/pages/components/ScriptResource";
|
||||
import ScriptSetting from "@App/pages/components/ScriptSetting";
|
||||
import { scriptClient } from "@App/pages/store/features/script";
|
||||
import { runtimeClient, scriptClient } from "@App/pages/store/features/script";
|
||||
import { i18nName } from "@App/locales/locales";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@ -188,57 +188,58 @@ function ScriptEditor() {
|
||||
|
||||
const save = (script: Script, e: editor.IStandaloneCodeEditor): Promise<Script> => {
|
||||
// 解析code生成新的script并更新
|
||||
return new Promise(() => {
|
||||
prepareScriptByCode(e.getValue(), script.origin || "", script.uuid)
|
||||
.then((prepareScript) => {
|
||||
const newScript = prepareScript.script;
|
||||
if (!newScript.name) {
|
||||
Message.warning(t("script_name_cannot_be_set_to_empty"));
|
||||
return;
|
||||
}
|
||||
scriptClient.install(newScript, e.getValue()).then(
|
||||
(update) => {
|
||||
if (!update) {
|
||||
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 = e.getValue();
|
||||
prev[i].isChanged = false;
|
||||
prev[i].script.name = newScript.name;
|
||||
break;
|
||||
return prepareScriptByCode(e.getValue(), script.origin || "", script.uuid)
|
||||
.then((prepareScript) => {
|
||||
const newScript = prepareScript.script;
|
||||
if (!newScript.name) {
|
||||
Message.warning(t("script_name_cannot_be_set_to_empty"));
|
||||
return Promise.reject(new Error("script name cannot be empty"));
|
||||
}
|
||||
return scriptClient
|
||||
.install(newScript, e.getValue())
|
||||
.then((update): Script => {
|
||||
if (!update) {
|
||||
Message.success("新建成功,请注意后台脚本不会默认开启");
|
||||
// 保存的时候如何左侧没有脚本即新建
|
||||
setScriptList((prev) => {
|
||||
setSelectSciptButtonAndTab(newScript.uuid);
|
||||
return [newScript, ...prev];
|
||||
});
|
||||
} else {
|
||||
setScriptList((prev) => {
|
||||
prev.map((script: Script) => {
|
||||
if (script.uuid === newScript.uuid) {
|
||||
script.name = newScript.name;
|
||||
}
|
||||
}
|
||||
});
|
||||
return [...prev];
|
||||
});
|
||||
},
|
||||
(err: any) => {
|
||||
Message.error(`保存失败: ${err}`);
|
||||
Message.success("保存成功");
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
Message.error(`错误的脚本代码: ${err}`);
|
||||
});
|
||||
});
|
||||
setEditors((prev) => {
|
||||
for (let i = 0; i < prev.length; i += 1) {
|
||||
if (prev[i].script.uuid === newScript.uuid) {
|
||||
prev[i].code = e.getValue();
|
||||
prev[i].isChanged = false;
|
||||
prev[i].script.name = newScript.name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return [...prev];
|
||||
});
|
||||
return newScript;
|
||||
})
|
||||
.catch((err: any) => {
|
||||
Message.error(`保存失败: ${err}`);
|
||||
return Promise.reject(err);
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
Message.error(`错误的脚本代码: ${err}`);
|
||||
return Promise.reject(err);
|
||||
});
|
||||
};
|
||||
|
||||
const saveAs = (script: Script, e: editor.IStandaloneCodeEditor) => {
|
||||
return new Promise<void>((resolve) => {
|
||||
chrome.downloads.download(
|
||||
@ -289,30 +290,35 @@ function ScriptEditor() {
|
||||
title: t("run"),
|
||||
items: [
|
||||
{
|
||||
id: "debug",
|
||||
title: t("debug"),
|
||||
id: "run",
|
||||
title: t("run"),
|
||||
hotKey: KeyMod.CtrlCmd | KeyCode.F5,
|
||||
hotKeyString: "Ctrl+F5",
|
||||
tooltip: "只有后台脚本/定时脚本才能调试, 且调试模式下不对进行权限校验(例如@connect)",
|
||||
tooltip: "只有后台脚本/定时脚本才能运行",
|
||||
action: async (script, e) => {
|
||||
// 保存更新代码之后再调试
|
||||
const newScript = await save(script, e);
|
||||
// 判断脚本类型
|
||||
if (newScript.type === SCRIPT_TYPE_NORMAL) {
|
||||
Message.error("只有后台脚本/定时脚本才能运行");
|
||||
return;
|
||||
}
|
||||
Message.loading({
|
||||
id: "debug_script",
|
||||
content: "正在准备脚本资源...",
|
||||
duration: 3000,
|
||||
});
|
||||
runtimeCtrl
|
||||
.debugScript(newScript)
|
||||
runtimeClient
|
||||
.runScript(newScript.uuid)
|
||||
.then(() => {
|
||||
Message.success({
|
||||
id: "debug_script",
|
||||
content: "构建成功, 可以打开开发者工具在控制台中查看输出",
|
||||
content: "构建成功, 可以在扩展页打开开发者工具在控制台中查看输出",
|
||||
duration: 3000,
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
LoggerCore.logger(Logger.E(err)).debug("debug script error");
|
||||
LoggerCore.logger(Logger.E(err)).debug("run script error");
|
||||
Message.error({
|
||||
id: "debug_script",
|
||||
content: `构建失败: ${err}`,
|
||||
|
@ -11,12 +11,14 @@ import {
|
||||
IconSearch,
|
||||
} from "@arco-design/web-react/icon";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { RiMessage2Line } from "react-icons/ri";
|
||||
import { RiMessage2Line, RiZzzFill } from "react-icons/ri";
|
||||
import semver from "semver";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ScriptMenuList from "../components/ScriptMenuList";
|
||||
import { popupClient } from "../store/features/script";
|
||||
import { ScriptMenu } from "@App/app/service/service_worker/popup";
|
||||
import { systemConfig } from "../store/global";
|
||||
import { isUserScriptsAvailable } from "@App/pkg/utils/utils";
|
||||
|
||||
const CollapseItem = Collapse.Item;
|
||||
|
||||
@ -30,11 +32,13 @@ function App() {
|
||||
const [scriptList, setScriptList] = useState<ScriptMenu[]>([]);
|
||||
const [backScriptList, setBackScriptList] = useState<ScriptMenu[]>([]);
|
||||
const [showAlert, setShowAlert] = useState(false);
|
||||
const [notice, setNotice] = useState("");
|
||||
const [isRead, setIsRead] = useState(true);
|
||||
const [version, setVersion] = useState(ExtVersion);
|
||||
const [checkUpdate, setCheckUpdate] = useState<Parameters<typeof systemConfig.setCheckUpdate>[0]>({
|
||||
version: ExtVersion,
|
||||
notice: "",
|
||||
isRead: false,
|
||||
});
|
||||
const [currentUrl, setCurrentUrl] = useState("");
|
||||
const [isEnableScript, setIsEnableScript] = useState(localStorage.enable_script !== "false");
|
||||
const [isEnableScript, setIsEnableScript] = useState(true);
|
||||
const { t } = useTranslation();
|
||||
|
||||
let url: URL | undefined;
|
||||
@ -45,22 +49,21 @@ function App() {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// systemManage.getNotice().then((res) => {
|
||||
// if (res) {
|
||||
// setNotice(res.notice);
|
||||
// setIsRead(res.isRead);
|
||||
// }
|
||||
// });
|
||||
// systemManage.getVersion().then((res) => {
|
||||
// res && setVersion(res);
|
||||
// });
|
||||
const loadConfig = async () => {
|
||||
const [isEnableScript, checkUpdate] = await Promise.all([
|
||||
systemConfig.getEnableScript(),
|
||||
systemConfig.getCheckUpdate(),
|
||||
]);
|
||||
setIsEnableScript(isEnableScript);
|
||||
setCheckUpdate(checkUpdate);
|
||||
};
|
||||
loadConfig();
|
||||
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
||||
if (!tabs.length) {
|
||||
return;
|
||||
}
|
||||
setCurrentUrl(tabs[0].url || "");
|
||||
popupClient.getPopupData({ url: tabs[0].url!, tabId: tabs[0].id! }).then((resp) => {
|
||||
console.log(resp);
|
||||
// 按照开启状态和更新时间排序
|
||||
const list = resp.scriptList;
|
||||
list.sort((a, b) => {
|
||||
@ -82,141 +85,147 @@ function App() {
|
||||
});
|
||||
}, []);
|
||||
return (
|
||||
<Card
|
||||
size="small"
|
||||
title={
|
||||
<div className="flex justify-between">
|
||||
<span className="text-xl">ScriptCat</span>
|
||||
<div className="flex flex-row items-center">
|
||||
<Switch
|
||||
size="small"
|
||||
checked={isEnableScript}
|
||||
onChange={(val) => {
|
||||
setIsEnableScript(val);
|
||||
if (val) {
|
||||
localStorage.enable_script = "true";
|
||||
} else {
|
||||
localStorage.enable_script = "false";
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<IconHome />}
|
||||
iconOnly
|
||||
onClick={() => {
|
||||
// 用a链接的方式,vivaldi竟然会直接崩溃
|
||||
window.open("/src/options.html", "_blank");
|
||||
}}
|
||||
/>
|
||||
<Badge count={isRead ? 0 : 1} dot offset={[-8, 6]}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<IconNotification />}
|
||||
iconOnly
|
||||
onClick={() => {
|
||||
setShowAlert(!showAlert);
|
||||
setIsRead(true);
|
||||
systemManage.setRead(true);
|
||||
<>
|
||||
{!isUserScriptsAvailable() && (
|
||||
<Alert type="warning" content={<div dangerouslySetInnerHTML={{ __html: t("develop_mode_guide") }} />} />
|
||||
)}
|
||||
<Card
|
||||
size="small"
|
||||
title={
|
||||
<div className="flex justify-between">
|
||||
<span className="text-xl">ScriptCat</span>
|
||||
<div className="flex flex-row items-center">
|
||||
<Switch
|
||||
size="small"
|
||||
checked={isEnableScript}
|
||||
onChange={(val) => {
|
||||
setIsEnableScript(val);
|
||||
if (val) {
|
||||
systemConfig.setEnableScript(true);
|
||||
} else {
|
||||
systemConfig.setEnableScript(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Badge>
|
||||
<Dropdown
|
||||
droplist={
|
||||
<Menu
|
||||
style={{
|
||||
maxHeight: "none",
|
||||
<Button
|
||||
type="text"
|
||||
icon={<IconHome />}
|
||||
iconOnly
|
||||
onClick={() => {
|
||||
// 用a链接的方式,vivaldi竟然会直接崩溃
|
||||
window.open("/src/options.html", "_blank");
|
||||
}}
|
||||
/>
|
||||
<Badge count={checkUpdate.isRead ? 0 : 1} dot offset={[-8, 6]}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<IconNotification />}
|
||||
iconOnly
|
||||
onClick={() => {
|
||||
setShowAlert(!showAlert);
|
||||
checkUpdate.isRead = true;
|
||||
setCheckUpdate(checkUpdate);
|
||||
systemConfig.setCheckUpdate(checkUpdate);
|
||||
}}
|
||||
onClickMenuItem={async (key) => {
|
||||
switch (key) {
|
||||
case "newScript":
|
||||
await chrome.storage.local.set({
|
||||
activeTabUrl: {
|
||||
url: currentUrl,
|
||||
},
|
||||
});
|
||||
window.open("/src/options.html#/script/editor?target=initial", "_blank");
|
||||
break;
|
||||
default:
|
||||
window.open(key, "_blank");
|
||||
break;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Menu.Item key="newScript">
|
||||
<IconPlus style={iconStyle} />
|
||||
{t("create_script")}
|
||||
</Menu.Item>
|
||||
<Menu.Item key={`https://scriptcat.org/search?domain=${url && url.host}`}>
|
||||
<IconSearch style={iconStyle} />
|
||||
{t("get_script")}
|
||||
</Menu.Item>
|
||||
<Menu.Item key="https://github.com/scriptscat/scriptcat/issues">
|
||||
<IconBug style={iconStyle} />
|
||||
{t("report_issue")}
|
||||
</Menu.Item>
|
||||
<Menu.Item key="https://docs.scriptcat.org/">
|
||||
<IconBook style={iconStyle} />
|
||||
{t("project_docs")}
|
||||
</Menu.Item>
|
||||
<Menu.Item key="https://bbs.tampermonkey.net.cn/">
|
||||
<RiMessage2Line style={iconStyle} />
|
||||
{t("community")}
|
||||
</Menu.Item>
|
||||
<Menu.Item key="https://github.com/scriptscat/scriptcat">
|
||||
<IconGithub style={iconStyle} />
|
||||
GitHub
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
}
|
||||
trigger="click"
|
||||
>
|
||||
<Button type="text" icon={<IconMoreVertical />} iconOnly />
|
||||
</Dropdown>
|
||||
/>
|
||||
</Badge>
|
||||
<Dropdown
|
||||
droplist={
|
||||
<Menu
|
||||
style={{
|
||||
maxHeight: "none",
|
||||
}}
|
||||
onClickMenuItem={async (key) => {
|
||||
switch (key) {
|
||||
case "newScript":
|
||||
await chrome.storage.local.set({
|
||||
activeTabUrl: {
|
||||
url: currentUrl,
|
||||
},
|
||||
});
|
||||
window.open("/src/options.html#/script/editor?target=initial", "_blank");
|
||||
break;
|
||||
default:
|
||||
window.open(key, "_blank");
|
||||
break;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Menu.Item key="newScript">
|
||||
<IconPlus style={iconStyle} />
|
||||
{t("create_script")}
|
||||
</Menu.Item>
|
||||
<Menu.Item key={`https://scriptcat.org/search?domain=${url && url.host}`}>
|
||||
<IconSearch style={iconStyle} />
|
||||
{t("get_script")}
|
||||
</Menu.Item>
|
||||
<Menu.Item key="https://github.com/scriptscat/scriptcat/issues">
|
||||
<IconBug style={iconStyle} />
|
||||
{t("report_issue")}
|
||||
</Menu.Item>
|
||||
<Menu.Item key="https://docs.scriptcat.org/">
|
||||
<IconBook style={iconStyle} />
|
||||
{t("project_docs")}
|
||||
</Menu.Item>
|
||||
<Menu.Item key="https://bbs.tampermonkey.net.cn/">
|
||||
<RiMessage2Line style={iconStyle} />
|
||||
{t("community")}
|
||||
</Menu.Item>
|
||||
<Menu.Item key="https://github.com/scriptscat/scriptcat">
|
||||
<IconGithub style={iconStyle} />
|
||||
GitHub
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
}
|
||||
trigger="click"
|
||||
>
|
||||
<Button type="text" icon={<IconMoreVertical />} iconOnly />
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
bodyStyle={{ padding: 0 }}
|
||||
>
|
||||
<Alert
|
||||
style={{ marginBottom: 20, display: showAlert ? "flex" : "none" }}
|
||||
type="info"
|
||||
content={<div dangerouslySetInnerHTML={{ __html: notice }} />}
|
||||
/>
|
||||
<Collapse bordered={false} defaultActiveKey={["script", "background"]} style={{ maxWidth: 640 }}>
|
||||
<CollapseItem
|
||||
header={t("current_page_scripts")}
|
||||
name="script"
|
||||
style={{ padding: "0" }}
|
||||
contentStyle={{ padding: "0" }}
|
||||
>
|
||||
<ScriptMenuList script={scriptList} isBackscript={false} currentUrl={currentUrl} />
|
||||
</CollapseItem>
|
||||
|
||||
<CollapseItem
|
||||
header={t("enabled_background_scripts")}
|
||||
name="background"
|
||||
style={{ padding: "0" }}
|
||||
contentStyle={{ padding: "0" }}
|
||||
>
|
||||
<ScriptMenuList script={backScriptList} isBackscript currentUrl={currentUrl} />
|
||||
</CollapseItem>
|
||||
</Collapse>
|
||||
<div className="flex flex-row arco-card-header !h-6">
|
||||
<span className="text-[12px] font-500">{`v${ExtVersion}`}</span>
|
||||
{semver.lt(ExtVersion, version) && (
|
||||
<span
|
||||
onClick={() => {
|
||||
window.open(`https://github.com/scriptscat/scriptcat/releases/tag/v${version}`);
|
||||
}}
|
||||
className="text-1 font-500"
|
||||
style={{ cursor: "pointer" }}
|
||||
}
|
||||
bodyStyle={{ padding: 0 }}
|
||||
>
|
||||
<Alert
|
||||
style={{ display: showAlert ? "flex" : "none" }}
|
||||
type="info"
|
||||
content={<div dangerouslySetInnerHTML={{ __html: checkUpdate.notice || "" }} />}
|
||||
/>
|
||||
<Collapse bordered={false} defaultActiveKey={["script", "background"]} style={{ maxWidth: 640 }}>
|
||||
<CollapseItem
|
||||
header={t("current_page_scripts")}
|
||||
name="script"
|
||||
style={{ padding: "0" }}
|
||||
contentStyle={{ padding: "0" }}
|
||||
>
|
||||
{t("popup.new_version_available")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
<ScriptMenuList script={scriptList} isBackscript={false} currentUrl={currentUrl} />
|
||||
</CollapseItem>
|
||||
|
||||
<CollapseItem
|
||||
header={t("enabled_background_scripts")}
|
||||
name="background"
|
||||
style={{ padding: "0" }}
|
||||
contentStyle={{ padding: "0" }}
|
||||
>
|
||||
<ScriptMenuList script={backScriptList} isBackscript currentUrl={currentUrl} />
|
||||
</CollapseItem>
|
||||
</Collapse>
|
||||
<div className="flex flex-row arco-card-header !h-6">
|
||||
<span className="text-[12px] font-500">{`v${ExtVersion}`}</span>
|
||||
{semver.lt(ExtVersion, checkUpdate.version) && (
|
||||
<span
|
||||
onClick={() => {
|
||||
window.open(`https://github.com/scriptscat/scriptcat/releases/tag/v${checkUpdate.version}`);
|
||||
}}
|
||||
className="text-1 font-500"
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
{t("popup.new_version_available")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -102,6 +102,22 @@ export const scriptSlice = createAppSlice({
|
||||
script.runStatus = action.payload.runStatus;
|
||||
}
|
||||
},
|
||||
updateEnableStatus: (state, action: PayloadAction<{ uuids: string[]; enable: boolean }>) => {
|
||||
state.scripts = state.scripts.map((s) => {
|
||||
if (action.payload.uuids.includes(s.uuid)) {
|
||||
s.status = action.payload.enable ? SCRIPT_STATUS_ENABLE : SCRIPT_STATUS_DISABLE;
|
||||
}
|
||||
return s;
|
||||
});
|
||||
},
|
||||
enableLoading(state, action: PayloadAction<{ uuids: string[]; loading: boolean }>) {
|
||||
state.scripts = state.scripts.map((s) => {
|
||||
if (action.payload.uuids.includes(s.uuid)) {
|
||||
s.enableLoading = action.payload.loading;
|
||||
}
|
||||
return s;
|
||||
});
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
@ -144,6 +160,6 @@ export const scriptSlice = createAppSlice({
|
||||
},
|
||||
});
|
||||
|
||||
export const { sortScript, upsertScript, deleteScript } = scriptSlice.actions;
|
||||
export const { sortScript, upsertScript, deleteScript, enableLoading, updateEnableStatus } = scriptSlice.actions;
|
||||
|
||||
export const { selectScripts } = scriptSlice.selectors;
|
||||
|
@ -5,6 +5,7 @@ import { FileSystemType } from "@Packages/filesystem/factory";
|
||||
import { MessageQueue } from "@Packages/message/message_queue";
|
||||
import i18n from "@App/locales/locales";
|
||||
import dayjs from "dayjs";
|
||||
import { ExtVersion } from "@App/app/const";
|
||||
|
||||
export const SystamConfigChange = "systemConfigChange";
|
||||
|
||||
@ -34,8 +35,11 @@ export class SystemConfig {
|
||||
}
|
||||
|
||||
addListener(key: string, callback: (value: any) => void) {
|
||||
this.mq.subscribe(key, (msg) => {
|
||||
const { value } = msg;
|
||||
this.mq.subscribe(SystamConfigChange, (data: { key: string; value: string }) => {
|
||||
if (data.key !== key) {
|
||||
return;
|
||||
}
|
||||
const { value } = data;
|
||||
callback(value);
|
||||
});
|
||||
}
|
||||
@ -65,9 +69,7 @@ export class SystemConfig {
|
||||
|
||||
public set(key: string, val: any) {
|
||||
this.cache.set(key, val);
|
||||
this.storage.set(key, val).then(() => {
|
||||
console.log(chrome.runtime.lastError, val);
|
||||
});
|
||||
this.storage.set(key, val);
|
||||
// 发送消息通知更新
|
||||
this.mq.publish(SystamConfigChange, {
|
||||
key,
|
||||
@ -226,18 +228,19 @@ export class SystemConfig {
|
||||
this.set("menu_expand_num", val);
|
||||
}
|
||||
|
||||
async getLanguage() {
|
||||
const defaultLanguage = await new Promise<string>((resolve) => {
|
||||
chrome.i18n.getAcceptLanguages((lngs) => {
|
||||
// 遍历数组寻找匹配语言
|
||||
for (let i = 0; i < lngs.length; i += 1) {
|
||||
const lng = lngs[i];
|
||||
if (i18n.hasResourceBundle(lng, "translation")) {
|
||||
resolve(lng);
|
||||
break;
|
||||
}
|
||||
async getLanguage(acceptLanguages?: string[]): Promise<string> {
|
||||
const defaultLanguage = await new Promise<string>(async (resolve) => {
|
||||
if (!acceptLanguages) {
|
||||
acceptLanguages = await chrome.i18n.getAcceptLanguages();
|
||||
}
|
||||
// 遍历数组寻找匹配语言
|
||||
for (let i = 0; i < acceptLanguages.length; i += 1) {
|
||||
const lng = acceptLanguages[i];
|
||||
if (i18n.hasResourceBundle(lng, "translation")) {
|
||||
resolve(lng);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return this.get("language", defaultLanguage || chrome.i18n.getUILanguage());
|
||||
}
|
||||
@ -247,4 +250,28 @@ export class SystemConfig {
|
||||
i18n.changeLanguage(value);
|
||||
dayjs.locale(value.toLocaleLowerCase());
|
||||
}
|
||||
|
||||
setCheckUpdate(data: { notice: string; version: string; isRead: boolean }) {
|
||||
this.set("check_update", {
|
||||
notice: data.notice,
|
||||
version: data.version,
|
||||
isRead: data.isRead,
|
||||
});
|
||||
}
|
||||
|
||||
getCheckUpdate(): Promise<Parameters<typeof this.setCheckUpdate>[0]> {
|
||||
return this.get("check_update", {
|
||||
notice: "",
|
||||
isRead: false,
|
||||
version: ExtVersion,
|
||||
});
|
||||
}
|
||||
|
||||
setEnableScript(enable: boolean) {
|
||||
this.set("enable_script", enable);
|
||||
}
|
||||
|
||||
getEnableScript(): Promise<boolean> {
|
||||
return this.get("enable_script", true);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { dealPatternMatches, parsePatternMatchesURL, UrlMatch } from "./match";
|
||||
import path from "path";
|
||||
|
||||
// https://developer.chrome.com/docs/extensions/mv3/match_patterns/
|
||||
describe("UrlMatch-google", () => {
|
||||
@ -39,16 +40,10 @@ describe("UrlMatch-google", () => {
|
||||
describe("UrlMatch-google-error", () => {
|
||||
const url = new UrlMatch<string>();
|
||||
it("error-1", () => {
|
||||
expect(() => {
|
||||
url.add("https://*foo/bar", "ok1");
|
||||
}).toThrow(Error);
|
||||
});
|
||||
// 从v0.17.0开始允许这种
|
||||
it("error-2", () => {
|
||||
url.add("https://foo.*.bar/baz", "ok1");
|
||||
expect(url.match("https://foo.api.bar/baz")).toEqual(["ok1"]);
|
||||
});
|
||||
it("error-3", () => {
|
||||
it("error-2", () => {
|
||||
expect(() => {
|
||||
url.add("http:/bar", "ok1");
|
||||
}).toThrow(Error);
|
||||
@ -77,6 +72,13 @@ describe("UrlMatch-search", () => {
|
||||
expect(url.match("http://api.bar.example.com/")).toEqual(["ok1"]);
|
||||
expect(url.match("http://api.example.com/")).toEqual([]);
|
||||
});
|
||||
it("*://example*/*/example.path*", () => {
|
||||
const url = new UrlMatch<string>();
|
||||
url.add("*://example*/*/example.path*", "ok1");
|
||||
expect(url.match("https://example.com/foo/example.path")).toEqual(["ok1"]);
|
||||
expect(url.match("https://example.com/foo/bar/example.path")).toEqual(["ok1"]);
|
||||
expect(url.match("https://example.com/foo/bar/example.path2")).toEqual(["ok1"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("UrlMatch-port1", () => {
|
||||
@ -177,6 +179,12 @@ describe("parsePatternMatchesURL", () => {
|
||||
host: "127.0.0.1",
|
||||
path: "",
|
||||
});
|
||||
const matches4 = parsePatternMatchesURL("*://*/*");
|
||||
expect(matches4).toEqual({
|
||||
scheme: "*",
|
||||
host: "*",
|
||||
path: "*",
|
||||
});
|
||||
});
|
||||
it("search", () => {
|
||||
// 会忽略掉search部分
|
||||
@ -191,7 +199,7 @@ describe("parsePatternMatchesURL", () => {
|
||||
const matches = parsePatternMatchesURL("*://www.example.com*");
|
||||
expect(matches).toEqual({
|
||||
scheme: "*",
|
||||
host: "www.example.com",
|
||||
host: "*",
|
||||
path: "*",
|
||||
});
|
||||
});
|
||||
@ -203,4 +211,24 @@ describe("parsePatternMatchesURL", () => {
|
||||
path: "*",
|
||||
});
|
||||
});
|
||||
it("一些怪异的情况", () => {
|
||||
let matches = parsePatternMatchesURL("*://*./*");
|
||||
expect(matches).toEqual({
|
||||
scheme: "*",
|
||||
host: "*",
|
||||
path: "*",
|
||||
});
|
||||
matches = parsePatternMatchesURL("*://example*/*");
|
||||
expect(matches).toEqual({
|
||||
scheme: "*",
|
||||
host: "*",
|
||||
path: "*",
|
||||
});
|
||||
matches = parsePatternMatchesURL("http*://*.example.com/*");
|
||||
expect(matches).toEqual({
|
||||
scheme: "*",
|
||||
host: "*.example.com",
|
||||
path: "*",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -64,20 +64,11 @@ export default class Match<T> {
|
||||
let pos = u.host.indexOf("*");
|
||||
if (u.host === "*" || u.host === "**") {
|
||||
pos = -1;
|
||||
} else if (u.host.endsWith("*")) {
|
||||
// 处理*结尾
|
||||
if (!u.host.endsWith(":*")) {
|
||||
u.host = u.host.substring(0, u.host.length - 1);
|
||||
}
|
||||
}
|
||||
u.host = u.host.replace(/\*/g, "[^/]*?");
|
||||
// 处理 *.开头
|
||||
if (u.host.startsWith("[^/]*?.")) {
|
||||
u.host = `([^/]*?\\.?)${u.host.substring(7)}`;
|
||||
} else if (pos !== -1) {
|
||||
if (u.host.indexOf(".") === -1) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
// 处理顶域
|
||||
if (u.host.endsWith("tld")) {
|
||||
@ -223,6 +214,7 @@ export interface PatternMatchesUrl {
|
||||
}
|
||||
|
||||
// 解析URL, 根据https://developer.chrome.com/docs/extensions/develop/concepts/match-patterns?hl=zh-cn进行处理
|
||||
// 将一些异常情况直接转为通配,用最大的范围去注册userScript,在执行的时候再用UrlMatch去匹配过滤
|
||||
export function parsePatternMatchesURL(
|
||||
url: string,
|
||||
options?: {
|
||||
@ -251,6 +243,9 @@ export function parsePatternMatchesURL(
|
||||
}
|
||||
}
|
||||
if (result) {
|
||||
if (result.scheme === "http*") {
|
||||
result.scheme = "*";
|
||||
}
|
||||
if (result.host !== "*") {
|
||||
// *开头但是不是*.的情况
|
||||
if (result.host.startsWith("*")) {
|
||||
@ -261,6 +256,10 @@ export function parsePatternMatchesURL(
|
||||
}
|
||||
// 结尾是*的情况
|
||||
if (result.host.endsWith("*")) {
|
||||
result.host = "*";
|
||||
}
|
||||
// 结尾是.的情况
|
||||
if (result.host.endsWith(".")) {
|
||||
result.host = result.host.slice(0, -1);
|
||||
}
|
||||
// 处理 www.*.example.com 的情况为 *.example.com
|
||||
|
@ -264,3 +264,14 @@ export function errorMsg(e: any): string {
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
export function isUserScriptsAvailable() {
|
||||
try {
|
||||
// Property access which throws if developer mode is not enabled.
|
||||
chrome.userScripts;
|
||||
return true;
|
||||
} catch {
|
||||
// Not available.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user