Compare commits

..

31 Commits

Author SHA1 Message Date
Gabe Yuan
620ac464eb v1.5.6 2023-08-27 17:59:47 +08:00
Gabe Yuan
62289f8ab8 catch detect lang err 2023-08-27 17:43:27 +08:00
Gabe Yuan
d84594da96 catch global error and display on top of page 2023-08-27 16:45:57 +08:00
Gabe Yuan
e1d74aae6a catch global error and display on top of page 2023-08-27 16:41:14 +08:00
Gabe Yuan
c4980d9eb7 fix rules 2023-08-26 22:12:48 +08:00
Gabe Yuan
882d83c6b7 update helper text 2023-08-26 15:08:21 +08:00
Gabe Yuan
c4a7fd81f8 v1.5.5 2023-08-26 14:47:15 +08:00
Gabe Yuan
0e55799109 fix sync test button 2023-08-26 14:42:50 +08:00
Gabe Yuan
a3cdcb2a1a add sync test button 2023-08-26 14:31:13 +08:00
Gabe Yuan
e0ccc298f9 add foxnews rule 2023-08-26 13:49:44 +08:00
Gabe Yuan
36b49bb577 modify fab opacity to 0.2 2023-08-26 13:45:24 +08:00
Gabe Yuan
2636c24e84 re translate when text changed 2023-08-26 13:10:13 +08:00
Gabe Yuan
6bcf294635 userscript in iframe 2023-08-26 12:11:21 +08:00
Gabe Yuan
c5fa6689a4 content script in iframe 2023-08-26 12:02:16 +08:00
Gabe Yuan
3bf0cb2485 usescript in iframe 2023-08-26 11:43:00 +08:00
Gabe Yuan
19c9335527 shadow root 2023-08-26 00:08:12 +08:00
Gabe Yuan
20da2e1b97 shadow root 2023-08-25 22:48:47 +08:00
Gabe Yuan
9eceb8641d shadow root 2023-08-25 22:48:11 +08:00
Gabe Yuan
86bc915d74 shadow root 2023-08-25 17:07:53 +08:00
Gabe Yuan
6b35525207 run script in iframe 2023-08-24 16:40:42 +08:00
Gabe Yuan
4633bf4fc6 run script in iframe 2023-08-24 16:39:35 +08:00
Gabe Yuan
2665f31d94 fix iframe bug 2023-08-24 16:21:01 +08:00
Gabe Yuan
6c4d3149eb fix shadow dom 2023-08-24 15:07:13 +08:00
Gabe Yuan
a2762e6ce6 fix shadow dom 2023-08-24 14:57:54 +08:00
Gabe Yuan
792a1bfcad Merge branch 'master' into dev 2023-08-24 10:10:26 +08:00
Gabe Yuan
a0eba9d60e update readme 2023-08-24 10:10:00 +08:00
Gabe Yuan
c2e0064253 support shadow dom 2023-08-23 18:01:47 +08:00
Gabe Yuan
f246efc84b support shadow dom 2023-08-23 17:53:46 +08:00
Gabe Yuan
4a3bf7e96c some minor modifications 2023-08-23 10:39:01 +08:00
Gabe Yuan
523b81090d min length & max length can be set 2023-08-22 21:45:23 +08:00
Gabe Yuan
d706c405d9 add shortcut text to pop page 2023-08-22 21:14:33 +08:00
26 changed files with 580 additions and 324 deletions

2
.env
View File

@@ -2,7 +2,7 @@ GENERATE_SOURCEMAP=false
REACT_APP_NAME=KISS Translator
REACT_APP_NAME_CN=简约翻译
REACT_APP_VERSION=1.5.4
REACT_APP_VERSION=1.5.6
REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator
REACT_APP_OPTIONSPAGE=https://kiss-translator.rayjar.com/options
REACT_APP_OPTIONSPAGE2=https://fishjar.github.io/kiss-translator/options.html

View File

@@ -32,9 +32,9 @@ If you also like a little more simplicity, welcome to pick it up.
- [x] Microsoft
- [x] OpenAI
- [ ] DeepL
- [ ] Upload to app Store
- [x] Upload to app Store
- [x] [Chrome](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof)
- [ ] Edge
- [x] [Edge](https://microsoftedge.microsoft.com/addons/detail/kiss-translator/jemckldkclkinpjighnoilpbldbdmmlh)
- [x] [Firefox](https://addons.mozilla.org/zh-CN/firefox/addon/kiss-translator/)
- [ ] Safari
- [x] [Greasy Fork](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator)

View File

@@ -32,9 +32,9 @@
- [x] Microsoft
- [x] OpenAI
- [ ] DeepL
- [ ] 上架应用市场
- [x] 上架应用市场
- [x] [Chrome](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof?hl=zh-CN)
- [ ] Edge
- [x] [Edge](https://microsoftedge.microsoft.com/addons/detail/%E7%AE%80%E7%BA%A6%E7%BF%BB%E8%AF%91/jemckldkclkinpjighnoilpbldbdmmlh?hl=zh-CN)
- [x] [Firefox](https://addons.mozilla.org/zh-CN/firefox/addon/kiss-translator/)
- [ ] Safari
- [x] [Greasy Fork](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator)

View File

@@ -1,7 +1,7 @@
{
"name": "kiss-translator",
"description": "A minimalist bilingual translation Extension & Greasemonkey Script",
"version": "1.5.4",
"version": "1.5.6",
"author": "Gabe<yugang2002@gmail.com>",
"private": true,
"dependencies": {

View File

@@ -15,12 +15,63 @@
max-height: 1.2em;
}
</style>
<script>
document.addEventListener("DOMContentLoaded", function () {
// (() => {
// var shadow = document.querySelector("#shadow1");
// var root = shadow.attachShadow({ mode: "open" });
// var newLine = document.createElement("p");
// newLine.innerText = "new line";
// root.appendChild(newLine);
// })();
// setTimeout(function () {
// var shadow = document.querySelector("#shadow2");
// var root = shadow.attachShadow({ mode: "open" });
// }, 1000);
// setTimeout(() => {
// var newLine = document.createElement("p");
// newLine.innerText = "new line";
// var shadow = document.querySelector("#shadow2");
// shadow.shadowRoot.appendChild(newLine);
// }, 2000);
// setTimeout(() => {
// var newLine = document.createElement("div");
// newLine.innerHTML = "<p>second line</p><p>third line</p>";
// var shadow = document.querySelector("#shadow2");
// shadow.shadowRoot.appendChild(newLine);
// }, 3000);
// setTimeout(function () {
// var el = document.querySelector("h2");
// el.innerText = "hello world";
// var title = document.querySelector("#addtitle");
// title.innerHTML =
// "<div><p>second title</p><ul><li>second title</li><li><p>second title</p></li></ul></div>";
// }, 1000);
setTimeout(function () {
var el = document.querySelector("h2>p>span");
el.innerText = "hello world";
}, 1000);
});
</script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root">
<h2>React is a JavaScript library for building user interfaces.</h2>
<h2>
<p><span>React is a JavaScript library for building user interfaces.</span></p>
</h2>
<div id="addtitle"></div>
<h2>Shadow 1</h2>
<div id="shadow1"></div>
<h2>Shadow 2</h2>
<div id="shadow2"></div>
<br />
<br />
<br />
@@ -53,7 +104,16 @@
<br />
<br />
<br />
<h2>React is a JavaScript library for building user interfaces.</h2>
<h2>
React Server Components (or RSC) is a new application architecture
designed by the React team.
</h2>
<iframe
id="iframe1"
width="800px"
height="600px"
src="http://localhost:3000/index.html"
></iframe>
<br />
<br />
<br />
@@ -86,7 +146,10 @@
<br />
<br />
<br />
<h2>React is a JavaScript library for building user interfaces.</h2>
<h2>
Weve first shared our research on RSC in an introductory talk and an
RFC.
</h2>
<br />
<br />
<br />
@@ -119,7 +182,17 @@
<br />
<br />
<br />
<h2>React is a JavaScript library for building user interfaces.</h2>
<h2>
To recap them, we are introducing a new kind of component—Server
Components—that run ahead of time and are excluded from your JavaScript
bundle.
</h2>
<iframe
id="iframe2"
width="800px"
height="600px"
src="https://react.dev/"
></iframe>
<br />
<br />
<br />
@@ -153,175 +226,42 @@
<br />
<br />
<div class="cont cont1">
<h2>React is a JavaScript library for building user interfaces.</h2>
<ul>
<li>
Declarative: React makes it painless to create interactive UIs.
Design simple views for each state in your application, and React
will efficiently update and render just the right components when
your data changes. Declarative views make your code more
predictable, simpler to understand, and easier to debug.
</li>
<li>
Component-Based: Build encapsulated components that manage their own
state, then compose them to make complex UIs. Since component logic
is written in JavaScript instead of templates, you can easily pass
rich data through your app and keep the state out of the DOM.
</li>
<li>
React 使创建交互式 UI
变得轻而易举。为你应用的每一个状态设计简洁的视图,当数据变动时 React
能高效更新并渲染合适的组件。
</li>
<li>以声明式编写 UI可以让你的代码更加可靠且方便调试。</li>
</ul>
</div>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<div class="cont cont2">
<h2>React is a JavaScript library for building user interfaces.</h2>
<ul>
<li>
Declarative: React makes it painless to create interactive UIs.
Design simple views for each state in your application, and React
will efficiently update and render just the right components when
your data changes. Declarative views make your code more
predictable, simpler to understand, and easier to debug.
</li>
<li>
Component-Based: Build encapsulated components that manage their own
state, then compose them to make complex UIs. Since component logic
is written in JavaScript instead of templates, you can easily pass
rich data through your app and keep the state out of the DOM.
</li>
<li>
React 使创建交互式 UI
变得轻而易举。为你应用的每一个状态设计简洁的视图,当数据变动时 React
能高效更新并渲染合适的组件。
</li>
<li>以声明式编写 UI可以让你的代码更加可靠且方便调试。</li>
</ul>
</div>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<div class="cont cont3">
<h2>React is a JavaScript library for building user interfaces.</h2>
<ul>
<li>
Declarative: React makes it painless to create interactive UIs.
Design simple views for each state in your application, and React
will efficiently update and render just the right components when
your data changes. Declarative views make your code more
predictable, simpler to understand, and easier to debug.
</li>
<li>
Component-Based: Build encapsulated components that manage their own
state, then compose them to make complex UIs. Since component logic
is written in JavaScript instead of templates, you can easily pass
rich data through your app and keep the state out of the DOM.
</li>
<li>
React 使创建交互式 UI
变得轻而易举。为你应用的每一个状态设计简洁的视图,当数据变动时 React
能高效更新并渲染合适的组件。
</li>
<li>以声明式编写 UI可以让你的代码更加可靠且方便调试。</li>
</ul>
</div>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<div class="cont cont4">
<h2>
React is a <code>JavaScript</code> <a href="#">library</a> for
building user interfaces.
Server Components can run during the build, letting you read from the
filesystem or fetch static content.
</h2>
<ul>
<li>
Declarative: React makes it painless to create interactive UIs.
Design simple views for each state in your application, and React
will efficiently update and render just the right components when
your data changes. Declarative views make your code more
predictable, simpler to understand, and easier to debug.
</li>
<li>
Component-Based: Build encapsulated components that manage their own
state, then compose them to make complex UIs. Since component logic
is written in JavaScript instead of templates, you can easily pass
rich data through your app and keep the state out of the DOM.
</li>
<li>
React 使创建交互式 UI
变得轻而易举。为你应用的每一个状态设计简洁的视图,当数据变动时 React
能高效更新并渲染合适的组件。
They can also run on the server, letting you access your data layer
without having to build an API. You can pass data by props from
Server Components to the interactive Client Components in the
browser.
</li>
<li>以声明式编写 UI可以让你的代码更加可靠且方便调试。</li>
</ul>
</div>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<p></p>
<div class="cont cont5">
<h2>React is a JavaScript library for building user interfaces.</h2>
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<div class="cont cont2">
<h2>
Since our last update, we have merged the React Server Components RFC
to ratify the proposal.
</h2>
<ul>
<li>
Declarative: React makes it painless to create interactive UIs.
Design simple views for each state in your application, and React
will efficiently update and render just the right components when
your data changes. Declarative views make your code more
predictable, simpler to understand, and easier to debug.
</li>
<li>
Component-Based: Build encapsulated components that manage their own
state, then compose them to make complex UIs. Since component logic
is written in JavaScript instead of templates, you can easily pass
rich data through your app and keep the state out of the DOM.
RSC combines the simple “request/response” mental model of
server-centric Multi-Page Apps with the seamless interactivity of
client-centric Single-Page Apps, giving you the best of both worlds.
</li>
<li>
React 使创建交互式 UI

View File

@@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "__MSG_app_name__",
"description": "__MSG_app_description__",
"version": "1.5.4",
"version": "1.5.6",
"default_locale": "en",
"author": "Gabe<yugang2002@gmail.com>",
"homepage_url": "https://github.com/fishjar/kiss-translator",
@@ -12,7 +12,8 @@
"content_scripts": [
{
"js": ["content.js"],
"matches": ["<all_urls>"]
"matches": ["<all_urls>"],
"all_frames": true
}
],
"commands": {

View File

@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "__MSG_app_name__",
"description": "__MSG_app_description__",
"version": "1.5.4",
"version": "1.5.6",
"default_locale": "en",
"author": "Gabe<yugang2002@gmail.com>",
"homepage_url": "https://github.com/fishjar/kiss-translator",
@@ -13,7 +13,8 @@
"content_scripts": [
{
"js": ["content.js"],
"matches": ["<all_urls>"]
"matches": ["<all_urls>"],
"all_frames": true
}
],
"commands": {

View File

@@ -19,7 +19,7 @@ import {
} from "./config";
import storage from "./libs/storage";
import { getSetting } from "./libs";
import { syncAll } from "./libs/sync";
import { trySyncAll } from "./libs/sync";
import { fetchData, fetchPool } from "./libs/fetch";
import { sendTabMsg } from "./libs/msg";
import { trySyncAllSubRules } from "./libs/rules";
@@ -45,7 +45,7 @@ browser.runtime.onStartup.addListener(async () => {
console.log("browser onStartup");
// 同步数据
await syncAll(true);
await trySyncAll(true);
// 清除缓存
const setting = await getSetting();

View File

@@ -12,6 +12,10 @@ export const I18N = {
zh: `翻译`,
en: `Translate`,
},
translate_alt: {
zh: `翻译 (Alt+Q)`,
en: `Translate (Alt+Q)`,
},
basic_setting: {
zh: `基本设置`,
en: `Basic Setting`,
@@ -41,12 +45,20 @@ export const I18N = {
en: `Interface Language`,
},
fetch_limit: {
zh: `最大请求数量`,
en: `Maximum Number Of Request`,
zh: `最大请求数量 (1-100)`,
en: `Maximum Number Of Request (1-100)`,
},
fetch_interval: {
zh: `请求间隔时间(ms)`,
en: `Request Interval(ms)`,
zh: `请求间隔时间 (0-5000ms)`,
en: `Request Interval (0-5000ms)`,
},
min_translate_length: {
zh: `最小翻译长度 (1-100)`,
en: `Min Translate Length (1-100)`,
},
max_translate_length: {
zh: `最大翻译长度 (100-10000)`,
en: `Max Translate Length (100-10000)`,
},
translate_service: {
zh: `翻译服务`,
@@ -64,6 +76,10 @@ export const I18N = {
zh: `文字样式`,
en: `Text Style`,
},
text_style_alt: {
zh: `文字样式 (Alt+C)`,
en: `Text Style (Alt+C)`,
},
bg_color: {
zh: `样式颜色`,
en: `Style Color`,
@@ -177,8 +193,8 @@ export const I18N = {
en: `1. The asterisk (*) wildcard is supported. 2. Multiple URLs can be separated by English commas ",".`,
},
selector_helper: {
zh: `1、遵循CSS选择器规则。2、留空表示采用全局设置。`,
en: `1. Follow CSS selector rules. 2. Leave blank to adopt the global setting.`,
zh: `1、遵循CSS选择器规则。2、留空表示采用全局设置。3、多个CSS选择器之间用“;”隔开。4、“shadow root”选择器和内部选择器用“>>>”隔开。`,
en: `1. Follow the CSS selector rules. 2. Leave blank to adopt the global setting. 3. Separate multiple CSS selectors with ";". 4. The "shadow root" selector and the internal selector are separated by ">>>".`,
},
translate_switch: {
zh: `开启翻译`,
@@ -256,6 +272,18 @@ export const I18N = {
zh: `数据同步密钥`,
en: `Data Sync Key`,
},
data_sync_test: {
zh: `数据同步测试`,
en: `Data Sync Test`,
},
data_sync_success: {
zh: `数据同步成功!`,
en: `Data Sync Success`,
},
data_sync_error: {
zh: `数据同步失败!`,
en: `Data Sync Error`,
},
error_got_some_wrong: {
zh: "抱歉,出错了!",
en: "Sorry, something went wrong!",

View File

@@ -1,11 +1,12 @@
import {
DEFAULT_SELECTOR,
GLOBAL_KEY,
SHADOW_KEY,
DEFAULT_RULE,
BUILTIN_RULES,
} from "./rules";
export { I18N, UI_LANGS } from "./i18n";
export { GLOBAL_KEY, DEFAULT_RULE, BUILTIN_RULES };
export { GLOBAL_KEY, SHADOW_KEY, DEFAULT_RULE, BUILTIN_RULES };
const APP_NAME = process.env.REACT_APP_NAME.trim().split(/\s+/).join("-");
@@ -165,11 +166,16 @@ export const DEFAULT_SUBRULES_LIST = [
},
];
export const TRANS_MIN_LENGTH = 5; // 最短翻译长度
export const TRANS_MAX_LENGTH = 5000; // 最长翻译长度
export const DEFAULT_SETTING = {
darkMode: false, // 深色模式
uiLang: "en", // 界面语言
fetchLimit: DEFAULT_FETCH_LIMIT, // 最大任务数量
fetchInterval: DEFAULT_FETCH_INTERVAL, // 任务间隔时间
minLength: TRANS_MIN_LENGTH,
maxLength: TRANS_MAX_LENGTH,
clearCache: false, // 是否在浏览器下次启动时清除缓存
injectRules: true, // 是否注入订阅规则
subrulesList: DEFAULT_SUBRULES_LIST, // 订阅列表
@@ -182,9 +188,6 @@ export const DEFAULT_SETTING = {
export const DEFAULT_RULES = [GLOBLA_RULE];
export const TRANS_MIN_LENGTH = 5; // 最短翻译长度
export const TRANS_MAX_LENGTH = 5000; // 最长翻译长度
export const DEFAULT_SYNC = {
syncUrl: "", // 数据同步接口
syncKey: "", // 数据同步密钥

View File

@@ -4,6 +4,8 @@ export const DEFAULT_SELECTOR = `:is(${els})`;
export const GLOBAL_KEY = "*";
export const SHADOW_KEY = ">>>";
export const DEFAULT_RULE = {
pattern: "",
selector: "",
@@ -21,9 +23,13 @@ const RULES = [
selector: `h3, .IsZvec, .VwiC3b`,
},
{
pattern: `https://news.google.com/`,
pattern: `news.google.com`,
selector: `h4`,
},
{
pattern: `www.foxnews.com`,
selector: `h1, h2, .title, .sidebar [data-type="Title"], .article-content ${DEFAULT_SELECTOR}; [data-spotim-module="conversation"]>div >>> [data-spot-im-class="message-text"] p, [data-spot-im-class="message-text"]`,
},
{
pattern: `bearblog.dev, www.theverge.com, www.tampermonkey.net/documentation.php`,
selector: DEFAULT_SELECTOR,

View File

@@ -7,14 +7,16 @@ import {
} from "./config";
import { getSetting, getRules, matchRule } from "./libs";
import { Translator } from "./libs/translator";
import { isIframe } from "./libs/iframe";
/**
* 入口函数
*/
(async () => {
const init = async () => {
const href = isIframe ? document.referrer : document.location.href;
const setting = await getSetting();
const rules = await getRules();
const rule = await matchRule(rules, document.location.href, setting);
const rule = await matchRule(rules, href, setting);
const translator = new Translator(rule, setting);
// 监听消息
@@ -36,4 +38,15 @@ import { Translator } from "./libs/translator";
}
return { data: translator.rule };
});
};
(async () => {
try {
await init();
} catch (err) {
const $err = document.createElement("div");
$err.innerText = `KISS-Translator: ${err.message}`;
$err.style.cssText = "background:red; color:#fff; z-index:10000;";
document.body.prepend($err);
}
})();

View File

@@ -1,7 +1,7 @@
import { STOKEY_RULES, DEFAULT_SUBRULES_LIST } from "../config";
import storage from "../libs/storage";
import { useStorages } from "./Storage";
import { syncRules } from "../libs/sync";
import { trySyncRules } from "../libs/sync";
import { useSync } from "./Sync";
import { useSetting, useSettingUpdate } from "./Setting";
import { checkRules } from "../libs/rules";
@@ -19,7 +19,7 @@ export function useRules() {
const updateAt = sync.opt?.rulesUpdateAt ? Date.now() : 0;
await storage.setObj(STOKEY_RULES, rules);
await sync.update({ rulesUpdateAt: updateAt });
syncRules();
trySyncRules();
};
const add = async (rule) => {

View File

@@ -2,7 +2,7 @@ import { STOKEY_SETTING } from "../config";
import storage from "../libs/storage";
import { useStorages } from "./Storage";
import { useSync } from "./Sync";
import { syncSetting } from "../libs/sync";
import { trySyncSetting } from "../libs/sync";
/**
* 设置hook
@@ -23,6 +23,6 @@ export function useSettingUpdate() {
const updateAt = sync.opt?.settingUpdateAt ? Date.now() : 0;
await storage.putObj(STOKEY_SETTING, obj);
await sync.update({ settingUpdateAt: updateAt });
syncSetting();
trySyncSetting();
};
}

View File

@@ -1,18 +1,31 @@
import React from "react";
import React, { useState } from "react";
import ReactDOM from "react-dom/client";
import CircularProgress from "@mui/material/CircularProgress";
import Divider from "@mui/material/Divider";
import ReactMarkdown from "react-markdown";
import Paper from "@mui/material/Paper";
import Stack from "@mui/material/Stack";
import Button from "@mui/material/Button";
import { useFetch } from "./hooks/Fetch";
import { I18N, URL_RAW_PREFIX } from "./config";
function App() {
const [lang, setLang] = useState("zh");
const [data, loading, error] = useFetch(
`${URL_RAW_PREFIX}/${I18N?.["about_md"]?.["zh"]}`
`${URL_RAW_PREFIX}/${I18N?.["about_md"]?.[lang]}`
);
return (
<Paper sx={{ padding: 2, margin: 2 }}>
<Stack spacing={2} direction="row" justifyContent="flex-end">
<Button
variant="text"
onClick={() => {
setLang((pre) => (pre === "zh" ? "en" : "zh"));
}}
>
{lang === "zh" ? "ENGLISH" : "中文"}
</Button>
</Stack>
<Divider>{`KISS Translator v${process.env.REACT_APP_VERSION}`}</Divider>
{loading ? (
<center>

7
src/libs/iframe.js Normal file
View File

@@ -0,0 +1,7 @@
export const isIframe = window.self !== window.top;
export const sendIframeMsg = (action, args) => {
document.querySelectorAll("iframe").forEach((iframe) => {
iframe.contentWindow.postMessage({ action, args }, "*");
});
};

View File

@@ -12,15 +12,6 @@ import { browser } from "./browser";
import { isMatch } from "./utils";
import { loadSubRules } from "./rules";
/**
* 获取节点列表并转为数组
* @param {*} selector
* @param {*} el
* @returns
*/
export const queryEls = (selector, el = document) =>
Array.from(el.querySelectorAll(selector));
/**
* 查询storage中的设置
* @returns
@@ -108,6 +99,10 @@ export const matchRule = async (
* @returns
*/
export const detectLang = async (q) => {
try {
const res = await browser?.i18n.detectLanguage(q);
return res?.languages?.[0]?.language;
} catch (err) {
console.log("[detect lang]", err);
}
};

View File

@@ -28,7 +28,6 @@ export const syncOpt = {
* @returns
*/
export const syncSetting = async (isBg = false) => {
try {
const { syncUrl, syncKey, settingUpdateAt } = await syncOpt.load();
if (!syncUrl || !syncKey) {
return;
@@ -55,6 +54,11 @@ export const syncSetting = async (isBg = false) => {
} else {
await syncOpt.update({ settingSyncAt: res.updateAt });
}
};
export const trySyncSetting = async (isBg = false) => {
try {
await syncSetting(isBg);
} catch (err) {
console.log("[sync setting]", err);
}
@@ -65,7 +69,6 @@ export const syncSetting = async (isBg = false) => {
* @returns
*/
export const syncRules = async (isBg = false) => {
try {
const { syncUrl, syncKey, rulesUpdateAt } = await syncOpt.load();
if (!syncUrl || !syncKey) {
return;
@@ -92,6 +95,11 @@ export const syncRules = async (isBg = false) => {
} else {
await syncOpt.update({ rulesSyncAt: res.updateAt });
}
};
export const trySyncRules = async (isBg = false) => {
try {
await syncRules(isBg);
} catch (err) {
console.log("[sync user rules]", err);
}
@@ -121,3 +129,8 @@ export const syncAll = async (isBg = false) => {
await syncSetting(isBg);
await syncRules(isBg);
};
export const trySyncAll = async (isBg = false) => {
await trySyncSetting(isBg);
await trySyncRules(isBg);
};

View File

@@ -7,17 +7,39 @@ import {
MSG_TRANS_CURRULE,
OPT_STYLE_DASHLINE,
OPT_STYLE_FUZZY,
SHADOW_KEY,
} from "../config";
import { queryEls } from ".";
import Content from "../views/Content";
import { fetchUpdate, fetchClear } from "./fetch";
import { debounce } from "./utils";
/**
* 翻译类
*/
export class Translator {
_rule = {};
_minLength = 0;
_maxLength = 0;
_skipNodeNames = [
APP_LCNAME,
"style",
"svg",
"img",
"audio",
"video",
"textarea",
"input",
"button",
"select",
"option",
"head",
"script",
"iframe",
];
_rootNodes = new Set();
_tranNodes = new Map();
// 显示
_interseObserver = new IntersectionObserver(
(intersections) => {
intersections.forEach((intersection) => {
@@ -32,22 +54,46 @@ export class Translator {
}
);
// 变化
_mutaObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
try {
queryEls(this.rule.selector, node).forEach((el) => {
this._interseObserver.observe(el);
if (
!this._skipNodeNames.includes(mutation.target.localName) &&
mutation.addedNodes.length > 0
) {
const nodes = Array.from(mutation.addedNodes).filter((node) => {
if (
this._skipNodeNames.includes(node.localName) ||
node.id === APP_LCNAME
) {
return false;
}
return true;
});
} catch (err) {
//
if (nodes.length > 0) {
// const rootNode = mutation.target.getRootNode();
// todo
this._reTranslate();
}
}
});
});
});
constructor(rule, { fetchInterval, fetchLimit }) {
// 插入 shadowroot
_overrideAttachShadow = () => {
const _this = this;
const _attachShadow = HTMLElement.prototype.attachShadow;
HTMLElement.prototype.attachShadow = function () {
_this._reTranslate();
return _attachShadow.apply(this, arguments);
};
};
constructor(rule, { fetchInterval, fetchLimit, minLength, maxLength }) {
fetchUpdate(fetchInterval, fetchLimit);
this._overrideAttachShadow();
this._minLength = minLength ?? TRANS_MIN_LENGTH;
this._maxLength = maxLength ?? TRANS_MAX_LENGTH;
this.rule = rule;
if (rule.transOpen === "true") {
this._register();
@@ -96,16 +142,80 @@ export class Translator {
this.rule = { ...this.rule, textStyle };
};
_querySelectorAll = (selector, node) => {
try {
return Array.from(node.querySelectorAll(selector));
} catch (err) {
console.log(`[querySelectorAll err]: ${selector}`);
}
return [];
};
_queryFilter = (selector, rootNode) => {
return this._querySelectorAll(selector, rootNode).filter(
(node) => this._queryFilter(selector, node).length === 0
);
};
_queryNodes = (rootNode = document) => {
// const childRoots = Array.from(rootNode.querySelectorAll("*"))
// .map((item) => item.shadowRoot)
// .filter(Boolean);
// const childNodes = childRoots.map((item) => this._queryNodes(item));
// const nodes = Array.from(rootNode.querySelectorAll(this.rule.selector));
// return nodes.concat(childNodes).flat();
this._rootNodes.add(rootNode);
this._rule.selector
.split(";")
.map((item) => item.trim())
.filter(Boolean)
.forEach((selector) => {
if (selector.includes(SHADOW_KEY)) {
const [outSelector, inSelector] = selector
.split(SHADOW_KEY)
.map((item) => item.trim());
if (outSelector && inSelector) {
const outNodes = this._querySelectorAll(outSelector, rootNode);
outNodes.forEach((outNode) => {
if (outNode.shadowRoot) {
this._rootNodes.add(outNode.shadowRoot);
this._queryFilter(inSelector, outNode.shadowRoot).forEach(
(item) => {
if (!this._tranNodes.has(item)) {
this._tranNodes.set(item, "");
}
}
);
}
});
}
} else {
this._queryFilter(selector, rootNode).forEach((item) => {
if (!this._tranNodes.has(item)) {
this._tranNodes.set(item, "");
}
});
}
});
};
_register = () => {
// 监听节点变化
this._mutaObserver.observe(document, {
// 搜索节点
this._queryNodes();
this._rootNodes.forEach((node) => {
// 监听节点变化;
this._mutaObserver.observe(node, {
childList: true,
subtree: true,
// characterData: true,
});
});
this._tranNodes.forEach((_, node) => {
// 监听节点显示
queryEls(this.rule.selector).forEach((el) => {
this._interseObserver.observe(el);
this._interseObserver.observe(node);
});
};
@@ -114,45 +224,67 @@ export class Translator {
this._mutaObserver.disconnect();
// 解除节点显示监听
queryEls(this.rule.selector).forEach((el) =>
this._interseObserver.unobserve(el)
);
this._interseObserver.disconnect();
// 移除已插入元素
queryEls(APP_LCNAME).forEach((el) => el.remove());
this._tranNodes.forEach((_, node) => {
node.querySelector(APP_LCNAME)?.remove();
});
// 清空节点集合
this._rootNodes.clear();
this._tranNodes.clear();
// 清空任务池
fetchClear();
};
_render = (el) => {
// 含子元素
if (el.querySelector(this.rule.selector)) {
return;
_reTranslate = debounce(() => {
if (this._rule.transOpen === "true") {
this._register();
}
}, 500);
_render = (el) => {
let traEl = el.querySelector(APP_LCNAME);
// 已翻译
if (el.querySelector(APP_LCNAME)) {
if (traEl) {
const preText = this._tranNodes.get(el);
const curText = el.innerText.trim();
// const traText = traEl.innerText.trim();
// todo
// 1. traText when loading
// 2. replace startsWith
if (curText.startsWith(preText)) {
return;
}
// 太长或太短
traEl.remove();
}
const q = el.innerText.trim();
if (!q || q.length < TRANS_MIN_LENGTH || q.length > TRANS_MAX_LENGTH) {
this._tranNodes.set(el, q);
// 太长或太短
if (!q || q.length < this._minLength || q.length > this._maxLength) {
return;
}
// console.log("---> ", q);
const span = document.createElement(APP_LCNAME);
span.style.visibility = "visible";
el.appendChild(span);
traEl = document.createElement(APP_LCNAME);
traEl.style.visibility = "visible";
el.appendChild(traEl);
el.style.cssText +=
"-webkit-line-clamp: unset; max-height: none; height: auto;";
if (el.parentElement) {
el.parentElement.style.cssText +=
"-webkit-line-clamp: unset; max-height: none; height: auto;";
}
const root = createRoot(span);
const root = createRoot(traEl);
root.render(<Content q={q} translator={this} />);
};
}

View File

@@ -6,11 +6,14 @@ import { CacheProvider } from "@emotion/react";
import { getSetting, getRules, matchRule, getFab } from "./libs";
import { Translator } from "./libs/translator";
import { trySyncAllSubRules } from "./libs/rules";
import { isGm } from "./libs/browser";
import { MSG_TRANS_TOGGLE, MSG_TRANS_PUTRULE } from "./config";
import { isIframe } from "./libs/iframe";
/**
* 入口函数
*/
(async () => {
const init = async () => {
// 设置页面
if (
document.location.href.includes(process.env.REACT_APP_OPTIONSPAGE_DEV) ||
@@ -22,23 +25,36 @@ import { trySyncAllSubRules } from "./libs/rules";
return;
}
// skip iframe
if (window.self !== window.top) {
return;
}
// 翻译页面
const href = isIframe ? document.referrer : document.location.href;
const setting = await getSetting();
const rules = await getRules();
const rule = await matchRule(rules, document.location.href, setting);
const rule = await matchRule(rules, href, setting);
const translator = new Translator(rule, setting);
if (isIframe) {
// iframe
window.addEventListener("message", (e) => {
const action = e?.data?.action;
switch (action) {
case MSG_TRANS_TOGGLE:
translator.toggle();
break;
case MSG_TRANS_PUTRULE:
translator.updateRule(e.data.args || {});
break;
default:
}
});
return;
}
// 浮球按钮
const fab = await getFab();
const $action = document.createElement("div");
$action.setAttribute("id", "kiss-translator");
document.body.parentElement.appendChild($action);
const shadowContainer = $action.attachShadow({ mode: "open" });
const shadowContainer = $action.attachShadow({ mode: "closed" });
const emotionRoot = document.createElement("style");
const shadowRootElement = document.createElement("div");
shadowContainer.appendChild(emotionRoot);
@@ -57,6 +73,8 @@ import { trySyncAllSubRules } from "./libs/rules";
);
// 注册菜单
if (isGm) {
try {
GM.registerMenuCommand(
"Toggle Translate",
(event) => {
@@ -71,7 +89,22 @@ import { trySyncAllSubRules } from "./libs/rules";
},
"C"
);
} catch (err) {
console.log("[registerMenuCommand]", err);
}
}
// 同步订阅规则
trySyncAllSubRules(setting);
};
(async () => {
try {
await init();
} catch (err) {
const $err = document.createElement("div");
$err.innerText = `KISS-Translator: ${err.message}`;
$err.style.cssText = "background:red; color:#fff; z-index:10000;";
document.body.prepend($err);
}
})();

View File

@@ -163,7 +163,7 @@ export default function Draggable({
const opacity = useMemo(() => {
if (snapEdge) {
return position.hide ? 0.1 : 1;
return position.hide ? 0.2 : 1;
}
return origin ? 0.8 : 1;
}, [origin, snapEdge, position.hide]);

View File

@@ -514,7 +514,7 @@ function UserRules() {
return (
<Stack spacing={3}>
<Stack direction="row" spacing={2} useFlexGap flexWrap="wrap">
<Stack direction="row" alignItems="center" spacing={2} useFlexGap flexWrap="wrap">
<Button
size="small"
variant="contained"
@@ -638,6 +638,7 @@ function SubRulesEdit({ subrules }) {
const [inputText, setInputText] = useState("");
const [inputError, setInputError] = useState("");
const [showInput, setShowInput] = useState(false);
const [loading, setLoading] = useState(false);
const handleCancel = (e) => {
e.preventDefault();
@@ -661,6 +662,7 @@ function SubRulesEdit({ subrules }) {
}
try {
setLoading(true);
const rules = await syncSubRules(url);
if (rules.length === 0) {
throw new Error("empty rules");
@@ -671,6 +673,8 @@ function SubRulesEdit({ subrules }) {
} catch (err) {
console.log("[fetch rules]", err);
setInputError(i18n("error_fetch_url"));
} finally {
setLoading(false);
}
};
@@ -713,7 +717,12 @@ function SubRulesEdit({ subrules }) {
/>
<Stack direction="row" alignItems="center" spacing={2}>
<Button size="small" variant="contained" onClick={handleSave}>
<Button
size="small"
variant="contained"
onClick={handleSave}
disabled={loading}
>
{i18n("save")}
</Button>
<Button size="small" variant="outlined" onClick={handleCancel}>

View File

@@ -28,6 +28,12 @@ export default function Settings() {
case "fetchInterval":
value = limitNumber(value, 0, 5000);
break;
case "minLength":
value = limitNumber(value, 1, 100);
break;
case "maxLength":
value = limitNumber(value, 100, 10000);
break;
default:
}
updateSetting({
@@ -46,6 +52,8 @@ export default function Settings() {
googleUrl,
fetchLimit,
fetchInterval,
minLength,
maxLength,
openaiUrl,
openaiKey,
openaiModel,
@@ -90,6 +98,24 @@ export default function Settings() {
onChange={handleChange}
/>
<TextField
size="small"
label={i18n("min_translate_length")}
type="number"
name="minLength"
defaultValue={minLength}
onChange={handleChange}
/>
<TextField
size="small"
label={i18n("max_translate_length")}
type="number"
name="maxLength"
defaultValue={maxLength}
onChange={handleChange}
/>
<FormControl size="small">
<InputLabel>{i18n("clear_cache")}</InputLabel>
<Select

View File

@@ -7,12 +7,18 @@ import Alert from "@mui/material/Alert";
import Link from "@mui/material/Link";
import { URL_KISS_WORKER } from "../../config";
import { debounce } from "../../libs/utils";
import { useMemo } from "react";
import { useMemo, useState } from "react";
import { syncAll } from "../../libs/sync";
import Button from "@mui/material/Button";
import { useAlert } from "../../hooks/Alert";
import SyncIcon from "@mui/icons-material/Sync";
import CircularProgress from "@mui/material/CircularProgress";
export default function SyncSetting() {
const i18n = useI18n();
const sync = useSync();
const alert = useAlert();
const [loading, setLoading] = useState(false);
const handleChange = useMemo(
() =>
@@ -22,11 +28,25 @@ export default function SyncSetting() {
await sync.update({
[name]: value,
});
await syncAll();
}, 1000),
// trySyncAll();
}, 500),
[sync]
);
const handleSyncTest = async (e) => {
e.preventDefault();
try {
setLoading(true);
await syncAll();
alert.success(i18n("data_sync_success"));
} catch (err) {
console.log("[sync all]", err);
alert.error(i18n("data_sync_error"));
} finally {
setLoading(false);
}
};
if (!sync.opt) {
return;
}
@@ -57,6 +77,19 @@ export default function SyncSetting() {
defaultValue={syncKey}
onChange={handleChange}
/>
<Stack direction="row" alignItems="center" spacing={2} useFlexGap flexWrap="wrap">
<Button
size="small"
variant="contained"
disabled={!syncUrl || !syncKey || loading}
onClick={handleSyncTest}
startIcon={<SyncIcon />}
>
{i18n("data_sync_test")}
</Button>
{loading && <CircularProgress size={16} />}
</Stack>
</Stack>
</Box>
);

View File

@@ -10,7 +10,7 @@ import { useEffect, useState } from "react";
import { isGm } from "../../libs/browser";
import { sleep } from "../../libs/utils";
import CircularProgress from "@mui/material/CircularProgress";
import { syncAll } from "../../libs/sync";
import { trySyncAll } from "../../libs/sync";
import { AlertProvider } from "../../hooks/Alert";
export default function Options() {
@@ -38,7 +38,7 @@ export default function Options() {
}
// 同步数据
syncAll();
trySyncAll();
})();
}, []);

View File

@@ -18,6 +18,7 @@ import {
OPT_LANGS_TO,
OPT_STYLE_ALL,
} from "../../config";
import { sendIframeMsg } from "../../libs/iframe";
export default function Popup({ setShowPopup, translator: tran }) {
const i18n = useI18n();
@@ -40,6 +41,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
await sendTabMsg(MSG_TRANS_TOGGLE);
} else {
tran.toggle();
sendIframeMsg(MSG_TRANS_TOGGLE);
}
} catch (err) {
console.log("[toggle trans]", err);
@@ -55,6 +57,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
await sendTabMsg(MSG_TRANS_PUTRULE, { [name]: value });
} else {
tran.updateRule({ [name]: value });
sendIframeMsg(MSG_TRANS_PUTRULE, { [name]: value });
}
} catch (err) {
console.log("[update rule]", err);
@@ -101,7 +104,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
onChange={handleTransToggle}
/>
}
label={i18n("translate")}
label={i18n("translate_alt")}
/>
<TextField
@@ -158,7 +161,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
size="small"
value={textStyle}
name="textStyle"
label={i18n("text_style")}
label={i18n("text_style_alt")}
onChange={handleChange}
>
{OPT_STYLE_ALL.map((item) => (