userscript...

This commit is contained in:
Gabe Yuan
2023-08-05 15:32:51 +08:00
parent bec207a09f
commit 1e82c280ad
11 changed files with 323 additions and 202 deletions

2
.env
View File

@@ -1,3 +1,5 @@
GENERATE_SOURCEMAP=false
REACT_APP_NAME=KISS Translator
REACT_APP_VERSION=1.2.3
REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator

View File

@@ -2,9 +2,38 @@ const paths = require("react-scripts/config/paths");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const webpack = require("webpack");
module.exports = {
webpack: (config, env) => {
const isEnvUserscript = process.env.REACT_APP_CLIENT === "userscript";
const banner = `// ==UserScript==
// @name ${process.env.REACT_APP_NAME}
// @namespace ${process.env.REACT_APP_HOMEPAGE}
// @version ${process.env.REACT_APP_VERSION}
// @description A minimalist bilingual translation extension.
// @author Gabe<yugang2002@gmail.com>
// @homepageURL ${process.env.REACT_APP_HOMEPAGE}
// @match *://*/*
// @icon https://raw.githubusercontent.com/fishjar/kiss-translator/master/public/images/logo192.png
// @downloadURL https://raw.githubusercontent.com/fishjar/kiss-translator/master/dist/userscript/kiss-translator.user.js
// @updateURL https://raw.githubusercontent.com/fishjar/kiss-translator/master/dist/userscript/kiss-translator.user.js
// @grant GM_xmlhttpRequest
// @grant GM.xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM.setValue
// @grant GM.getValue
// @connect translate.googleapis.com
// @connect api-edge.cognitive.microsofttranslator.com
// @connect edge.microsoft.com
// @connect api.openai.com
// @connect localhost
// ==/UserScript==
`;
// 扩展
const extWebpack = (config, env) => {
const isEnvProduction = env === "production";
const minify = isEnvProduction && {
removeComments: true,
@@ -71,5 +100,40 @@ module.exports = {
);
return config;
},
};
// 油猴
const userscriptWebpack = (config, env) => {
const names = [
"HtmlWebpackPlugin",
"WebpackManifestPlugin",
"MiniCssExtractPlugin",
];
config.entry = {
userscript: paths.appSrc + "/userscript.js",
};
config.output.filename = "kiss-translator.user.js";
config.optimization.splitChunks = { cacheGroups: { default: false } };
config.optimization.runtimeChunk = false;
config.optimization.minimize = false;
config.plugins = config.plugins.filter(
(plugin) => !names.includes(plugin.constructor.name)
);
config.plugins.push(
new webpack.BannerPlugin({
banner,
raw: true,
entryOnly: true,
})
);
return config;
};
module.exports = {
webpack: isEnvUserscript ? userscriptWebpack : extWebpack,
};

View File

@@ -19,9 +19,11 @@
},
"scripts": {
"start": "REACT_APP_CLIENT=web react-app-rewired start",
"start:userscript": "REACT_APP_CLIENT=userscript react-app-rewired start",
"build": "BUILD_PATH=./build/chrome REACT_APP_CLIENT=chrome react-app-rewired build && rm ./build/chrome/manifest.firefox.json",
"build:edge": "BUILD_PATH=./build/edge REACT_APP_CLIENT=edge react-app-rewired build && rm ./build/edge/manifest.firefox.json",
"build:firefox": "BUILD_PATH=./build/firefox REACT_APP_CLIENT=firefox react-app-rewired build && rm ./build/firefox/manifest.json && mv ./build/firefox/manifest.firefox.json ./build/firefox/manifest.json",
"build:userscript": "BUILD_PATH=./build/userscript REACT_APP_CLIENT=userscript react-app-rewired build",
"build:all": "yarn build && yarn build:edge && yarn build:firefox",
"dist": "yarn build:all && rm -r dist && cp -r build dist",
"test": "react-app-rewired test",

View File

@@ -1,5 +1,3 @@
const URL_APP_HOMEPAGE = "https://github.com/fishjar/kiss-translator";
export const UI_LANGS = [
["zh", "中文"],
["en", "English"],
@@ -35,8 +33,8 @@ export const I18N = {
en: `README.en.md`,
},
about_md_local: {
zh: `请 [点击这里](${URL_APP_HOMEPAGE}) 查看详情。`,
en: `Please [click here](${URL_APP_HOMEPAGE}) for details.`,
zh: `请 [点击这里](${process.env.REACT_APP_HOMEPAGE}) 查看详情。`,
en: `Please [click here](${process.env.REACT_APP_HOMEPAGE}) for details.`,
},
ui_lang: {
zh: `界面语言`,

View File

@@ -129,7 +129,7 @@ export const DEFAULT_RULE = {
fromLang: "auto",
toLang: "zh-CN",
textStyle: OPT_STYLE_DASHLINE,
transOpen: true,
transOpen: false,
};
export const DEFAULT_SETTING = {

View File

@@ -1,129 +1,13 @@
import { browser } from "./libs/browser";
import { createRoot } from "react-dom/client";
import {
APP_LCNAME,
MSG_TRANS_TOGGLE,
MSG_TRANS_GETRULE,
MSG_TRANS_PUTRULE,
TRANS_MIN_LENGTH,
TRANS_MAX_LENGTH,
} from "./config";
import Content from "./views/Content";
import { StoragesProvider } from "./hooks/Storage";
import { queryEls, getRules, matchRule } from "./libs";
import { getRules, matchRule } from "./libs";
import { getSetting } from "./libs";
import { transPool } from "./libs/pool";
/**
* 翻译类
*/
class Translator {
_rule = {};
_interseObserver = new IntersectionObserver(
(intersections) => {
intersections.forEach((intersection) => {
if (intersection.isIntersecting) {
this._render(intersection.target);
this._interseObserver.unobserve(intersection.target);
}
});
},
{
threshold: 0.1,
}
);
_mutaObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
try {
queryEls(this._rule.selector, node).forEach((el) => {
this._interseObserver.observe(el);
});
} catch (err) {
//
}
});
});
});
constructor(rule) {
this._rule = rule;
if (rule.transOpen) {
this._register();
}
}
get rule() {
return this._rule;
}
updateRule = (obj) => {
this._rule = { ...this._rule, ...obj };
};
toggle = () => {
if (this._rule.transOpen) {
this._rule.transOpen = false;
this._unRegister();
} else {
this._rule.transOpen = true;
this._register();
}
};
_register = () => {
// 监听节点变化
this._mutaObserver.observe(document, {
childList: true,
subtree: true,
});
// 监听节点显示
queryEls(this._rule.selector).forEach((el) => {
this._interseObserver.observe(el);
});
};
_unRegister = () => {
// 解除节点变化监听
this._mutaObserver.disconnect();
// 解除节点显示监听
queryEls(this._rule.selector).forEach((el) =>
this._interseObserver.unobserve(el)
);
// 移除已插入元素
queryEls(APP_LCNAME).forEach((el) => el.remove());
};
_render = (el) => {
if (el.querySelector(APP_LCNAME)) {
return;
}
// 除openai外保留code和a标签
const q = el.innerText.trim();
if (!q || q.length < TRANS_MIN_LENGTH || q.length > TRANS_MAX_LENGTH) {
// 太长或太短不翻译
return;
}
// console.log("---> ", q);
const span = document.createElement(APP_LCNAME);
el.appendChild(span);
const root = createRoot(span);
root.render(
<StoragesProvider>
<Content q={q} rule={this._rule} />
</StoragesProvider>
);
};
}
import { Translator } from "./libs/translator";
/**
* 入口函数

View File

@@ -2,7 +2,14 @@ import storage from "./storage";
import { STOKEY_MSAUTH, URL_MICROSOFT_AUTH } from "../config";
import { fetchPolyfill } from "./fetch";
const parseMSToken = (token) => JSON.parse(atob(token.split(".")[1])).exp;
const parseMSToken = (token) => {
try {
return JSON.parse(atob(token.split(".")[1])).exp;
} catch (err) {
console.log("[parseMSToken]", err);
}
return 0;
};
/**
* 闭包缓存token减少对storage查询

View File

@@ -13,20 +13,29 @@ import {
* @param {*} init
* @returns
*/
const fetchGM = async (input, { method, headers, body }) =>
const fetchGM = async (input, { method = "GET", headers, body } = {}) =>
new Promise((resolve, reject) => {
try {
window.GM.xmlhttpRequest({
window.GM_xmlhttpRequest({
method,
url: input,
headers,
data: body,
onload: (response) => {
resolve(new Response(response.response));
},
onerror: (error) => {
reject(error);
if (response.status === 200) {
const headers = new Headers();
response.responseHeaders.split("\n").forEach((line) => {
let [name, value] = line.split(":").map((item) => item.trim());
if (name && value) {
headers.append(name, value);
}
});
resolve(new Response(response.response, { headers }));
} else {
reject(new Error(`[${response.status}] ${response.responseText}`));
}
},
onerror: reject,
});
} catch (error) {
reject(error);

View File

@@ -21,11 +21,11 @@ async function set(key, val) {
async function get(key) {
if (isExt) {
const res = await browser.storage.local.get([key]);
return res[key];
const val = await browser.storage.local.get([key]);
return val[key];
} else if (isGm) {
const res = await window.GM.getValue(key);
return res;
const val = await window.GM.getValue(key);
return val;
}
return window.localStorage.getItem(key);
}

116
src/libs/translator.js Normal file
View File

@@ -0,0 +1,116 @@
import { createRoot } from "react-dom/client";
import { APP_LCNAME, TRANS_MIN_LENGTH, TRANS_MAX_LENGTH } from "../config";
import { StoragesProvider } from "../hooks/Storage";
import { queryEls } from ".";
import Content from "../views/Content";
/**
* 翻译类
*/
export class Translator {
_rule = {};
_interseObserver = new IntersectionObserver(
(intersections) => {
intersections.forEach((intersection) => {
if (intersection.isIntersecting) {
this._render(intersection.target);
this._interseObserver.unobserve(intersection.target);
}
});
},
{
threshold: 0.1,
}
);
_mutaObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
try {
queryEls(this._rule.selector, node).forEach((el) => {
this._interseObserver.observe(el);
});
} catch (err) {
//
}
});
});
});
constructor(rule) {
this._rule = rule;
if (rule.transOpen) {
this._register();
}
}
get rule() {
return this._rule;
}
updateRule = (obj) => {
this._rule = { ...this._rule, ...obj };
};
toggle = () => {
if (this._rule.transOpen) {
this._rule.transOpen = false;
this._unRegister();
} else {
this._rule.transOpen = true;
this._register();
}
};
_register = () => {
// 监听节点变化
this._mutaObserver.observe(document, {
childList: true,
subtree: true,
});
// 监听节点显示
queryEls(this._rule.selector).forEach((el) => {
this._interseObserver.observe(el);
});
};
_unRegister = () => {
// 解除节点变化监听
this._mutaObserver.disconnect();
// 解除节点显示监听
queryEls(this._rule.selector).forEach((el) =>
this._interseObserver.unobserve(el)
);
// 移除已插入元素
queryEls(APP_LCNAME).forEach((el) => el.remove());
};
_render = (el) => {
if (el.querySelector(APP_LCNAME)) {
return;
}
// 除openai外保留code和a标签
const q = el.innerText.trim();
if (!q || q.length < TRANS_MIN_LENGTH || q.length > TRANS_MAX_LENGTH) {
// 太长或太短不翻译
return;
}
// console.log("---> ", q);
const span = document.createElement(APP_LCNAME);
el.appendChild(span);
const root = createRoot(span);
root.render(
<StoragesProvider>
<Content q={q} rule={this._rule} />
</StoragesProvider>
);
};
}

39
src/userscript.js Normal file
View File

@@ -0,0 +1,39 @@
import { browser } from "./libs/browser";
import {
MSG_TRANS_TOGGLE,
MSG_TRANS_GETRULE,
MSG_TRANS_PUTRULE,
} from "./config";
import { getRules, matchRule } from "./libs";
import { getSetting } from "./libs";
import { transPool } from "./libs/pool";
import { Translator } from "./libs/translator";
/**
* 入口函数
*/
(async () => {
const { fetchInterval, fetchLimit } = await getSetting();
transPool.update(fetchInterval, fetchLimit);
const rules = await getRules();
const rule = matchRule(rules, document.location.href);
const translator = new Translator(rule);
// 监听消息
browser?.runtime.onMessage.addListener(async ({ action, args }) => {
switch (action) {
case MSG_TRANS_TOGGLE:
translator.toggle();
break;
case MSG_TRANS_GETRULE:
break;
case MSG_TRANS_PUTRULE:
translator.updateRule(args);
break;
default:
return { error: `message action is unavailable: ${action}` };
}
return { data: translator.rule };
});
})();