diff --git a/.env b/.env index a429922..e04fb10 100644 --- a/.env +++ b/.env @@ -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 diff --git a/config-overrides.js b/config-overrides.js index 3eab540..d969632 100644 --- a/config-overrides.js +++ b/config-overrides.js @@ -2,74 +2,138 @@ 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"); + +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 +// @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, + collapseWhitespace: true, + removeRedundantAttributes: true, + useShortDoctype: true, + removeEmptyAttributes: true, + removeStyleLinkTypeAttributes: true, + keepClosingSlash: true, + minifyJS: true, + minifyCSS: true, + minifyURLs: true, + }; + const names = [ + "HtmlWebpackPlugin", + "WebpackManifestPlugin", + "MiniCssExtractPlugin", + ]; + + config.entry = { + popup: paths.appIndexJs, + options: paths.appSrc + "/options.js", + background: paths.appSrc + "/background.js", + content: paths.appSrc + "/content.js", + }; + + config.output.filename = "[name].js"; + config.output.assetModuleFilename = "media/[name][ext]"; + config.optimization.splitChunks = { cacheGroups: { default: false } }; + config.optimization.runtimeChunk = false; + + config.plugins = config.plugins.filter( + (plugin) => !names.includes(plugin.constructor.name) + ); + + config.plugins.push( + new HtmlWebpackPlugin({ + inject: true, + chunks: ["options"], + template: paths.appHtml, + filename: "options.html", + minify, + }), + new HtmlWebpackPlugin({ + inject: true, + chunks: ["content"], + template: paths.appPublic + "/content.html", + filename: "content.html", + minify, + }), + new HtmlWebpackPlugin({ + inject: true, + chunks: ["popup"], + template: paths.appHtml, + filename: "popup.html", + minify, + }), + new WebpackManifestPlugin({ + fileName: "asset-manifest.json", + }), + new MiniCssExtractPlugin({ + filename: "css/[name].css", + }) + ); + + 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: (config, env) => { - const isEnvProduction = env === "production"; - const minify = isEnvProduction && { - removeComments: true, - collapseWhitespace: true, - removeRedundantAttributes: true, - useShortDoctype: true, - removeEmptyAttributes: true, - removeStyleLinkTypeAttributes: true, - keepClosingSlash: true, - minifyJS: true, - minifyCSS: true, - minifyURLs: true, - }; - const names = [ - "HtmlWebpackPlugin", - "WebpackManifestPlugin", - "MiniCssExtractPlugin", - ]; - - config.entry = { - popup: paths.appIndexJs, - options: paths.appSrc + "/options.js", - background: paths.appSrc + "/background.js", - content: paths.appSrc + "/content.js", - }; - - config.output.filename = "[name].js"; - config.output.assetModuleFilename = "media/[name][ext]"; - config.optimization.splitChunks = { cacheGroups: { default: false } }; - config.optimization.runtimeChunk = false; - - config.plugins = config.plugins.filter( - (plugin) => !names.includes(plugin.constructor.name) - ); - - config.plugins.push( - new HtmlWebpackPlugin({ - inject: true, - chunks: ["options"], - template: paths.appHtml, - filename: "options.html", - minify, - }), - new HtmlWebpackPlugin({ - inject: true, - chunks: ["content"], - template: paths.appPublic + "/content.html", - filename: "content.html", - minify, - }), - new HtmlWebpackPlugin({ - inject: true, - chunks: ["popup"], - template: paths.appHtml, - filename: "popup.html", - minify, - }), - new WebpackManifestPlugin({ - fileName: "asset-manifest.json", - }), - new MiniCssExtractPlugin({ - filename: "css/[name].css", - }) - ); - - return config; - }, + webpack: isEnvUserscript ? userscriptWebpack : extWebpack, }; diff --git a/package.json b/package.json index 15be2c1..c914bad 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/config/i18n.js b/src/config/i18n.js index 74a7986..f41e5f0 100644 --- a/src/config/i18n.js +++ b/src/config/i18n.js @@ -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: `界面语言`, diff --git a/src/config/index.js b/src/config/index.js index d15bb3a..c436d1c 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -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 = { diff --git a/src/content.js b/src/content.js index 3bd81b4..444b217 100644 --- a/src/content.js +++ b/src/content.js @@ -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( - - - - ); - }; -} +import { Translator } from "./libs/translator"; /** * 入口函数 diff --git a/src/libs/auth.js b/src/libs/auth.js index bfc7725..303590c 100644 --- a/src/libs/auth.js +++ b/src/libs/auth.js @@ -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查询 diff --git a/src/libs/fetch.js b/src/libs/fetch.js index 4d6129d..1e85257 100644 --- a/src/libs/fetch.js +++ b/src/libs/fetch.js @@ -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); diff --git a/src/libs/storage.js b/src/libs/storage.js index 0531bd8..5a63e5d 100644 --- a/src/libs/storage.js +++ b/src/libs/storage.js @@ -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); } diff --git a/src/libs/translator.js b/src/libs/translator.js new file mode 100644 index 0000000..ed61249 --- /dev/null +++ b/src/libs/translator.js @@ -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( + + + + ); + }; +} diff --git a/src/userscript.js b/src/userscript.js new file mode 100644 index 0000000..444b217 --- /dev/null +++ b/src/userscript.js @@ -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 }; + }); +})();