Compare commits

...

86 Commits

Author SHA1 Message Date
Gabe Yuan
2f5d875c47 v1.8.5 2024-04-02 17:01:34 +08:00
Gabe Yuan
fdb2ddc5f7 fix: rules 2024-04-01 12:35:54 +08:00
Gabe Yuan
7a12c5315a feat: close tranbox when click away 2024-04-01 12:25:59 +08:00
Gabe Yuan
60d788288d feat: add more custom apis 2024-04-01 11:50:29 +08:00
Gabe Yuan
dc3c510d57 fix: update observer callback 2024-03-27 14:24:41 +08:00
Gabe Yuan
ec6a49f01e fix: update readme 2024-03-26 17:50:56 +08:00
Gabe Yuan
2b9bfbc20d feat: csp list 2024-03-26 12:42:39 +08:00
Gabe Yuan
06a51df834 feat: csp list 2024-03-26 12:05:35 +08:00
Gabe Yuan
6fa183dc56 feat: csp list 2024-03-26 12:00:09 +08:00
Gabe Yuan
b3cb4049ed fix: tranbox input onfocus 2024-03-25 22:46:02 +08:00
Gabe Yuan
602b51b1f5 fix: dict audio 2024-03-25 21:00:39 +08:00
Gabe Yuan
a83039577c feat: word pronunciation supported 2024-03-25 18:14:12 +08:00
Gabe Yuan
1c77a289a6 fix: upgrade dependencies 2024-03-21 23:19:15 +08:00
Gabe Yuan
6278b9124d v1.8.4 2024-03-21 16:15:45 +08:00
Gabe Yuan
f94cafdbcc fix: i18n text 2024-03-21 16:12:38 +08:00
Gabe Yuan
e13da2caba fix: i18n text 2024-03-21 16:07:56 +08:00
Gabe Yuan
d833fa8dfd fix: build cmd 2024-03-21 15:48:54 +08:00
Gabe Yuan
c921cc59b9 feat: add pack cmd 2024-03-21 15:38:28 +08:00
Gabe Yuan
7a2c594324 fix: setting ui 2024-03-21 15:21:38 +08:00
Gabe Yuan
0eeb9c2fbf feat: move fetch setting to apis 2024-03-21 15:07:50 +08:00
Gabe Yuan
ac921cd5a0 feat: move fixer to rules 2024-03-21 11:40:47 +08:00
Gabe Yuan
3ea14c1687 fix: webfix 2024-03-21 10:15:23 +08:00
Gabe Yuan
6e927473b9 feat: add log function 2024-03-19 18:07:18 +08:00
Gabe Yuan
1d9e9c1b7d fix: change runtime.onMessage to async fn 2024-03-19 17:28:07 +08:00
Gabe Yuan
54a6189b0c fix: sendResponse ok 2024-03-19 15:37:23 +08:00
Gabe Yuan
85aa9f39c2 fix: add timeout for fetch 2024-03-19 14:27:26 +08:00
Gabe Yuan
bda83ce76e fix: add timeout for fetch 2024-03-19 14:25:37 +08:00
Gabe Yuan
9ee4c20250 fix: sync rules 2024-03-19 14:08:23 +08:00
Gabe Yuan
fbc70e43e3 feat: add baidu suggest 2024-03-19 11:48:30 +08:00
Gabe Yuan
bc4b4a2171 fix: i118n text 2024-03-19 10:52:32 +08:00
Gabe Yuan
96f9bf6f6f fix: replace default transtag to span 2024-03-19 10:00:00 +08:00
Gabe Yuan
9f0986536a feat: wrap trems by <i> tag 2024-03-19 09:55:56 +08:00
Gabe Yuan
f668aa7acd fix: hide transbox 2024-03-18 17:55:32 +08:00
Gabe Yuan
fc50f4784a fix: hide transbox 2024-03-18 17:52:13 +08:00
Gabe Yuan
1fa58cad31 fix: rules 2024-03-18 11:02:33 +08:00
Gabe Yuan
469e62557c fix: icon & text 2024-03-18 10:48:38 +08:00
Gabe Yuan
75830aaea7 feat: add translate interval setting 2024-03-17 12:09:32 +08:00
Gabe Yuan
61ef5df559 fix: rules 2024-03-17 11:35:43 +08:00
Gabe Yuan
14b5ba9c4c feat: move settings to rule 2024-03-16 23:37:27 +08:00
Gabe Yuan
9e9c56a3b4 fix: inject js/css 2024-03-15 17:38:24 +08:00
Gabe Yuan
3a79f55614 fix: update rules 2024-03-15 16:59:53 +08:00
Gabe Yuan
af25ee5c11 fix: showMore 2024-03-15 16:19:08 +08:00
Gabe Yuan
6dd581d5e2 fix: default trans tag 2024-03-15 16:00:20 +08:00
Gabe Yuan
746ec019c4 fix: inject js/css 2024-03-15 15:47:57 +08:00
Gabe Yuan
2b70f28b0b fix: help text 2024-03-15 10:47:35 +08:00
Gabe Yuan
45127646e8 fix: inject js/css 2024-03-15 10:35:30 +08:00
Gabe Yuan
83e9c1dd97 feat: inject user js/css 2024-03-14 18:08:02 +08:00
Gabe Yuan
2eabb7d5ac feat: inject user js/css 2024-03-14 18:06:28 +08:00
Gabe Yuan
9d4c596b4b fix: getCurTab 2024-03-14 16:28:32 +08:00
Gabe Yuan
cc38ab6c45 fix: styledSpan & transOnly 2024-03-14 16:26:17 +08:00
Gabe Yuan
586fa09c7b fix: clean content.html 2024-03-14 11:54:34 +08:00
Gabe Yuan
0c45bc5ea8 fix: clean comment code 2024-03-14 11:50:31 +08:00
Gabe Yuan
9d9c0633f0 feat: transTag && transOnly 2024-03-13 16:35:40 +08:00
Gabe Yuan
47f9635b10 v1.8.3 2024-02-26 16:42:36 +08:00
Gabe Yuan
68088f5e17 fix: remove excess line breaks 2024-02-26 16:35:29 +08:00
Gabe Yuan
77a37cc6df fix: remove excess line breaks 2024-02-26 16:34:53 +08:00
Gabe Yuan
420e59bf81 fix: translate text 2024-02-22 22:57:34 +08:00
Gabe Yuan
dbc5135301 fix: update default blacklist 2024-02-22 17:44:17 +08:00
Gabe Yuan
8c7d6bb552 fix: remove postMessage from iframe 2024-02-22 17:26:22 +08:00
Gabe Yuan
2b5c1952c0 fix: update default blacklist 2024-02-22 11:45:41 +08:00
Gabe Yuan
85a82618e5 fix: contextMenus text 2024-02-21 17:01:11 +08:00
Gabe Yuan
0280ac34c3 fix: dict voice not exist 2024-02-21 15:47:14 +08:00
Gabe Yuan
439900154b fix: dict voice not exist 2024-02-21 15:41:27 +08:00
Gabe Yuan
4a6e902684 fix: update readme 2024-02-06 10:22:56 +08:00
Gabe Yuan
71bbd2e54a v1.8.1 2024-02-06 10:09:33 +08:00
Gabe Yuan
3083d8e147 feat: context menu type 2024-02-05 11:28:34 +08:00
Gabe Yuan
e74883e9c2 fix: contextMenus: duplicate id err 2024-02-05 10:51:42 +08:00
Gabe Yuan
0816a9d167 fix: add BLOCKQUOTE to webfix 2024-02-05 10:02:30 +08:00
Gabe Yuan
4b3e91fa84 v1.8.1 2024-02-02 15:45:33 +08:00
Gabe Yuan
0973a0b60e fix: some js syntax 2024-02-02 15:44:44 +08:00
Gabe Yuan
de5f61126d fix: terms hepler text 2024-02-02 12:35:56 +08:00
Gabe Yuan
0c20ca761f fix: update toggle_translate CN text 2024-02-02 12:22:08 +08:00
Gabe Yuan
4bce56207e fix: optimize terms function 2024-02-02 12:10:27 +08:00
Gabe Yuan
dca54e0033 feat: setting: translate page title 2024-02-02 11:20:39 +08:00
Gabe Yuan
309646bf1d feat: setting: translate page title 2024-02-02 11:13:41 +08:00
Gabe Yuan
18b9961b39 fix: try add context menux on startup 2024-02-02 10:49:15 +08:00
Gabe Yuan
1e51ff17f2 v1.8.0 2024-01-22 13:19:37 +08:00
Gabe Yuan
63b5f707e2 fix: update ui when shortcut changed 2024-01-22 13:11:02 +08:00
Gabe Yuan
30efb6ee7a fix: title translate 2024-01-19 21:03:51 +08:00
Gabe Yuan
61b017618a feat: supported translation all when page opened 2024-01-19 17:55:18 +08:00
Gabe Yuan
1e0397adc9 feat: translate page title 2024-01-19 17:18:05 +08:00
Gabe Yuan
48b34bf95f fix: save new rule with hostname 2024-01-19 16:13:46 +08:00
Gabe Yuan
d5fc69e210 feat: support custom terms 2024-01-19 16:02:53 +08:00
Gabe Yuan
59f9dd697f fix: update baidu translate api 2024-01-18 15:26:37 +08:00
Gabe Yuan
c9d72323f1 keep selector support for sub-element 2024-01-12 16:04:34 +08:00
Gabe Yuan
e87f7f3abe fix: help text 2024-01-12 09:42:49 +08:00
63 changed files with 2380 additions and 1656 deletions

4
.env
View File

@@ -2,7 +2,7 @@ GENERATE_SOURCEMAP=false
REACT_APP_NAME=KISS Translator REACT_APP_NAME=KISS Translator
REACT_APP_NAME_CN=简约翻译 REACT_APP_NAME_CN=简约翻译
REACT_APP_VERSION=1.7.16 REACT_APP_VERSION=1.8.5
REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator
@@ -17,8 +17,6 @@ REACT_APP_RULESURL=https://fishjar.github.io/kiss-rules/kiss-rules.json
REACT_APP_RULESURL_ON=https://fishjar.github.io/kiss-rules/kiss-rules-on.json REACT_APP_RULESURL_ON=https://fishjar.github.io/kiss-rules/kiss-rules-on.json
REACT_APP_RULESURL_OFF=https://fishjar.github.io/kiss-rules/kiss-rules-off.json REACT_APP_RULESURL_OFF=https://fishjar.github.io/kiss-rules/kiss-rules-off.json
REACT_APP_WEBFIXURL=https://fishjar.github.io/kiss-rules/kiss-webfix.json
REACT_APP_VERSIONFILE=https://fishjar.github.io/kiss-translator/version.txt REACT_APP_VERSIONFILE=https://fishjar.github.io/kiss-translator/version.txt
REACT_APP_VERSIONFILE2=https://kiss-translator.rayjar.com/version.txt REACT_APP_VERSIONFILE2=https://kiss-translator.rayjar.com/version.txt

View File

@@ -9,7 +9,7 @@ A simple, open source [bilingual translation extension & Greasemonkey script](ht
- [x] Keep it simple, smart - [x] Keep it simple, smart
- [x] Open source - [x] Open source
- [x] Adapt to common browsers - [x] Adapt to common browsers
- [x] Chrome/Edge/Firefox/Kiwi - [x] Chrome/Edge/Firefox/Kiwi/Orion
- [ ] Safari - [ ] Safari
- [x] Supports multiple translation services - [x] Supports multiple translation services
- [x] Google/Microsoft/DeepL/OpenAI/Gemini/CloudflareAI/Baidu/Tencent - [x] Google/Microsoft/DeepL/OpenAI/Gemini/CloudflareAI/Baidu/Tencent
@@ -26,6 +26,7 @@ A simple, open source [bilingual translation extension & Greasemonkey script](ht
- [x] WebDAV - [x] WebDAV
- [x] Custom translation rules - [x] Custom translation rules
- [x] Rule subscription/rule sharing - [x] Rule subscription/rule sharing
- [x] Customized terminology
- [x] Custom translation style - [x] Custom translation style
- [x] Custom shortcut keys - [x] Custom shortcut keys
- `Alt+Q` Toggle Translation - `Alt+Q` Toggle Translation
@@ -44,12 +45,14 @@ A simple, open source [bilingual translation extension & Greasemonkey script](ht
- [x] Browser extension - [x] Browser extension
- [x] Chrome [Installation address](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof?hl=zh-CN) - [x] Chrome [Installation address](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof?hl=zh-CN)
- [x] Kiwi (Android)
- [x] Orion (iOS)
- [x] Edge [Installation address](https://microsoftedge.microsoft.com/addons/detail/%E7%AE%80%E7%BA%A6%E7%BF%BB%E8%AF%91/jemckldkclkinpjighnoilpbldbdmmlh?hl=zh-CN) - [x] Edge [Installation address](https://microsoftedge.microsoft.com/addons/detail/%E7%AE%80%E7%BA%A6%E7%BF%BB%E8%AF%91/jemckldkclkinpjighnoilpbldbdmmlh?hl=zh-CN)
- [x] Firefox [Installation address](https://addons.mozilla.org/zh-CN/firefox/addon/kiss-translator/) - [x] Firefox [Installation address](https://addons.mozilla.org/zh-CN/firefox/addon/kiss-translator/)
- [ ] Safari - [ ] Safari
- [x] GreaseMonkey Script - [x] GreaseMonkey Script
- [x] Chrome/Edge/Firefox ([Tampermonkey](https://www.tampermonkey.net/)/[Violentmonkey](https://violentmonkey.github.io/)) [Installation link](https://fishjar.github.io/kiss-translator/kiss-translator.user.js) - [x] Chrome/Edge/Firefox ([Tampermonkey](https://www.tampermonkey.net/)/[Violentmonkey](https://violentmonkey.github.io/)) [Installation link](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)
- Greasy Fork [Installation address](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator) - [Greasy Fork](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator)
- [x] iOS Safari ([Userscripts Safari](https://github.com/quoid/userscripts)) [Installation link](https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js) - [x] iOS Safari ([Userscripts Safari](https://github.com/quoid/userscripts)) [Installation link](https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js)
## Associated Projects ## Associated Projects
@@ -61,9 +64,6 @@ A simple, open source [bilingual translation extension & Greasemonkey script](ht
- Community subscription rules: [https://github.com/fishjar/kiss-rules](https://github.com/fishjar/kiss-rules) - Community subscription rules: [https://github.com/fishjar/kiss-rules](https://github.com/fishjar/kiss-rules)
- Provides the latest and most complete list of subscription rules maintained by the community. - Provides the latest and most complete list of subscription rules maintained by the community.
- Help with rules-related issues. - Help with rules-related issues.
- Web page correction script: [https://github.com/fishjar/kiss-webfixer](https://github.com/fishjar/kiss-webfixer)
- Fixed scripts for some special sites.
- So that the translation software can get better display effect.
- Translation interface agent: [https://github.com/fishjar/kiss-proxy](https://github.com/fishjar/kiss-proxy) - Translation interface agent: [https://github.com/fishjar/kiss-proxy](https://github.com/fishjar/kiss-proxy)
- If you encounter network problems when accessing a certain translation interface, this proxy service may help you. - If you encounter network problems when accessing a certain translation interface, this proxy service may help you.
- Deploy and manage by yourself. - Deploy and manage by yourself.

View File

@@ -9,7 +9,7 @@
- [x] 保持简约 - [x] 保持简约
- [x] 开放源代码 - [x] 开放源代码
- [x] 适配常见浏览器 - [x] 适配常见浏览器
- [x] Chrome/Edge/Firefox/Kiwi - [x] Chrome/Edge/Firefox/Kiwi/Orion
- [ ] Safari - [ ] Safari
- [x] 支持多种翻译服务 - [x] 支持多种翻译服务
- [x] Google/Microsoft/DeepL/OpenAI/Gemini/CloudflareAI/Baidu/Tencent - [x] Google/Microsoft/DeepL/OpenAI/Gemini/CloudflareAI/Baidu/Tencent
@@ -26,6 +26,7 @@
- [x] WebDAV - [x] WebDAV
- [x] 自定义翻译规则 - [x] 自定义翻译规则
- [x] 规则订阅/规则分享 - [x] 规则订阅/规则分享
- [x] 自定义专业术语
- [x] 自定义译文样式 - [x] 自定义译文样式
- [x] 自定义快捷键 - [x] 自定义快捷键
- `Alt+Q` 开启翻译 - `Alt+Q` 开启翻译
@@ -44,12 +45,14 @@
- [x] 浏览器扩展 - [x] 浏览器扩展
- [x] Chrome [安装地址](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof?hl=zh-CN) - [x] Chrome [安装地址](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof?hl=zh-CN)
- [x] Kiwi (Android)
- [x] Orion (iOS)
- [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] 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/) - [x] Firefox [安装地址](https://addons.mozilla.org/zh-CN/firefox/addon/kiss-translator/)
- [ ] Safari - [ ] Safari
- [x] 油猴脚本 - [x] 油猴脚本
- [x] Chrome/Edge/Firefox ([Tampermonkey](https://www.tampermonkey.net/)/[Violentmonkey](https://violentmonkey.github.io/)) [安装链接](https://fishjar.github.io/kiss-translator/kiss-translator.user.js) - [x] Chrome/Edge/Firefox ([Tampermonkey](https://www.tampermonkey.net/)/[Violentmonkey](https://violentmonkey.github.io/)) [安装链接](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)
- Greasy Fork [安装地址](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator) - [Greasy Fork](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator)
- [x] iOS Safari ([Userscripts Safari](https://github.com/quoid/userscripts)) [安装链接](https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js) - [x] iOS Safari ([Userscripts Safari](https://github.com/quoid/userscripts)) [安装链接](https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js)
## 关联项目 ## 关联项目
@@ -61,9 +64,6 @@
- 社区订阅规则: [https://github.com/fishjar/kiss-rules](https://github.com/fishjar/kiss-rules) - 社区订阅规则: [https://github.com/fishjar/kiss-rules](https://github.com/fishjar/kiss-rules)
- 提供社区维护的,最新最全的订阅规则列表。 - 提供社区维护的,最新最全的订阅规则列表。
- 求助规则相关的问题。 - 求助规则相关的问题。
- 网页修正脚本: [https://github.com/fishjar/kiss-webfixer](https://github.com/fishjar/kiss-webfixer)
- 针对一些特殊网站的修正脚本。
- 以便翻译软件得到更好的展示效果。
- 翻译接口代理: [https://github.com/fishjar/kiss-proxy](https://github.com/fishjar/kiss-proxy) - 翻译接口代理: [https://github.com/fishjar/kiss-proxy](https://github.com/fishjar/kiss-proxy)
- 如果访问某个翻译接口遇到网络问题,这个代理服务也许可以帮到你。 - 如果访问某个翻译接口遇到网络问题,这个代理服务也许可以帮到你。
- 自己部署,自己管理。 - 自己部署,自己管理。

View File

@@ -1,20 +1,20 @@
{ {
"name": "kiss-translator", "name": "kiss-translator",
"description": "A minimalist bilingual translation Extension & Greasemonkey Script", "description": "A minimalist bilingual translation Extension & Greasemonkey Script",
"version": "1.7.16", "version": "1.8.5",
"author": "Gabe<yugang2002@gmail.com>", "author": "Gabe<yugang2002@gmail.com>",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@emotion/cache": "^11.11.0", "@emotion/cache": "^11.11.0",
"@emotion/react": "^11.11.1", "@emotion/react": "^11.11.1",
"@emotion/styled": "^11.10.8", "@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.11.11", "@mui/icons-material": "^5.14.9",
"@mui/material": "^5.11.12", "@mui/material": "^5.14.10",
"query-string": "^8.1.0", "query-string": "^8.1.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-markdown": "^8.0.7", "react-markdown": "^8.0.7",
"react-router-dom": "^6.10.0", "react-router-dom": "^6.16.0",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"webdav": "^5.3.0", "webdav": "^5.3.0",
"webextension-polyfill": "^0.10.0" "webextension-polyfill": "^0.10.0"
@@ -22,14 +22,15 @@
"scripts": { "scripts": {
"start": "REACT_APP_CLIENT=web react-app-rewired start", "start": "REACT_APP_CLIENT=web react-app-rewired start",
"start:userscript": "REACT_APP_CLIENT=userscript react-app-rewired start", "start:userscript": "REACT_APP_CLIENT=userscript react-app-rewired start",
"build:chrome": "rm -rf build/chrome && BUILD_PATH=./build/chrome REACT_APP_CLIENT=chrome react-app-rewired build", "build:chrome": "rm -rf build/chrome && BUILD_PATH=./build/chrome REACT_APP_CLIENT=chrome react-app-rewired build && rm build/chrome/content.html",
"build:edge": "rm -rf build/edge && cp -r build/chrome build/edge", "build:edge": "rm -rf build/edge && cp -r build/chrome build/edge",
"build:firefox": "rm -rf build/firefox && cp -r build/chrome build/firefox && cat ./build/firefox/manifest.firefox.json > ./build/firefox/manifest.json", "build:firefox": "rm -rf build/firefox && cp -r build/chrome build/firefox && cat ./build/firefox/manifest.firefox.json > ./build/firefox/manifest.json && rm build/*/manifest.firefox.json",
"build:web": "rm -rf build/web && BUILD_PATH=./build/web REACT_APP_CLIENT=userscript react-app-rewired build", "build:web": "rm -rf build/web && BUILD_PATH=./build/web REACT_APP_CLIENT=userscript react-app-rewired build",
"build:userscript-ios": "file1=build/web/kiss-translator.user.js file2=build/web/kiss-translator-ios-safari.user.js && cp $file1 $file2 && sed -i 's|// @grant unsafeWindow|// @inject-into content|g' $file2", "build:userscript-ios": "file1=build/web/kiss-translator.user.js file2=build/web/kiss-translator-ios-safari.user.js && cp $file1 $file2 && sed -i 's|// @grant unsafeWindow|// @inject-into content|g' $file2",
"build:userscript": "rm -rf build/userscript && mkdir build/userscript && cp build/web/*.user.js build/userscript/", "build:userscript": "rm -rf build/userscript && mkdir build/userscript && cp build/web/*.user.js build/userscript/",
"build:rules": "babel-node src/rules.js", "build:rules": "babel-node src/rules.js",
"build": "pnpm build:chrome && pnpm build:edge && pnpm build:firefox && pnpm build:web && pnpm build:userscript-ios && pnpm build:userscript && pnpm build:rules", "build": "pnpm build:chrome && pnpm build:edge && pnpm build:firefox && pnpm build:web && pnpm build:userscript-ios && pnpm build:userscript && pnpm build:rules",
"pack": "cd build && zip -r chrome.zip chrome && zip -r edge.zip edge && cd firefox && zip -r ../firefox.zip .",
"test": "react-app-rewired test", "test": "react-app-rewired test",
"eject": "react-scripts eject" "eject": "react-scripts eject"
}, },
@@ -57,10 +58,10 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.22.10", "@babel/core": "^7.22.20",
"@babel/node": "^7.22.10", "@babel/node": "^7.22.19",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-env": "^7.22.10", "@babel/preset-env": "^7.22.20",
"react-app-rewired": "^2.2.1" "react-app-rewired": "^2.2.1"
} }
} }

520
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,15 +6,15 @@
"message": "A simple bilingual translation extension & Greasemonkey script" "message": "A simple bilingual translation extension & Greasemonkey script"
}, },
"toggle_translate": { "toggle_translate": {
"message": "Toggle Translate (Alt+Q)" "message": "Toggle Translate"
}, },
"toggle_style": { "toggle_style": {
"message": "Toggle Style (Alt+C)" "message": "Toggle Style"
}, },
"open_options": { "open_options": {
"message": "Open Options (Alt+O)" "message": "Open Options"
}, },
"open_tranbox": { "open_tranbox": {
"message": "Translate Popup/Selected (Alt+S)" "message": "Translate Popup/Selected"
} }
} }

View File

@@ -6,15 +6,15 @@
"message": "一个简约的双语对照翻译扩展 & 油猴脚本" "message": "一个简约的双语对照翻译扩展 & 油猴脚本"
}, },
"toggle_translate": { "toggle_translate": {
"message": "开启翻译 (Alt+Q)" "message": "开启翻译"
}, },
"toggle_style": { "toggle_style": {
"message": "切换样式 (Alt+C)" "message": "切换样式"
}, },
"open_options": { "open_options": {
"message": "打开设置 (Alt+O)" "message": "打开设置"
}, },
"open_tranbox": { "open_tranbox": {
"message": "翻译弹窗/选中文字 (Alt+S)" "message": "翻译弹窗/选中文字"
} }
} }

View File

@@ -2,7 +2,7 @@
"manifest_version": 2, "manifest_version": 2,
"name": "__MSG_app_name__", "name": "__MSG_app_name__",
"description": "__MSG_app_description__", "description": "__MSG_app_description__",
"version": "1.7.16", "version": "1.8.5",
"default_locale": "en", "default_locale": "en",
"author": "Gabe<yugang2002@gmail.com>", "author": "Gabe<yugang2002@gmail.com>",
"homepage_url": "https://github.com/fishjar/kiss-translator", "homepage_url": "https://github.com/fishjar/kiss-translator",
@@ -44,7 +44,13 @@
"description": "__MSG_open_options__" "description": "__MSG_open_options__"
} }
}, },
"permissions": ["<all_urls>", "storage", "contextMenus"], "permissions": [
"<all_urls>",
"storage",
"contextMenus",
"scripting",
"declarativeNetRequest"
],
"icons": { "icons": {
"16": "images/logo16.png", "16": "images/logo16.png",
"32": "images/logo32.png", "32": "images/logo32.png",

View File

@@ -2,7 +2,7 @@
"manifest_version": 3, "manifest_version": 3,
"name": "__MSG_app_name__", "name": "__MSG_app_name__",
"description": "__MSG_app_description__", "description": "__MSG_app_description__",
"version": "1.7.16", "version": "1.8.5",
"default_locale": "en", "default_locale": "en",
"author": "Gabe<yugang2002@gmail.com>", "author": "Gabe<yugang2002@gmail.com>",
"homepage_url": "https://github.com/fishjar/kiss-translator", "homepage_url": "https://github.com/fishjar/kiss-translator",
@@ -45,7 +45,12 @@
"description": "__MSG_open_options__" "description": "__MSG_open_options__"
} }
}, },
"permissions": ["storage", "contextMenus"], "permissions": [
"storage",
"contextMenus",
"scripting",
"declarativeNetRequest"
],
"host_permissions": ["<all_urls>"], "host_permissions": ["<all_urls>"],
"icons": { "icons": {
"16": "images/logo16.png", "16": "images/logo16.png",

View File

@@ -1,6 +1,10 @@
import queryString from "query-string"; import queryString from "query-string";
import { getBdauth, setBdauth } from "../libs/storage"; import { getBdauth, setBdauth } from "../libs/storage";
import { URL_BAIDU_WEB, URL_BAIDU_TRAN } from "../config"; import {
URL_BAIDU_WEB,
URL_BAIDU_TRANSAPI_V2,
URL_BAIDU_TRANSAPI,
} from "../config";
import { fetchApi } from "../libs/fetch"; import { fetchApi } from "../libs/fetch";
/* eslint-disable */ /* eslint-disable */
@@ -203,7 +207,12 @@ const _bdAuth = () => {
const bdAuth = _bdAuth(); const bdAuth = _bdAuth();
export const genBaidu = async ({ text, from, to }) => { /**
* 失效作废
* @param {*} param0
* @returns
*/
export const genBaiduV2 = async ({ text, from, to }) => {
const { token, gtk } = await bdAuth(); const { token, gtk } = await bdAuth();
const sign = getSign(text, gtk); const sign = getSign(text, gtk);
const data = { const data = {
@@ -217,7 +226,7 @@ export const genBaidu = async ({ text, from, to }) => {
ts: Date.now(), ts: Date.now(),
}; };
const input = `${URL_BAIDU_TRAN}?from=${from}&to=${to}`; const input = `${URL_BAIDU_TRANSAPI_V2}?from=${from}&to=${to}`;
const init = { const init = {
headers: { headers: {
"content-type": "application/x-www-form-urlencoded; charset=UTF-8", "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
@@ -228,3 +237,22 @@ export const genBaidu = async ({ text, from, to }) => {
return [input, init]; return [input, init];
}; };
export const genBaidu = async ({ text, from, to }) => {
const data = {
from,
to,
query: text,
source: "txt",
};
const init = {
headers: {
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
},
method: "POST",
body: queryString.stringify(data),
};
return [URL_BAIDU_TRANSAPI, init];
};

View File

@@ -15,6 +15,8 @@ import {
URL_CACHE_TRAN, URL_CACHE_TRAN,
KV_SALT_SYNC, KV_SALT_SYNC,
URL_BAIDU_LANGDETECT, URL_BAIDU_LANGDETECT,
URL_BAIDU_SUGGEST,
URL_BAIDU_TTS,
OPT_LANGS_BAIDU, OPT_LANGS_BAIDU,
URL_TENCENT_TRANSMART, URL_TENCENT_TRANSMART,
OPT_LANGS_TENCENT, OPT_LANGS_TENCENT,
@@ -70,6 +72,44 @@ export const apiBaiduLangdetect = async (text) => {
return ""; return "";
}; };
/**
* 百度翻译建议
* @param {*} text
* @returns
*/
export const apiBaiduSuggest = async (text) => {
const res = await fetchPolyfill(URL_BAIDU_SUGGEST, {
headers: {
"Content-type": "application/json",
},
method: "POST",
body: JSON.stringify({
kw: text,
}),
useCache: true,
});
if (res.errno === 0) {
return res.data;
}
return [];
};
/**
* 百度语音
* @param {*} text
* @param {*} lan
* @param {*} spd
* @returns
*/
export const apiBaiduTTS = (text, lan = "uk", spd = 3) => {
const url = `${URL_BAIDU_TTS}?${queryString.stringify({ lan, text, spd })}`;
return fetchPolyfill(url, {
useCache: false, // 为避免缓存过快增长,禁用缓存语音数据
});
};
/** /**
* 腾讯语言识别 * 腾讯语言识别
* @param {*} text * @param {*} text
@@ -125,11 +165,14 @@ export const apiTranslate = async ({
return [trText, isSame]; return [trText, isSame];
} }
// 版本号一/二位升级,旧缓存失效
const [v1, v2] = process.env.REACT_APP_VERSION.split(".");
const cacheOpts = { const cacheOpts = {
translator, translator,
text, text,
fromLang, fromLang,
toLang, toLang,
version: [v1, v2].join("."),
}; };
const transOpts = { const transOpts = {
@@ -155,7 +198,9 @@ export const apiTranslate = async ({
isSame = to === res.src; isSame = to === res.src;
break; break;
case OPT_TRANS_MICROSOFT: case OPT_TRANS_MICROSOFT:
trText = res[0].translations.map((item) => item.text).join(" "); trText = res
.map((item) => item.translations.map((item) => item.text).join(" "))
.join(" ");
isSame = text === trText; isSame = text === trText;
break; break;
case OPT_TRANS_DEEPL: case OPT_TRANS_DEEPL:
@@ -171,19 +216,28 @@ export const apiTranslate = async ({
isSame = to === res.source_lang; isSame = to === res.source_lang;
break; break;
case OPT_TRANS_BAIDU: case OPT_TRANS_BAIDU:
trText = res.trans_result?.data.map((item) => item.dst).join(" "); // trText = res.trans_result?.data.map((item) => item.dst).join(" ");
isSame = res.trans_result?.to === res.trans_result?.from; // isSame = res.trans_result?.to === res.trans_result?.from;
if (res.type === 1) {
trText = Object.keys(JSON.parse(res.result).content[0].mean[0].cont)[0];
isSame = to === res.from;
} else if (res.type === 2) {
trText = res.data.map((item) => item.dst).join(" ");
isSame = to === res.from;
}
break; break;
case OPT_TRANS_TENCENT: case OPT_TRANS_TENCENT:
trText = res.auto_translation; trText = res.auto_translation;
isSame = text === trText; isSame = text === trText;
break; break;
case OPT_TRANS_OPENAI: case OPT_TRANS_OPENAI:
trText = res?.choices?.[0].message.content; trText = res?.choices?.map((item) => item.message.content).join(" ");
isSame = text === trText; isSame = text === trText;
break; break;
case OPT_TRANS_GEMINI: case OPT_TRANS_GEMINI:
trText = res?.candidates?.[0].content.parts[0].text; trText = res?.candidates
?.map((item) => item.content?.parts.map((item) => item.text).join(" "))
.join(" ");
isSame = text === trText; isSame = text === trText;
break; break;
case OPT_TRANS_CLOUDFLAREAI: case OPT_TRANS_CLOUDFLAREAI:

View File

@@ -9,6 +9,11 @@ import {
MSG_TRANS_TOGGLE_STYLE, MSG_TRANS_TOGGLE_STYLE,
MSG_OPEN_TRANBOX, MSG_OPEN_TRANBOX,
MSG_CONTEXT_MENUS, MSG_CONTEXT_MENUS,
MSG_COMMAND_SHORTCUTS,
MSG_INJECT_JS,
MSG_INJECT_CSS,
MSG_UPDATE_CSP,
DEFAULT_CSPLIST,
CMD_TOGGLE_TRANSLATE, CMD_TOGGLE_TRANSLATE,
CMD_TOGGLE_STYLE, CMD_TOGGLE_STYLE,
CMD_OPEN_OPTIONS, CMD_OPEN_OPTIONS,
@@ -21,45 +26,102 @@ import { sendTabMsg } from "./libs/msg";
import { trySyncAllSubRules } from "./libs/subRules"; import { trySyncAllSubRules } from "./libs/subRules";
import { tryClearCaches } from "./libs"; import { tryClearCaches } from "./libs";
import { saveRule } from "./libs/rules"; import { saveRule } from "./libs/rules";
import { getCurTabId } from "./libs/msg";
import { injectInlineJs, injectInternalCss } from "./libs/injector";
import { kissLog } from "./libs/log";
globalThis.ContextType = "BACKGROUND"; globalThis.ContextType = "BACKGROUND";
const REMOVE_HEADERS = [
`content-security-policy`,
`content-security-policy-report-only`,
`x-webkit-csp`,
`x-content-security-policy`,
];
/** /**
* 添加右键菜单 * 添加右键菜单
*/ */
function addContextMenus() { async function addContextMenus(contextMenuType = 1) {
browser.contextMenus.create({ // 添加前先删除,避免重复ID的错误
id: CMD_TOGGLE_TRANSLATE, try {
title: browser.i18n.getMessage("toggle_translate"), await browser.contextMenus.removeAll();
contexts: ["page", "selection"], } catch (err) {
}); //
browser.contextMenus.create({ }
id: CMD_TOGGLE_STYLE,
title: browser.i18n.getMessage("toggle_style"), switch (contextMenuType) {
contexts: ["page", "selection"], case 1:
}); browser.contextMenus.create({
browser.contextMenus.create({ id: CMD_TOGGLE_TRANSLATE,
id: CMD_OPEN_TRANBOX, title: browser.i18n.getMessage("app_name"),
title: browser.i18n.getMessage("open_tranbox"), contexts: ["page", "selection"],
contexts: ["page", "selection"], });
}); break;
browser.contextMenus.create({ case 2:
id: "options_separator", browser.contextMenus.create({
type: "separator", id: CMD_TOGGLE_TRANSLATE,
contexts: ["page", "selection"], title: browser.i18n.getMessage("toggle_translate"),
}); contexts: ["page", "selection"],
browser.contextMenus.create({ });
id: CMD_OPEN_OPTIONS, browser.contextMenus.create({
title: browser.i18n.getMessage("open_options"), id: CMD_TOGGLE_STYLE,
contexts: ["page", "selection"], title: browser.i18n.getMessage("toggle_style"),
}); contexts: ["page", "selection"],
});
browser.contextMenus.create({
id: CMD_OPEN_TRANBOX,
title: browser.i18n.getMessage("open_tranbox"),
contexts: ["page", "selection"],
});
browser.contextMenus.create({
id: "options_separator",
type: "separator",
contexts: ["page", "selection"],
});
browser.contextMenus.create({
id: CMD_OPEN_OPTIONS,
title: browser.i18n.getMessage("open_options"),
contexts: ["page", "selection"],
});
break;
default:
}
} }
/** /**
* 清除右键菜单 * 更新CSP策略
* @param {*} csplist
*/ */
function removeContextMenus() { async function updateCspRules(csplist = DEFAULT_CSPLIST.join(",\n")) {
browser.contextMenus.removeAll(); try {
const newRules = csplist
.split(/\n|,/)
.map((url) => url.trim())
.filter(Boolean)
.map((url, idx) => ({
id: idx + 1,
action: {
type: "modifyHeaders",
responseHeaders: REMOVE_HEADERS.map((header) => ({
operation: "remove",
header,
})),
},
condition: {
urlFilter: url,
resourceTypes: ["main_frame", "sub_frame"],
},
}));
const oldRules = await browser.declarativeNetRequest.getDynamicRules();
const oldRuleIds = oldRules.map((rule) => rule.id);
await browser.declarativeNetRequest.updateDynamicRules({
removeRuleIds: oldRuleIds,
addRules: newRules,
});
} catch (err) {
kissLog(err, "update csp rules");
}
} }
/** /**
@@ -70,6 +132,9 @@ browser.runtime.onInstalled.addListener(() => {
// 右键菜单 // 右键菜单
addContextMenus(); addContextMenus();
// 禁用CSP
updateCspRules();
}); });
/** /**
@@ -79,11 +144,8 @@ browser.runtime.onStartup.addListener(async () => {
// 同步数据 // 同步数据
await trySyncSettingAndRules(); await trySyncSettingAndRules();
const { const { clearCache, contextMenuType, subrulesList, csplist } =
clearCache, await getSettingWithDefault();
contextMenus = true,
subrulesList,
} = await getSettingWithDefault();
// 清除缓存 // 清除缓存
if (clearCache) { if (clearCache) {
@@ -91,9 +153,11 @@ browser.runtime.onStartup.addListener(async () => {
} }
// 右键菜单 // 右键菜单
if (!contextMenus) { // firefox重启后菜单会消失,故重复添加
removeContextMenus(); addContextMenus(contextMenuType);
}
// 禁用CSP
updateCspRules(csplist);
// 同步订阅规则 // 同步订阅规则
trySyncAllSubRules({ subrulesList }); trySyncAllSubRules({ subrulesList });
@@ -102,48 +166,44 @@ browser.runtime.onStartup.addListener(async () => {
/** /**
* 监听消息 * 监听消息
*/ */
browser.runtime.onMessage.addListener( browser.runtime.onMessage.addListener(async ({ action, args }) => {
({ action, args }, sender, sendResponse) => { switch (action) {
switch (action) { case MSG_FETCH:
case MSG_FETCH: const { input, opts } = args;
const { input, opts } = args; return await fetchData(input, opts);
fetchData(input, opts) case MSG_FETCH_LIMIT:
.then((data) => { const { interval, limit } = args;
sendResponse({ data }); return fetchPool.update(interval, limit);
}) case MSG_FETCH_CLEAR:
.catch((error) => { return fetchPool.clear();
sendResponse({ error: error.message, cause: error.cause }); case MSG_OPEN_OPTIONS:
}); return await browser.runtime.openOptionsPage();
break; case MSG_SAVE_RULE:
case MSG_FETCH_LIMIT: return await saveRule(args);
const { interval, limit } = args; case MSG_INJECT_JS:
fetchPool.update(interval, limit); return await browser.scripting.executeScript({
sendResponse({ data: "ok" }); target: { tabId: await getCurTabId(), allFrames: true },
break; func: injectInlineJs,
case MSG_FETCH_CLEAR: args: [args],
fetchPool.clear(); world: "MAIN",
sendResponse({ data: "ok" }); });
break; case MSG_INJECT_CSS:
case MSG_OPEN_OPTIONS: return await browser.scripting.executeScript({
browser.runtime.openOptionsPage(); target: { tabId: await getCurTabId(), allFrames: true },
break; func: injectInternalCss,
case MSG_SAVE_RULE: args: [args],
saveRule(args); world: "MAIN",
break; });
case MSG_CONTEXT_MENUS: case MSG_UPDATE_CSP:
const { contextMenus } = args; return await updateCspRules(args);
if (contextMenus) { case MSG_CONTEXT_MENUS:
addContextMenus(); return await addContextMenus(args);
} else { case MSG_COMMAND_SHORTCUTS:
removeContextMenus(); return await browser.commands.getAll();
} default:
break; throw new Error(`message action is unavailable: ${action}`);
default:
sendResponse({ error: `message action is unavailable: ${action}` });
}
return true;
} }
); });
/** /**
* 监听快捷键 * 监听快捷键

View File

@@ -14,13 +14,12 @@ import {
} from "./config"; } from "./config";
import { getFabWithDefault, getSettingWithDefault } from "./libs/storage"; import { getFabWithDefault, getSettingWithDefault } from "./libs/storage";
import { Translator } from "./libs/translator"; import { Translator } from "./libs/translator";
import { isIframe, sendIframeMsg, sendParentMsg } from "./libs/iframe"; import { isIframe, sendIframeMsg } from "./libs/iframe";
import Slection from "./views/Selection"; import Slection from "./views/Selection";
import { touchTapListener } from "./libs/touch"; import { touchTapListener } from "./libs/touch";
import { debounce, genEventName } from "./libs/utils"; import { debounce, genEventName } from "./libs/utils";
import { handlePing, injectScript } from "./libs/gm"; import { handlePing, injectScript } from "./libs/gm";
import { browser } from "./libs/browser"; import { browser } from "./libs/browser";
import { matchFixer } from "./libs/webfix";
import { matchRule } from "./libs/rules"; import { matchRule } from "./libs/rules";
import { trySyncAllSubRules } from "./libs/subRules"; import { trySyncAllSubRules } from "./libs/subRules";
import { isInBlacklist } from "./libs/blacklist"; import { isInBlacklist } from "./libs/blacklist";
@@ -79,10 +78,9 @@ function runtimeListener(translator) {
/** /**
* iframe 页面执行 * iframe 页面执行
* @param {*} setting * @param {*} translator
*/ */
function runIframe(setting) { function runIframe(translator) {
let translator;
window.addEventListener("message", (e) => { window.addEventListener("message", (e) => {
const { action, args } = e.data || {}; const { action, args } = e.data || {};
switch (action) { switch (action) {
@@ -93,16 +91,11 @@ function runIframe(setting) {
translator?.toggleStyle(); translator?.toggleStyle();
break; break;
case MSG_TRANS_PUTRULE: case MSG_TRANS_PUTRULE:
if (!translator) { translator.updateRule(args || {});
translator = new Translator(args, setting);
} else {
translator.updateRule(args || {});
}
break; break;
default: default:
} }
}); });
sendParentMsg(MSG_TRANS_GETRULE);
} }
/** /**
@@ -114,6 +107,9 @@ async function showFab(translator) {
const fab = await getFabWithDefault(); const fab = await getFabWithDefault();
const $action = document.createElement("div"); const $action = document.createElement("div");
$action.setAttribute("id", APP_LCNAME); $action.setAttribute("id", APP_LCNAME);
$action.style.fontSize = "0";
$action.style.width = "0";
$action.style.height = "0";
document.body.parentElement.appendChild($action); document.body.parentElement.appendChild($action);
const shadowContainer = $action.attachShadow({ mode: "closed" }); const shadowContainer = $action.attachShadow({ mode: "closed" });
const emotionRoot = document.createElement("style"); const emotionRoot = document.createElement("style");
@@ -140,7 +136,7 @@ async function showFab(translator) {
* @returns * @returns
*/ */
function showTransbox({ function showTransbox({
contextMenus = true, contextMenuType,
tranboxSetting = DEFAULT_TRANBOX_SETTING, tranboxSetting = DEFAULT_TRANBOX_SETTING,
transApis, transApis,
}) { }) {
@@ -150,6 +146,9 @@ function showTransbox({
const $tranbox = document.createElement("div"); const $tranbox = document.createElement("div");
$tranbox.setAttribute("id", "kiss-transbox"); $tranbox.setAttribute("id", "kiss-transbox");
$tranbox.style.fontSize = "0";
$tranbox.style.width = "0";
$tranbox.style.height = "0";
document.body.parentElement.appendChild($tranbox); document.body.parentElement.appendChild($tranbox);
const shadowContainer = $tranbox.attachShadow({ mode: "closed" }); const shadowContainer = $tranbox.attachShadow({ mode: "closed" });
const emotionRoot = document.createElement("style"); const emotionRoot = document.createElement("style");
@@ -165,7 +164,7 @@ function showTransbox({
<React.StrictMode> <React.StrictMode>
<CacheProvider value={cache}> <CacheProvider value={cache}>
<Slection <Slection
contextMenus={contextMenus} contextMenuType={contextMenuType}
tranboxSetting={tranboxSetting} tranboxSetting={tranboxSetting}
transApis={transApis} transApis={transApis}
/> />
@@ -174,22 +173,6 @@ function showTransbox({
); );
} }
/**
* 监听来自iframe页面消息
* @param {*} rule
*/
function windowListener(rule) {
window.addEventListener("message", (e) => {
const { action } = e.data || {};
switch (action) {
case MSG_TRANS_GETRULE:
sendIframeMsg(MSG_TRANS_PUTRULE, rule);
break;
default:
}
});
}
/** /**
* 显示错误信息到页面顶部 * 显示错误信息到页面顶部
* @param {*} message * @param {*} message
@@ -245,21 +228,17 @@ export async function run(isUserscript = false) {
return; return;
} }
// 翻译网页
const rule = await matchRule(href, setting);
const translator = new Translator(rule, setting);
// 适配iframe // 适配iframe
if (isIframe) { if (isIframe) {
runIframe(setting); runIframe(translator);
return; return;
} }
// 不规范网页修复
const fixerSetting = await matchFixer(href, setting);
// 翻译网页
const rule = await matchRule(href, setting);
const translator = new Translator(rule, setting, fixerSetting);
// 监听消息 // 监听消息
windowListener(rule);
!isUserscript && runtimeListener(translator); !isUserscript && runtimeListener(translator);
// 输入框翻译 // 输入框翻译

View File

@@ -112,8 +112,8 @@ export const I18N = {
en: customApiHelpEN, en: customApiHelpEN,
}, },
translate_alt: { translate_alt: {
zh: `翻译 (Alt+Q)`, zh: `翻译`,
en: `Translate (Alt+Q)`, en: `Translate`,
}, },
basic_setting: { basic_setting: {
zh: `基本设置`, zh: `基本设置`,
@@ -160,20 +160,24 @@ export const I18N = {
en: `Interface Language`, en: `Interface Language`,
}, },
fetch_limit: { fetch_limit: {
zh: `最大请求数量 (1-100)`, zh: `最大并发请求数量 (1-100)`,
en: `Maximum Number Of Request (1-100)`, en: `Maximum Number Of Concurrent Requests (1-100)`,
}, },
fetch_interval: { fetch_interval: {
zh: `请求间隔时间 (0-5000ms)`, zh: `每次请求间隔时间 (0-5000ms)`,
en: `Request Interval (0-5000ms)`, en: `Time Between Requests (0-5000ms)`,
},
translate_interval: {
zh: `重新翻译间隔时间 (100-5000ms)`,
en: `Retranslation Interval (100-5000ms)`,
}, },
min_translate_length: { min_translate_length: {
zh: `最小翻译长度 (1-100)`, zh: `最小翻译字符数 (1-100)`,
en: `Min Translate Length (1-100)`, en: `Minimum number Of Translated Characters (1-100)`,
}, },
max_translate_length: { max_translate_length: {
zh: `最大翻译长度 (100-10000)`, zh: `最大翻译字符数 (100-10000)`,
en: `Max Translate Length (100-10000)`, en: `Maximum number Of Translated Characters (100-10000)`,
}, },
num_of_newline_characters: { num_of_newline_characters: {
zh: `换行字符数 (1-1000)`, zh: `换行字符数 (1-1000)`,
@@ -183,16 +187,20 @@ export const I18N = {
zh: `翻译服务`, zh: `翻译服务`,
en: `Translate Service`, en: `Translate Service`,
}, },
mouseover_translation: { translate_timing: {
zh: `鼠标悬停翻译`, zh: `翻译时机`,
en: `Mouseover translation`, en: `Translate Timing`,
}, },
mk_disable: { mk_pagescroll: {
zh: `禁用`, zh: `滚动加载翻译(推荐)`,
en: `Disable`, en: `Rolling Loading (Suggested)`,
},
mk_pageopen: {
zh: `页面打开全部翻译`,
en: `Page Open`,
}, },
mk_mouseover: { mk_mouseover: {
zh: `鼠标悬停`, zh: `鼠标悬停翻译`,
en: `Mouseover`, en: `Mouseover`,
}, },
mk_ctrlKey: { mk_ctrlKey: {
@@ -224,12 +232,12 @@ export const I18N = {
en: `After setting, it will produce mutual translation effect with the target language, but it relies on remote language recognition.`, en: `After setting, it will produce mutual translation effect with the target language, but it relies on remote language recognition.`,
}, },
text_style: { text_style: {
zh: `样式`, zh: `文样式`,
en: `Text Style`, en: `Text Style`,
}, },
text_style_alt: { text_style_alt: {
zh: `文字样式 (Alt+C)`, zh: `译文样式`,
en: `Text Style (Alt+C)`, en: `Text Style`,
}, },
bg_color: { bg_color: {
zh: `样式颜色`, zh: `样式颜色`,
@@ -303,6 +311,10 @@ export const I18N = {
zh: `2、“订阅规则”的注入位置是倒数第二的位置因此除全局规则(*)外,“个人规则”优先级比“订阅规则”高,“个人规则”填写同样的网址会覆盖”订阅规则“的条目。`, zh: `2、“订阅规则”的注入位置是倒数第二的位置因此除全局规则(*)外,“个人规则”优先级比“订阅规则”高,“个人规则”填写同样的网址会覆盖”订阅规则“的条目。`,
en: `2. The injection position of "Subscription Rules" is the penultimate position. Therefore, except for the global rules (*), the priority of "Personal Rules" is higher than that of "Subscription Rules". Filling in the same url in "Personal Rules" will overwrite "Subscription Rules" entry.`, en: `2. The injection position of "Subscription Rules" is the penultimate position. Therefore, except for the global rules (*), the priority of "Personal Rules" is higher than that of "Subscription Rules". Filling in the same url in "Personal Rules" will overwrite "Subscription Rules" entry.`,
}, },
rules_warn_3: {
zh: `3、关于规则填写输入框留空或下拉框选“*”表示采用全局规则。`,
en: `3. Regarding filling in the rules: Leave the input box blank or select "*" in the drop-down box to use global rule.`,
},
sync_warn: { sync_warn: {
zh: `如果服务器存在其他客户端同步的数据,第一次同步将直接覆盖本地配置,后面则根据修改时间,新的覆盖旧的。`, zh: `如果服务器存在其他客户端同步的数据,第一次同步将直接覆盖本地配置,后面则根据修改时间,新的覆盖旧的。`,
en: `If the server has data synchronized by other clients, the first synchronization will directly overwrite the local configuration, and later, according to the modification time, the new one will overwrite the old one.`, en: `If the server has data synchronized by other clients, the first synchronization will directly overwrite the local configuration, and later, according to the modification time, the new one will overwrite the old one.`,
@@ -364,12 +376,12 @@ export const I18N = {
en: `URL pattern`, en: `URL pattern`,
}, },
pattern_helper: { pattern_helper: {
zh: `1、支持星号(*)通配符。2、多个URL用英文逗号“,”分隔。`, zh: `1、支持星号(*)通配符。2、多个URL用换行或英文逗号“,”分隔。`,
en: `1. The asterisk (*) wildcard is supported. 2. Multiple URLs separated by English commas ",".`, en: `1. Supports the asterisk (*) wildcard character. 2. Separate multiple URLs with newlines or English commas ",".`,
}, },
selector_helper: { selector_helper: {
zh: `1、遵循CSS选择器语法。2、留空表示采用全局设置。3、多个CSS选择器之间用“;”隔开。4、“shadow root”选择器和内部选择器用“>>>”隔开。`, zh: `1、遵循CSS选择器语法。2、多个CSS选择器之间用“;”隔开。3、“shadow root”选择器和内部选择器用“>>>”隔开。`,
en: `1. Follow CSS selector syntax. 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 ">>>".`, en: `1. Follow CSS selector syntax. 2. Separate multiple CSS selectors with ";". 3. The "shadow root" selector and the internal selector are separated by ">>>".`,
}, },
translate_switch: { translate_switch: {
zh: `开启翻译`, zh: `开启翻译`,
@@ -392,8 +404,44 @@ export const I18N = {
en: `Keep unchanged selector`, en: `Keep unchanged selector`,
}, },
keep_selector_helper: { keep_selector_helper: {
zh: `1、遵循CSS选择器语法。2、留空表示采用全局设置`, zh: `1、遵循CSS选择器语法。2、子元素选择器用“>>>”隔开`,
en: `1. Follow CSS selector syntax. 2. Leave blank to adopt the global setting.`, en: `1. Follow CSS selector syntax. 2. Sub-element selectors are separated by ">>>".`,
},
terms: {
zh: `专业术语`,
en: `Terms`,
},
terms_helper: {
zh: `1、支持正则表达式匹配无需斜杆不支持修饰符。2、多条术语用换行或分号“;”隔开。3、术语和译文用英文逗号“,”隔开。4、没有译文视为不翻译术语。`,
en: `1. Supports regular expression matching, no slash required, and no modifiers are supported. 2. Separate multiple terms with newlines or semicolons ";". 3. Terms and translations are separated by English commas ",". 4. If there is no translation, the term will be deemed not to be translated.`,
},
selector_style: {
zh: `选择器节点样式`,
en: `Selector Style`,
},
selector_style_helper: {
zh: `开启翻译时注入,关闭翻译时不会移除。`,
en: `It is injected when translation is turned on and will not be removed when translation is turned off.`,
},
selector_parent_style: {
zh: `选择器父节点样式`,
en: `Selector Parent Style`,
},
inject_js: {
zh: `注入JS`,
en: `Inject JS`,
},
inject_js_helper: {
zh: `1、开启翻译时注入运行关闭翻译时移除。2、随着页面变化可能会多次注入运行。`,
en: `1. Inject and run when translation is turned on, and removed when translation is turned off. 2. As the page changes, it may be injected and run multiple times.`,
},
inject_css: {
zh: `注入CSS`,
en: `Inject CSS`,
},
inject_css_helper: {
zh: `开启翻译时注入,关闭翻译时将移除。`,
en: `Injected when translation is enabled and removed when translation is disabled.`,
}, },
root_selector: { root_selector: {
zh: `根选择器`, zh: `根选择器`,
@@ -403,6 +451,10 @@ export const I18N = {
zh: `修复函数`, zh: `修复函数`,
en: `Fixer Function`, en: `Fixer Function`,
}, },
fixer_function_helper: {
zh: `1、br是将<br>换行替换成<p "kiss-p">。2、bn是将\\n换行替换成<p "kiss-p">。3、brToDiv和bnToDiv是替换成<div class="kiss-p">。`,
en: `1. br replaces <br> line breaks with <p "kiss-p">. 2. bn replaces \\n newline with <p "kiss-p">. 3. brToDiv and bnToDiv are replaced with <div class="kiss-p">.`,
},
import: { import: {
zh: `导入`, zh: `导入`,
en: `Import`, en: `Import`,
@@ -571,6 +623,10 @@ export const I18N = {
zh: `隐藏翻译按钮`, zh: `隐藏翻译按钮`,
en: `Hide Translate Button`, en: `Hide Translate Button`,
}, },
hide_click_away: {
zh: `点击外部关闭弹窗`,
en: `Click outside to close the pop-up window`,
},
show: { show: {
zh: `显示`, zh: `显示`,
en: `Show`, en: `Show`,
@@ -703,20 +759,64 @@ export const I18N = {
zh: `禁用翻译名单`, zh: `禁用翻译名单`,
en: `Translate Blacklist`, en: `Translate Blacklist`,
}, },
disable_langs: { disabled_csplist: {
zh: `禁用CSP名单`,
en: `Disabled CSP List`,
},
disabled_csplist_helper: {
zh: `3、通过调整CSP策略使得某些页面能够注入JS/CSS/Media请谨慎使用除非您已知晓相关风险。`,
en: `3. By adjusting the CSP policy, some pages can inject JS/CSS/Media. Please use it with caution unless you are aware of the related risks.`,
},
skip_langs: {
zh: `不翻译的语言`, zh: `不翻译的语言`,
en: `Disable Languages`, en: `Disable Languages`,
}, },
disable_langs_helper: { skip_langs_helper: {
zh: `此功能依赖准确的语言检测,建议启用远程语言检测。`, zh: `此功能依赖准确的语言检测,建议启用远程语言检测。`,
en: `This feature relies on accurate language detection. It is recommended to enable remote language detection.`, en: `This feature relies on accurate language detection. It is recommended to enable remote language detection.`,
}, },
add_context_menus: { context_menus: {
zh: `添加右键菜单`, zh: `右键菜单`,
en: `Add Context Menus`, en: `Context Menus`,
},
hide_context_menus: {
zh: `隐藏右键菜单`,
en: `Hide Context Menus`,
},
simple_context_menus: {
zh: `简单右键菜单`,
en: `Simple_context_menus Context Menus`,
},
secondary_context_menus: {
zh: `二级右键菜单`,
en: `Secondary Context Menus`,
}, },
mulkeys_help: { mulkeys_help: {
zh: `支持英文逗号隔开多个KEY轮调用。`, zh: `支持用换行或英文逗号“,”分隔多个KEY轮调用。`,
en: `Supports multiple KEY round calling calls separated by English commas.`, en: `Supports multiple KEY polling calls separated by newlines or English commas ",".`,
},
translation_element_tag: {
zh: `译文元素标签`,
en: `Translation Element Tag`,
},
show_only_translations: {
zh: `仅显示译文`,
en: `Show Only Translations`,
},
show_only_translations_help: {
zh: `非完美实现,某些页面可能有样式等问题。`,
en: `It is not a perfect implementation and some pages may have style issues.`,
},
translate_page_title: {
zh: `是否翻译页面标题`,
en: `Translate Page Title`,
},
more: {
zh: `更多`,
en: `More`,
},
fixer_selector: {
zh: `网页修复选择器`,
en: `Fixer Selector`,
}, },
}; };

View File

@@ -24,12 +24,10 @@ export const STOKEY_MSAUTH = `${APP_NAME}_msauth`;
export const STOKEY_BDAUTH = `${APP_NAME}_bdauth`; export const STOKEY_BDAUTH = `${APP_NAME}_bdauth`;
export const STOKEY_SETTING = `${APP_NAME}_setting`; export const STOKEY_SETTING = `${APP_NAME}_setting`;
export const STOKEY_RULES = `${APP_NAME}_rules`; export const STOKEY_RULES = `${APP_NAME}_rules`;
export const STOKEY_WFRULES = `${APP_NAME}_webfix_rules`;
export const STOKEY_WORDS = `${APP_NAME}_words`; export const STOKEY_WORDS = `${APP_NAME}_words`;
export const STOKEY_SYNC = `${APP_NAME}_sync`; export const STOKEY_SYNC = `${APP_NAME}_sync`;
export const STOKEY_FAB = `${APP_NAME}_fab`; export const STOKEY_FAB = `${APP_NAME}_fab`;
export const STOKEY_RULESCACHE_PREFIX = `${APP_NAME}_rulescache_`; export const STOKEY_RULESCACHE_PREFIX = `${APP_NAME}_rulescache_`;
export const STOKEY_WEBFIXCACHE_PREFIX = `${APP_NAME}_webfixcache_`;
export const CMD_TOGGLE_TRANSLATE = "toggleTranslate"; export const CMD_TOGGLE_TRANSLATE = "toggleTranslate";
export const CMD_TOGGLE_STYLE = "toggleStyle"; export const CMD_TOGGLE_STYLE = "toggleStyle";
@@ -44,7 +42,6 @@ export const CLIENT_USERSCRIPT = "userscript";
export const CLIENT_EXTS = [CLIENT_CHROME, CLIENT_EDGE, CLIENT_FIREFOX]; export const CLIENT_EXTS = [CLIENT_CHROME, CLIENT_EDGE, CLIENT_FIREFOX];
export const KV_RULES_KEY = "kiss-rules.json"; export const KV_RULES_KEY = "kiss-rules.json";
export const KV_WFRULES_KEY = "kiss-webfix.json";
export const KV_WORDS_KEY = "kiss-words.json"; export const KV_WORDS_KEY = "kiss-words.json";
export const KV_RULES_SHARE_KEY = "kiss-rules-share.json"; export const KV_RULES_SHARE_KEY = "kiss-rules-share.json";
export const KV_SETTING_KEY = "kiss-setting.json"; export const KV_SETTING_KEY = "kiss-setting.json";
@@ -65,6 +62,10 @@ export const MSG_TRANS_GETRULE = "trans_getrule";
export const MSG_TRANS_PUTRULE = "trans_putrule"; export const MSG_TRANS_PUTRULE = "trans_putrule";
export const MSG_TRANS_CURRULE = "trans_currule"; export const MSG_TRANS_CURRULE = "trans_currule";
export const MSG_CONTEXT_MENUS = "context_menus"; export const MSG_CONTEXT_MENUS = "context_menus";
export const MSG_COMMAND_SHORTCUTS = "command_shortcuts";
export const MSG_INJECT_JS = "inject_js";
export const MSG_INJECT_CSS = "inject_css";
export const MSG_UPDATE_CSP = "update_csp";
export const THEME_LIGHT = "light"; export const THEME_LIGHT = "light";
export const THEME_DARK = "dark"; export const THEME_DARK = "dark";
@@ -82,8 +83,11 @@ export const URL_MICROSOFT_TRAN =
"https://api-edge.cognitive.microsofttranslator.com/translate"; "https://api-edge.cognitive.microsofttranslator.com/translate";
export const URL_MICROSOFT_AUTH = "https://edge.microsoft.com/translate/auth"; export const URL_MICROSOFT_AUTH = "https://edge.microsoft.com/translate/auth";
export const URL_BAIDU_LANGDETECT = "https://fanyi.baidu.com/langdetect"; export const URL_BAIDU_LANGDETECT = "https://fanyi.baidu.com/langdetect";
export const URL_BAIDU_SUGGEST = "https://fanyi.baidu.com/sug";
export const URL_BAIDU_TTS = "https://fanyi.baidu.com/gettts";
export const URL_BAIDU_WEB = "https://fanyi.baidu.com/"; export const URL_BAIDU_WEB = "https://fanyi.baidu.com/";
export const URL_BAIDU_TRAN = "https://fanyi.baidu.com/v2transapi"; export const URL_BAIDU_TRANSAPI = "https://fanyi.baidu.com/transapi";
export const URL_BAIDU_TRANSAPI_V2 = "https://fanyi.baidu.com/v2transapi";
export const URL_DEEPLFREE_TRAN = "https://www2.deepl.com/jsonrpc"; export const URL_DEEPLFREE_TRAN = "https://www2.deepl.com/jsonrpc";
export const URL_TENCENT_TRANSMART = "https://transmart.qq.com/api/imt"; export const URL_TENCENT_TRANSMART = "https://transmart.qq.com/api/imt";
@@ -98,18 +102,26 @@ export const OPT_TRANS_OPENAI = "OpenAI";
export const OPT_TRANS_GEMINI = "Gemini"; export const OPT_TRANS_GEMINI = "Gemini";
export const OPT_TRANS_CLOUDFLAREAI = "CloudflareAI"; export const OPT_TRANS_CLOUDFLAREAI = "CloudflareAI";
export const OPT_TRANS_CUSTOMIZE = "Custom"; export const OPT_TRANS_CUSTOMIZE = "Custom";
export const OPT_TRANS_CUSTOMIZE_2 = "Custom2";
export const OPT_TRANS_CUSTOMIZE_3 = "Custom3";
export const OPT_TRANS_CUSTOMIZE_4 = "Custom4";
export const OPT_TRANS_CUSTOMIZE_5 = "Custom5";
export const OPT_TRANS_ALL = [ export const OPT_TRANS_ALL = [
OPT_TRANS_GOOGLE, OPT_TRANS_GOOGLE,
OPT_TRANS_MICROSOFT, OPT_TRANS_MICROSOFT,
OPT_TRANS_BAIDU,
OPT_TRANS_TENCENT,
OPT_TRANS_DEEPL, OPT_TRANS_DEEPL,
OPT_TRANS_DEEPLFREE, OPT_TRANS_DEEPLFREE,
OPT_TRANS_DEEPLX, OPT_TRANS_DEEPLX,
OPT_TRANS_BAIDU,
OPT_TRANS_TENCENT,
OPT_TRANS_OPENAI, OPT_TRANS_OPENAI,
OPT_TRANS_GEMINI, OPT_TRANS_GEMINI,
OPT_TRANS_CLOUDFLAREAI, OPT_TRANS_CLOUDFLAREAI,
OPT_TRANS_CUSTOMIZE, OPT_TRANS_CUSTOMIZE,
OPT_TRANS_CUSTOMIZE_2,
OPT_TRANS_CUSTOMIZE_3,
OPT_TRANS_CUSTOMIZE_4,
OPT_TRANS_CUSTOMIZE_5,
]; ];
export const OPT_LANGS_TO = [ export const OPT_LANGS_TO = [
@@ -296,17 +308,19 @@ export const OPT_STYLE_USE_COLOR = [
OPT_STYLE_BLOCKQUOTE, OPT_STYLE_BLOCKQUOTE,
]; ];
export const OPT_MOUSEKEY_DISABLE = "mk_disable"; export const OPT_TIMING_PAGESCROLL = "mk_pagescroll"; // 滚动加载翻译
export const OPT_MOUSEKEY_MOUSEOVER = "mk_mouseover"; export const OPT_TIMING_PAGEOPEN = "mk_pageopen"; // 直接翻译到底
export const OPT_MOUSEKEY_CONTROL = "mk_ctrlKey"; export const OPT_TIMING_MOUSEOVER = "mk_mouseover";
export const OPT_MOUSEKEY_SHIFT = "mk_shiftKey"; export const OPT_TIMING_CONTROL = "mk_ctrlKey";
export const OPT_MOUSEKEY_ALT = "mk_altKey"; export const OPT_TIMING_SHIFT = "mk_shiftKey";
export const OPT_MOUSEKEY_ALL = [ export const OPT_TIMING_ALT = "mk_altKey";
OPT_MOUSEKEY_DISABLE, export const OPT_TIMING_ALL = [
OPT_MOUSEKEY_MOUSEOVER, OPT_TIMING_PAGESCROLL,
OPT_MOUSEKEY_CONTROL, OPT_TIMING_PAGEOPEN,
OPT_MOUSEKEY_SHIFT, OPT_TIMING_MOUSEOVER,
OPT_MOUSEKEY_ALT, OPT_TIMING_CONTROL,
OPT_TIMING_SHIFT,
OPT_TIMING_ALT,
]; ];
export const DEFAULT_FETCH_LIMIT = 10; // 默认最大任务数量 export const DEFAULT_FETCH_LIMIT = 10; // 默认最大任务数量
@@ -318,18 +332,35 @@ export const PROMPT_PLACE_TEXT = "{{text}}"; // 占位符
export const DEFAULT_COLOR = "#209CEE"; // 默认高亮背景色/线条颜色 export const DEFAULT_COLOR = "#209CEE"; // 默认高亮背景色/线条颜色
export const DEFAULT_TRANS_TAG = "span";
export const DEFAULT_SELECT_STYLE =
"-webkit-line-clamp: unset; max-height: none; height: auto;";
// 全局规则 // 全局规则
export const GLOBLA_RULE = { export const GLOBLA_RULE = {
pattern: "*", pattern: "*", // 匹配网址
selector: DEFAULT_SELECTOR, selector: DEFAULT_SELECTOR, // 选择器
keepSelector: DEFAULT_KEEP_SELECTOR, keepSelector: DEFAULT_KEEP_SELECTOR, // 保留元素选择器
translator: OPT_TRANS_MICROSOFT, terms: "", // 专业术语
fromLang: "auto", translator: OPT_TRANS_MICROSOFT, // 翻译服务
toLang: "zh-CN", fromLang: "auto", // 源语言
textStyle: OPT_STYLE_DASHLINE, toLang: "zh-CN", // 目标语言
transOpen: "false", textStyle: OPT_STYLE_DASHLINE, // 译文样式
bgColor: "", transOpen: "false", // 开启翻译
textDiyStyle: "", bgColor: "", // 译文颜色
textDiyStyle: "", // 自定义译文样式
selectStyle: DEFAULT_SELECT_STYLE, // 选择器节点样式
parentStyle: DEFAULT_SELECT_STYLE, // 选择器父节点样式
injectJs: "", // 注入JS
injectCss: "", // 注入CSS
transOnly: "false", // 是否仅显示译文
transTiming: OPT_TIMING_PAGESCROLL, // 翻译时机/鼠标悬停翻译
transTag: DEFAULT_TRANS_TAG, // 译文元素标签
transTitle: "false", // 是否同时翻译页面标题
detectRemote: "false", // 是否使用远程语言检测
skipLangs: [], // 不翻译的语言
fixerSelector: "", // 修复函数选择器
fixerFunc: "-", // 修复函数
}; };
// 输入框翻译 // 输入框翻译
@@ -357,7 +388,8 @@ export const DEFAULT_TRANBOX_SETTING = {
tranboxShortcut: DEFAULT_TRANBOX_SHORTCUT, tranboxShortcut: DEFAULT_TRANBOX_SHORTCUT,
btnOffsetX: 10, btnOffsetX: 10,
btnOffsetY: 10, btnOffsetY: 10,
hideTranBtn: false, hideTranBtn: false, // 是否隐藏翻译按钮
hideClickAway: false, // 是否点击外部关闭弹窗
}; };
// 订阅列表 // 订阅列表
@@ -377,39 +409,74 @@ export const DEFAULT_SUBRULES_LIST = [
]; ];
// 翻译接口 // 翻译接口
const defaultCustomApi = {
url: "",
key: "",
fetchLimit: DEFAULT_FETCH_LIMIT,
fetchInterval: DEFAULT_FETCH_INTERVAL,
};
export const DEFAULT_TRANS_APIS = { export const DEFAULT_TRANS_APIS = {
[OPT_TRANS_GOOGLE]: { [OPT_TRANS_GOOGLE]: {
url: "https://translate.googleapis.com/translate_a/single", url: "https://translate.googleapis.com/translate_a/single",
key: "", key: "",
fetchLimit: DEFAULT_FETCH_LIMIT, // 最大任务数量
fetchInterval: DEFAULT_FETCH_INTERVAL, // 任务间隔时间
},
[OPT_TRANS_MICROSOFT]: {
fetchLimit: DEFAULT_FETCH_LIMIT,
fetchInterval: DEFAULT_FETCH_INTERVAL,
},
[OPT_TRANS_BAIDU]: {
fetchLimit: DEFAULT_FETCH_LIMIT,
fetchInterval: DEFAULT_FETCH_INTERVAL,
},
[OPT_TRANS_TENCENT]: {
fetchLimit: DEFAULT_FETCH_LIMIT,
fetchInterval: DEFAULT_FETCH_INTERVAL,
}, },
[OPT_TRANS_DEEPL]: { [OPT_TRANS_DEEPL]: {
url: "https://api-free.deepl.com/v2/translate", url: "https://api-free.deepl.com/v2/translate",
key: "", key: "",
fetchLimit: 1,
fetchInterval: 500,
},
[OPT_TRANS_DEEPLFREE]: {
fetchLimit: 1,
fetchInterval: 500,
}, },
[OPT_TRANS_DEEPLX]: { [OPT_TRANS_DEEPLX]: {
url: "http://localhost:1188/translate", url: "http://localhost:1188/translate",
key: "", key: "",
fetchLimit: 1,
fetchInterval: 500,
}, },
[OPT_TRANS_OPENAI]: { [OPT_TRANS_OPENAI]: {
url: "https://api.openai.com/v1/chat/completions", url: "https://api.openai.com/v1/chat/completions",
key: "", key: "",
model: "gpt-4", model: "gpt-4",
prompt: `You will be provided with a sentence in ${PROMPT_PLACE_FROM}, and your task is to translate it into ${PROMPT_PLACE_TO}.`, prompt: `You will be provided with a sentence in ${PROMPT_PLACE_FROM}, and your task is to translate it into ${PROMPT_PLACE_TO}.`,
fetchLimit: 1,
fetchInterval: 500,
}, },
[OPT_TRANS_GEMINI]: { [OPT_TRANS_GEMINI]: {
url: "https://generativelanguage.googleapis.com/v1/models", url: "https://generativelanguage.googleapis.com/v1/models",
key: "", key: "",
model: "gemini-pro", model: "gemini-pro",
prompt: `Translate the following text from ${PROMPT_PLACE_FROM} to ${PROMPT_PLACE_TO}:\n\n${PROMPT_PLACE_TEXT}`, prompt: `Translate the following text from ${PROMPT_PLACE_FROM} to ${PROMPT_PLACE_TO}:\n\n${PROMPT_PLACE_TEXT}`,
fetchLimit: 1,
fetchInterval: 500,
}, },
[OPT_TRANS_CLOUDFLAREAI]: { [OPT_TRANS_CLOUDFLAREAI]: {
url: "https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/ai/run/@cf/meta/m2m100-1.2b", url: "https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/ai/run/@cf/meta/m2m100-1.2b",
key: "", key: "",
fetchLimit: 1,
fetchInterval: 500,
}, },
[OPT_TRANS_CUSTOMIZE]: { [OPT_TRANS_CUSTOMIZE]: defaultCustomApi,
url: "", [OPT_TRANS_CUSTOMIZE_2]: defaultCustomApi,
key: "", [OPT_TRANS_CUSTOMIZE_3]: defaultCustomApi,
}, [OPT_TRANS_CUSTOMIZE_4]: defaultCustomApi,
[OPT_TRANS_CUSTOMIZE_5]: defaultCustomApi,
}; };
// 默认快捷键 // 默认快捷键
@@ -434,30 +501,37 @@ export const DEFAULT_BLACKLIST = [
"oapi.dingtalk.com", "oapi.dingtalk.com",
"login.dingtalk.com", "login.dingtalk.com",
]; // 禁用翻译名单 ]; // 禁用翻译名单
export const DEFAULT_CSPLIST = ["https://github.com"]; // 禁用CSP名单
export const DEFAULT_SETTING = { export const DEFAULT_SETTING = {
darkMode: false, // 深色模式 darkMode: false, // 深色模式
uiLang: "en", // 界面语言 uiLang: "en", // 界面语言
fetchLimit: DEFAULT_FETCH_LIMIT, // 最大任务数量 // fetchLimit: DEFAULT_FETCH_LIMIT, // 最大任务数量(移至transApis作废)
fetchInterval: DEFAULT_FETCH_INTERVAL, // 任务间隔时间 // fetchInterval: DEFAULT_FETCH_INTERVAL, // 任务间隔时间(移至transApis作废)
minLength: TRANS_MIN_LENGTH, minLength: TRANS_MIN_LENGTH,
maxLength: TRANS_MAX_LENGTH, maxLength: TRANS_MAX_LENGTH,
newlineLength: TRANS_NEWLINE_LENGTH, newlineLength: TRANS_NEWLINE_LENGTH,
clearCache: false, // 是否在浏览器下次启动时清除缓存 clearCache: false, // 是否在浏览器下次启动时清除缓存
injectRules: true, // 是否注入订阅规则 injectRules: true, // 是否注入订阅规则
injectWebfix: true, // 是否注入修复补丁 // injectWebfix: true, // 是否注入修复补丁(作废)
detectRemote: false, // 是否使用远程语言检测 // detectRemote: false, // 是否使用远程语言检测(移至rule作废)
contextMenus: true, // 是否添加右键菜单 // contextMenus: true, // 是否添加右键菜单(作废)
contextMenuType: 1, // 右键菜单类型(0不显示1简单菜单2多级菜单)
// transTag: DEFAULT_TRANS_TAG, // 译文元素标签(移至rule作废)
// transOnly: false, // 是否仅显示译文(移至rule作废)
// transTitle: false, // 是否同时翻译页面标题(移至rule作废)
subrulesList: DEFAULT_SUBRULES_LIST, // 订阅列表 subrulesList: DEFAULT_SUBRULES_LIST, // 订阅列表
owSubrule: DEFAULT_OW_RULE, // 覆写订阅规则 owSubrule: DEFAULT_OW_RULE, // 覆写订阅规则
transApis: DEFAULT_TRANS_APIS, // 翻译接口 transApis: DEFAULT_TRANS_APIS, // 翻译接口
mouseKey: OPT_MOUSEKEY_DISABLE, // 鼠标悬停翻译 // mouseKey: OPT_TIMING_PAGESCROLL, // 翻译时机/鼠标悬停翻译(移至rule作废)
shortcuts: DEFAULT_SHORTCUTS, // 快捷键 shortcuts: DEFAULT_SHORTCUTS, // 快捷键
inputRule: DEFAULT_INPUT_RULE, // 输入框设置 inputRule: DEFAULT_INPUT_RULE, // 输入框设置
tranboxSetting: DEFAULT_TRANBOX_SETTING, // 划词翻译设置 tranboxSetting: DEFAULT_TRANBOX_SETTING, // 划词翻译设置
touchTranslate: 2, // 触屏翻译 touchTranslate: 2, // 触屏翻译
blacklist: DEFAULT_BLACKLIST.join(",\n"), // 禁用翻译名单 blacklist: DEFAULT_BLACKLIST.join(",\n"), // 禁用翻译名单
disableLangs: [], // 不翻译的语言 csplist: DEFAULT_CSPLIST.join(",\n"), // 禁用CSP名单
// disableLangs: [], // 不翻译的语言(移至rule作废)
transInterval: 500, // 翻译间隔时间
}; };
export const DEFAULT_RULES = [GLOBLA_RULE]; export const DEFAULT_RULES = [GLOBLA_RULE];

View File

@@ -1,22 +1,35 @@
export const DEFAULT_SELECTOR = `:is(li, p, h1, h2, h3, h4, h5, h6, dd, blockquote)`; import { FIXER_BR, FIXER_BN, FIXER_BR_DIV, FIXER_BN_DIV } from "../libs/webfix";
export const DEFAULT_KEEP_SELECTOR = `code, img, svg`;
export const GLOBAL_KEY = "*"; export const GLOBAL_KEY = "*";
export const REMAIN_KEY = "-"; export const REMAIN_KEY = "-";
export const SHADOW_KEY = ">>>"; export const SHADOW_KEY = ">>>";
export const DEFAULT_SELECTOR = `:is(li, p, h1, h2, h3, h4, h5, h6, dd, blockquote, .kiss-p)`;
export const DEFAULT_KEEP_SELECTOR = `code, img, svg, pre`;
export const DEFAULT_RULE = { export const DEFAULT_RULE = {
pattern: "", pattern: "", // 匹配网址
selector: "", selector: "", // 选择器
keepSelector: "", keepSelector: "", // 保留元素选择器
translator: GLOBAL_KEY, terms: "", // 专业术语
fromLang: GLOBAL_KEY, translator: GLOBAL_KEY, // 翻译服务
toLang: GLOBAL_KEY, fromLang: GLOBAL_KEY, // 源语言
textStyle: GLOBAL_KEY, toLang: GLOBAL_KEY, // 目标语言
transOpen: GLOBAL_KEY, textStyle: GLOBAL_KEY, // 译文样式
bgColor: "", transOpen: GLOBAL_KEY, // 开启翻译
textDiyStyle: "", bgColor: "", // 译文颜色
textDiyStyle: "", // 自定义译文样式
selectStyle: "", // 选择器节点样式
parentStyle: "", // 选择器父节点样式
injectJs: "", // 注入JS
injectCss: "", // 注入CSS
transOnly: GLOBAL_KEY, // 是否仅显示译文
transTiming: GLOBAL_KEY, // 翻译时机/鼠标悬停翻译
transTag: GLOBAL_KEY, // 译文元素标签
transTitle: GLOBAL_KEY, // 是否同时翻译页面标题
detectRemote: GLOBAL_KEY, // 是否使用远程语言检测
skipLangs: [], // 不翻译的语言
fixerSelector: "", // 修复函数选择器
fixerFunc: GLOBAL_KEY, // 修复函数
}; };
const DEFAULT_DIY_STYLE = `color: #666; const DEFAULT_DIY_STYLE = `color: #666;
@@ -43,154 +56,263 @@ export const DEFAULT_OW_RULE = {
}; };
const RULES_MAP = { const RULES_MAP = {
"www.google.com/search": [`h3, .IsZvec, .VwiC3b`], "www.google.com/search": {
"news.google.com": [`[role="link"], .DY5T1d, .ifw3f, ${DEFAULT_SELECTOR}`], selector: `h3, .IsZvec, .VwiC3b`,
"www.foxnews.com": [ },
`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"]`, "news.google.com": {
], selector: `[data-n-tid], ${DEFAULT_SELECTOR}`,
"bearblog.dev, www.theverge.com, www.tampermonkey.net/documentation.php": [ },
`${DEFAULT_SELECTOR}`, "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"]`,
"themessenger.com": [ },
`.leading-tight, .leading-tighter, .my-2 p, .font-body p, article ${DEFAULT_SELECTOR}`, "bearblog.dev, www.theverge.com, www.tampermonkey.net/documentation.php": {
], selector: `${DEFAULT_SELECTOR}`,
"www.telegraph.co.uk, go.dev/doc/": [`article ${DEFAULT_SELECTOR}`], },
"www.theguardian.com": [ "themessenger.com": {
`.show-underline, .dcr-hup5wm div, .dcr-7vl6y8 div, .dcr-12evv1c, figcaption, article ${DEFAULT_SELECTOR}, [data-cy="mostviewed-footer"] h4`, selector: `.leading-tight, .leading-tighter, .my-2 p, .font-body p, article ${DEFAULT_SELECTOR}`,
], },
"www.semafor.com": [ "www.telegraph.co.uk, go.dev/doc/": {
`${DEFAULT_SELECTOR}, .styles_intro__IYj__, [class*="styles_description"]`, selector: `article ${DEFAULT_SELECTOR}`,
], },
"www.noemamag.com": [ "www.theguardian.com": {
`.splash__title, .single-card__title, .single-card__type, .single-card__topic, .highlighted-content__title, .single-card__author, article ${DEFAULT_SELECTOR}, .quote__text, .wp-caption-text div`, selector: `.show-underline, .dcr-hup5wm div, .dcr-7vl6y8 div, .dcr-12evv1c, figcaption, article ${DEFAULT_SELECTOR}, [data-cy="mostviewed-footer"] h4`,
], },
"restofworld.org": [ "www.semafor.com": {
`${DEFAULT_SELECTOR}, .recirc-story__headline, .recirc-story__dek`, selector: `${DEFAULT_SELECTOR}, .styles_intro__IYj__, [class*="styles_description"]`,
], },
"www.axios.com": [`.h7, ${DEFAULT_SELECTOR}`], "www.noemamag.com": {
"www.newyorker.com": [ selector: `.splash__title, .single-card__title, .single-card__type, .single-card__topic, .highlighted-content__title, .single-card__author, article ${DEFAULT_SELECTOR}, .quote__text, .wp-caption-text div`,
`.summary-item__hed, .summary-item__dek, .summary-collection-grid__dek, .dqtvfu, .rubric__link, .caption, article ${DEFAULT_SELECTOR}, .HEhan ${DEFAULT_SELECTOR}, .ContributorBioBio-fBolsO, .BaseText-ewhhUZ`, },
], "restofworld.org": {
"time.com": [ selector: `${DEFAULT_SELECTOR}, .recirc-story__headline, .recirc-story__dek`,
`h1, h3, .summary, .video-title, #article-body ${DEFAULT_SELECTOR}, .image-wrap-container .credit.body-caption, .media-heading`, },
], "www.axios.com": {
"www.dw.com": [ selector: `.h7, ${DEFAULT_SELECTOR}`,
`.ts-teaser-title a, .news-title a, .title a, .teaser-description a, .hbudab h3, .hbudab p, figcaption ,article ${DEFAULT_SELECTOR}`, },
], "www.newyorker.com": {
"www.bbc.com": [ selector: `.summary-item__hed, .summary-item__dek, .summary-collection-grid__dek, .dqtvfu, .rubric__link, .caption, article ${DEFAULT_SELECTOR}, .HEhan ${DEFAULT_SELECTOR}, .ContributorBioBio-fBolsO, .BaseText-ewhhUZ`,
`h1, h2, .media__link, .media__summary, article ${DEFAULT_SELECTOR}, .ssrcss-y7krbn-Stack, .ssrcss-17zglt8-PromoHeadline, .ssrcss-18cjaf3-Headline, .gs-c-promo-heading__title, .gs-c-promo-summary, .media__content h3, .article__intro, .lx-c-summary-points>li`, },
], "time.com": {
"www.chinadaily.com.cn": [ selector: `h1, h3, .summary, .video-title, #article-body ${DEFAULT_SELECTOR}, .image-wrap-container .credit.body-caption, .media-heading`,
`h1, .tMain [shape="rect"], .cMain [shape="rect"], .photo_art [shape="rect"], .mai_r [shape="rect"], .lisBox li, #Content ${DEFAULT_SELECTOR}`, },
], "www.dw.com": {
"www.facebook.com": [`[role="main"] [dir="auto"]`], selector: `.ts-teaser-title a, .news-title a, .title a, .teaser-description a, .hbudab h3, .hbudab p, figcaption ,article ${DEFAULT_SELECTOR}`,
"www.reddit.com": [ },
`div:is(.tbIApBd2DM_drfZQJjIum, ._1zPvgKHteTOub9dKkvrOl4,.ULWj94BYSOqoJDetxgcnU),a:is([class^="_334yl59"],[class^="_2GrMpxD"]),h1,h2,h3,h4,h5,h6,p,button`, "www.bbc.com": {
], selector: `h1, h2, .media__link, .media__summary, article ${DEFAULT_SELECTOR}, .ssrcss-y7krbn-Stack, .ssrcss-17zglt8-PromoHeadline, .ssrcss-18cjaf3-Headline, .gs-c-promo-heading__title, .gs-c-promo-summary, .media__content h3, .article__intro, .lx-c-summary-points>li`,
"www.quora.com": [`.qu-wordBreak--break-word`], },
"edition.cnn.com": [ "www.chinadaily.com.cn": {
`.container__title, .container__headline, .headline__text, .image__caption, [data-type="Title"], .article__content ${DEFAULT_SELECTOR}`, selector: `h1, .tMain [shape="rect"], .cMain [shape="rect"], .photo_art [shape="rect"], .mai_r [shape="rect"], .lisBox li, #Content ${DEFAULT_SELECTOR}`,
], },
"www.reuters.com": [ "www.facebook.com": {
`#main-content [data-testid="Heading"], #main-content [data-testid="Body"], .article-body__content__17Yit ${DEFAULT_SELECTOR}`, selector: `[role="main"] [dir="auto"]`,
], },
"www.bloomberg.com": [ "www.reddit.com, new.reddit.com, sh.reddit.com": {
`[data-component="headline"], [data-component="related-item-headline"], [data-component="title"], article ${DEFAULT_SELECTOR}`, selector: `:is(#AppRouter-main-content, #overlayScrollContainer) :is([class^=tbIA],[class^=_1zP],[class^=ULWj],[class^=_2Jj], [class^=_334],[class^=_2Gr],[class^=_7T4],[class^=_1WO], ${DEFAULT_SELECTOR}); [id^="post-title"], :is([slot="text-body"], [slot="comment"]) ${DEFAULT_SELECTOR}, recent-posts h3, aside :is(span:has(>h2), p); shreddit-subreddit-header >>> :is(#title, #description)`,
], },
"deno.land, docs.github.com": [`main ${DEFAULT_SELECTOR}`, `code, img, svg`], "www.quora.com": {
"doc.rust-lang.org": [`.content ${DEFAULT_SELECTOR}`, `code, img, svg`], selector: `.qu-wordBreak--break-word`,
"www.indiehackers.com": [ },
`h1, h3, .content ${DEFAULT_SELECTOR}, .feed-item__title-link`, "edition.cnn.com": {
], selector: `.container__title, .container__headline, .headline__text, .image__caption, [data-type="Title"], .article__content ${DEFAULT_SELECTOR}`,
"platform.openai.com/docs": [ },
`.docs-body ${DEFAULT_SELECTOR}`, "www.reuters.com": {
`code, img, svg`, selector: `#main-content [data-testid="Heading"], #main-content [data-testid="Body"], .article-body__content__17Yit ${DEFAULT_SELECTOR}`,
], },
"en.wikipedia.org": [ "www.bloomberg.com": {
`h1, .mw-parser-output ${DEFAULT_SELECTOR}`, selector: `[data-component="headline"], [data-component="related-item-headline"], [data-component="title"], article ${DEFAULT_SELECTOR}`,
`.mwe-math-element`, },
], "deno.land, docs.github.com": {
"stackoverflow.com": [ selector: `main ${DEFAULT_SELECTOR}`,
`h1, .s-prose p, .comment-body .comment-copy`, keepSelector: DEFAULT_KEEP_SELECTOR,
`code, img, svg`, },
], "doc.rust-lang.org": {
"www.npmjs.com/package, developer.chrome.com/docs, medium.com, developers.cloudflare.com, react.dev, create-react-app.dev, pytorch.org": selector: `.content ${DEFAULT_SELECTOR}`,
[`article ${DEFAULT_SELECTOR}`], keepSelector: DEFAULT_KEEP_SELECTOR,
"news.ycombinator.com": [`.title, .commtext`], },
"github.com": [ "www.indiehackers.com": {
`.markdown-body ${DEFAULT_SELECTOR}, .repo-description p, .Layout-sidebar .f4, .container-lg .py-4 .f5, .container-lg .my-4 .f5, .Box-row .pr-4, .Box-row article .mt-1, [itemprop="description"], .markdown-title, bdi, .ws-pre-wrap, .status-meta, span.status-meta, .col-10.color-fg-muted, .TimelineItem-body, .pinned-item-list-item-content .color-fg-muted, .markdown-body td, .markdown-body th`, selector: `h1, h3, .content ${DEFAULT_SELECTOR}, .feed-item__title-link`,
`code, img, svg`, },
], "platform.openai.com/docs": {
"twitter.com": [ selector: `.docs-body ${DEFAULT_SELECTOR}`,
`[data-testid="tweetText"], [data-testid="birdwatch-pivot"]>div.css-1rynq56`, keepSelector: DEFAULT_KEEP_SELECTOR,
`img, a, .r-18u37iz, .css-175oi2r`, },
], "en.wikipedia.org": {
"m.youtube.com": [ selector: `h1, .mw-parser-output ${DEFAULT_SELECTOR}`,
`.slim-video-information-title .yt-core-attributed-string, .media-item-headline .yt-core-attributed-string, .comment-text .yt-core-attributed-string, .typography-body-2b .yt-core-attributed-string, #ytp-caption-window-container .ytp-caption-segment`, keepSelector: `.mwe-math-element`,
], },
"www.youtube.com": [ "stackoverflow.com, serverfault.com, superuser.com, stackexchange.com, askubuntu.com, stackapps.com, mathoverflow.net":
`h1, #video-title, #content-text, #title, yt-attributed-string>span>span, #ytp-caption-window-container .ytp-caption-segment`, {
], selector: `.s-prose ${DEFAULT_SELECTOR}, .comment-copy, .question-hyperlink, .s-post-summary--content-title, .s-post-summary--content-excerpt`,
"bard.google.com": [ keepSelector: `${DEFAULT_KEEP_SELECTOR}, .math-container`,
`.query-content ${DEFAULT_SELECTOR}, message-content ${DEFAULT_SELECTOR}`, },
], "www.npmjs.com/package, developer.chrome.com/docs, medium.com, react.dev, create-react-app.dev, pytorch.org":
"www.bing.com": [ {
`.b_algoSlug, .rwrl_padref; .cib-serp-main >>> .ac-textBlock ${DEFAULT_SELECTOR}, .text-message-content div`, selector: `article ${DEFAULT_SELECTOR}`,
], },
"www.phoronix.com": [`article ${DEFAULT_SELECTOR}`], "news.ycombinator.com": {
"wx2.qq.com": [`.js_message_plain`], selector: `.title, p`,
"app.slack.com/client/": [ fixerSelector: `.toptext, .commtext`,
`.p-rich_text_section, .c-message_attachment__text, .p-rich_text_list li`, fixerFunc: FIXER_BR,
], },
"discord.com/channels/": [ "github.com": {
`li[id^=chat-messages] div[id^=message-content], div[class^=headerText], div[class^=name_], section[aria-label='Search Results'] div[id^=message-content]`, selector: `.markdown-body ${DEFAULT_SELECTOR}, .repo-description p, .Layout-sidebar .f4, .container-lg .py-4 .f5, .container-lg .my-4 .f5, .Box-row .pr-4, .Box-row article .mt-1, [itemprop="description"], .markdown-title, bdi, .ws-pre-wrap, .status-meta, span.status-meta, .col-10.color-fg-muted, .TimelineItem-body, .pinned-item-list-item-content .color-fg-muted, .markdown-body td, .markdown-body th`,
], keepSelector: DEFAULT_KEEP_SELECTOR,
"t.me/s/": [`.js-message_text ${DEFAULT_SELECTOR}`], },
"web.telegram.org/k/": [ "twitter.com": {
`.message, .bot-commands-list-element-description, .reply-markup-button-text`, selector: `[data-testid="tweetText"], [data-testid="birdwatch-pivot"]>div.css-1rynq56`,
], keepSelector: `img, a, .r-18u37iz, .css-175oi2r`,
"web.telegram.org/a/": [ },
`.message, .text-content, .bot-commands-list-element-description, .reply-markup-button-text`, "m.youtube.com": {
], selector: `.slim-video-information-title .yt-core-attributed-string, .media-item-headline .yt-core-attributed-string, .comment-text .yt-core-attributed-string, .typography-body-2b .yt-core-attributed-string, #ytp-caption-window-container .ytp-caption-segment`,
"chromereleases.googleblog.com": [ selectStyle: `-webkit-line-clamp: unset; max-height: none; height: auto;`,
`.title, .publishdate, p, i, .header-desc, .header-title, .text`, parentStyle: `-webkit-line-clamp: unset; max-height: none; height: auto;`,
], keepSelector: `img, #content-text>a`,
"www.instagram.com/": [`h1, article span[dir=auto] > span[dir=auto], ._ab1y`], },
"www.instagram.com/p/,www.instagram.com/reels/": [ "www.youtube.com": {
`h1, div[class='x9f619 xjbqb8w x78zum5 x168nmei x13lgxp2 x5pf9jr xo71vjh x1uhb9sk x1plvlek xryxfnj x1c4vz4f x2lah0s xdt5ytf xqjyukv x1cy8zhl x1oa3qoh x1nhvcw1'] > span[class='x1lliihq x1plvlek xryxfnj x1n2onr6 x193iq5w xeuugli x1fj9vlw x13faqbe x1vvkbs x1s928wv xhkezso x1gmr53x x1cpjm7i x1fgarty x1943h6x x1i0vuye xvs91rp xo1l8bm x5n08af x10wh9bi x1wdrske x8viiok x18hxmgj'], span[class='x193iq5w xeuugli x1fj9vlw x13faqbe x1vvkbs xt0psk2 x1i0vuye xvs91rp xo1l8bm x5n08af x10wh9bi x1wdrske x8viiok x18hxmgj']`, selector: `h1, #video-title, #content-text, #title, yt-attributed-string>span>span, #ytp-caption-window-container .ytp-caption-segment`,
], selectStyle: `-webkit-line-clamp: unset; max-height: none; height: auto;`,
"mail.google.com": [ parentStyle: `-webkit-line-clamp: unset; max-height: none; height: auto;`,
`${DEFAULT_SELECTOR}, h2[data-thread-perm-id], span[data-thread-id], div[data-message-id] div[class=''], .messageBody, #views`, keepSelector: `img, #content-text>a`,
], },
"web.whatsapp.com": [`.copyable-text > span`], "bard.google.com": {
"chat.openai.com": [ selector: `.query-content ${DEFAULT_SELECTOR}, message-content ${DEFAULT_SELECTOR}`,
`div[data-message-author-role] > div ${DEFAULT_SELECTOR}`, },
], "www.bing.com, copilot.microsoft.com": {
"forum.ru-board.com": [`.tit, .dats, span.post, .lgf ${DEFAULT_SELECTOR}`], selector: `.b_algoSlug, .rwrl_padref; .cib-serp-main >>> .ac-textBlock ${DEFAULT_SELECTOR}, .text-message-content div`,
"education.github.com": [ },
`${DEFAULT_SELECTOR}, a, summary, span.Button-content`, "www.phoronix.com": {
], selector: `article ${DEFAULT_SELECTOR}`,
"blogs.windows.com": [`${DEFAULT_SELECTOR}, .c-uhf-nav-link, figcaption`], fixerSelector: `.content`,
"developer.apple.com/documentation/": [ fixerFunc: FIXER_BR,
`#main ${DEFAULT_SELECTOR}, #main .abstract .content, #main .abstract.content, #main .link span`, },
`code, img, svg`, "wx2.qq.com": {
], selector: `.js_message_plain`,
"greasyfork.org": [ },
`h2, .script-link, .script-description, #additional-info ${DEFAULT_SELECTOR}`, "app.slack.com/client/": {
], selector: `.p-rich_text_section, .c-message_attachment__text, .p-rich_text_list li`,
"www.fmkorea.com": [`#container ${DEFAULT_SELECTOR}`], },
"forum.arduino.cc": [ "discord.com/channels/": {
`.top-row>.title, .featured-topic>.title, .link-top-line>.title, .category-description, .topic-excerpt, .fancy-title, .cooked ${DEFAULT_SELECTOR}`, selector: `div[class^=message], div[class^=headerText], div[class^=name_], section[aria-label='Search Results'] div[id^=message-content], div[id^=message]`,
], keepSelector: `li[class^='card'] div[class^='message'], [class^='embedFieldValue'], [data-list-item-id^='forum-channel-list'] div[class^='headerText']`,
"docs.arduino.cc": [`[class^="tutorial-module--left"] ${DEFAULT_SELECTOR}`], },
"www.historydefined.net": [`.wp-element-caption, ${DEFAULT_SELECTOR}`], "t.me/s/": {
selector: `.js-message_text ${DEFAULT_SELECTOR}`,
fixerSelector: `.tgme_widget_message_text`,
fixerFunc: FIXER_BR,
},
"web.telegram.org/k": {
selector: `div.kiss-p`,
keepSelector: `div[class^=time], .peer-title, .document-wrapper, .message.spoilers-container custom-emoji-element, reactions-element`,
fixerSelector: `.message`,
fixerFunc: FIXER_BN_DIV,
},
"web.telegram.org/a": {
selector: `.text-content > .kiss-p`,
keepSelector: `.Reactions, .time, .peer-title, .document-wrapper, .message.spoilers-container custom-emoji-element`,
fixerSelector: `.text-content`,
fixerFunc: FIXER_BR_DIV,
},
"www.instagram.com/": {
selector: `h1, article span[dir=auto] > span[dir=auto], ._ab1y`,
},
"www.instagram.com/p/,www.instagram.com/reels/": {
selector: `h1, div[class='x9f619 xjbqb8w x78zum5 x168nmei x13lgxp2 x5pf9jr xo71vjh x1uhb9sk x1plvlek xryxfnj x1c4vz4f x2lah0s xdt5ytf xqjyukv x1cy8zhl x1oa3qoh x1nhvcw1'] > span[class='x1lliihq x1plvlek xryxfnj x1n2onr6 x193iq5w xeuugli x1fj9vlw x13faqbe x1vvkbs x1s928wv xhkezso x1gmr53x x1cpjm7i x1fgarty x1943h6x x1i0vuye xvs91rp xo1l8bm x5n08af x10wh9bi x1wdrske x8viiok x18hxmgj'], span[class='x193iq5w xeuugli x1fj9vlw x13faqbe x1vvkbs xt0psk2 x1i0vuye xvs91rp xo1l8bm x5n08af x10wh9bi x1wdrske x8viiok x18hxmgj']`,
},
"mail.google.com": {
selector: `.a3s.aiL ${DEFAULT_SELECTOR}, span[data-thread-id]`,
fixerSelector: `.a3s.aiL`,
fixerFunc: FIXER_BR,
},
"web.whatsapp.com": {
selector: `.copyable-text > span`,
},
"chat.openai.com": {
selector: `div[data-message-author-role] > div ${DEFAULT_SELECTOR}`,
fixerSelector: `div[data-message-author-role='user'] > div`,
fixerFunc: FIXER_BN,
},
"forum.ru-board.com": {
selector: `.tit, .dats, .kiss-p, .lgf ${DEFAULT_SELECTOR}`,
fixerSelector: `span.post`,
fixerFunc: FIXER_BR,
},
"education.github.com": {
selector: `${DEFAULT_SELECTOR}, a, summary, span.Button-content`,
},
"blogs.windows.com": {
selector: `${DEFAULT_SELECTOR}, .c-uhf-nav-link, figcaption`,
fixerSelector: `.t-content>div>ul>li`,
fixerFunc: FIXER_BR,
},
"developer.apple.com/documentation/": {
selector: `#main ${DEFAULT_SELECTOR}, #main .abstract .content, #main .abstract.content, #main .link span`,
keepSelector: DEFAULT_KEEP_SELECTOR,
},
"greasyfork.org": {
selector: `h2, .script-link, .script-description, #additional-info ${DEFAULT_SELECTOR}`,
},
"www.fmkorea.com": {
selector: `#container ${DEFAULT_SELECTOR}`,
},
"forum.arduino.cc": {
selector: `.top-row>.title, .featured-topic>.title, .link-top-line>.title, .category-description, .topic-excerpt, .fancy-title, .cooked ${DEFAULT_SELECTOR}`,
},
"docs.arduino.cc": {
selector: `[class^="tutorial-module--left"] ${DEFAULT_SELECTOR}`,
},
"www.historydefined.net": {
selector: `.wp-element-caption, ${DEFAULT_SELECTOR}`,
},
"gobyexample.com": {
selector: `.docs p`,
keepSelector: `code`,
},
"go.dev/tour": {
selector: `#left-side ${DEFAULT_SELECTOR}`,
keepSelector: `code, img, svg >>> code`,
},
"pkg.go.dev": {
selector: `.Documentation-content ${DEFAULT_SELECTOR}`,
keepSelector: `${DEFAULT_KEEP_SELECTOR}, a, span`,
},
"docs.rs": {
selector: `.docblock ${DEFAULT_SELECTOR}, .docblock-short`,
keepSelector: `code >>> code`,
},
"randomnerdtutorials.com": {
selector: `article ${DEFAULT_SELECTOR}`,
},
"notebooks.githubusercontent.com/view/ipynb": {
selector: `#notebook-container ${DEFAULT_SELECTOR}`,
keepSelector: DEFAULT_KEEP_SELECTOR,
},
"developers.cloudflare.com": {
selector: `article ${DEFAULT_SELECTOR}, .WorkerStarter--description`,
keepSelector: `a[rel='noopener'], code`,
},
"ubuntuforums.org": {
fixerSelector: `.postcontent`,
fixerFunc: FIXER_BR,
},
"play.google.com/store/apps/details": {
fixerSelector: `[data-g-id="description"]`,
fixerFunc: FIXER_BR,
},
"news.yahoo.co.jp/articles/": {
fixerSelector: `.sc-cTsKDU`,
fixerFunc: FIXER_BN,
},
"chromereleases.googleblog.com": {
fixerSelector: `.post-content, .post-content > span, li > span`,
fixerFunc: FIXER_BR,
},
}; };
export const BUILTIN_RULES = Object.entries(RULES_MAP) export const BUILTIN_RULES = Object.entries(RULES_MAP)
.sort((a, b) => a[0].localeCompare(b[0])) .sort((a, b) => a[0].localeCompare(b[0]))
.map(([pattern, [selector, keepSelector = ""]]) => ({ .map(([pattern, rule]) => ({
...DEFAULT_RULE, ...DEFAULT_RULE,
...rule,
pattern, pattern,
selector,
keepSelector,
})); }));

61
src/hooks/Audio.js Normal file
View File

@@ -0,0 +1,61 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { apiBaiduTTS } from "../apis";
import { kissLog } from "../libs/log";
/**
* 声音播放hook
* @param {*} src
* @returns
*/
export function useAudio(src) {
const audioRef = useRef(null);
const [error, setError] = useState(null);
const [ready, setReady] = useState(false);
const [playing, setPlaying] = useState(false);
const onPlay = useCallback(() => {
audioRef.current?.play();
}, []);
useEffect(() => {
if (!src) {
return;
}
const audio = new Audio(src);
audio.addEventListener("error", (err) => setError(err));
audio.addEventListener("canplaythrough", () => setReady(true));
audio.addEventListener("play", () => setPlaying(true));
audio.addEventListener("ended", () => setPlaying(false));
audioRef.current = audio;
}, [src]);
return {
error,
ready,
playing,
onPlay,
};
}
/**
* 获取语音hook
* @param {*} text
* @param {*} lan
* @param {*} spd
* @returns
*/
export function useTextAudio(text, lan = "uk", spd = 3) {
const [src, setSrc] = useState("");
useEffect(() => {
(async () => {
try {
setSrc(await apiBaiduTTS(text, lan, spd));
} catch (err) {
kissLog(err, "baidu tts");
}
})();
}, [text, lan, spd]);
return useAudio(src);
}

View File

@@ -3,6 +3,7 @@ import { useCallback, useEffect, useState } from "react";
import { trySyncWords } from "../libs/sync"; import { trySyncWords } from "../libs/sync";
import { getWordsWithDefault, setWords } from "../libs/storage"; import { getWordsWithDefault, setWords } from "../libs/storage";
import { useSyncMeta } from "./Sync"; import { useSyncMeta } from "./Sync";
import { kissLog } from "../libs/log";
export function useFavWords() { export function useFavWords() {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -56,7 +57,7 @@ export function useFavWords() {
const favWords = await getWordsWithDefault(); const favWords = await getWordsWithDefault();
setFavWords(favWords); setFavWords(favWords);
} catch (err) { } catch (err) {
console.log("[query fav]", err); kissLog(err, "query fav");
} finally { } finally {
setLoading(false); setLoading(false);
} }

View File

@@ -1,11 +1,12 @@
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { storage } from "../libs/storage"; import { storage } from "../libs/storage";
import { kissLog } from "../libs/log";
/** /**
* *
* @param {*} key * @param {*} key
* @param {*} defaultVal 需为调用hook外的常量 * @param {*} defaultVal 需为调用hook外的常量
* @returns * @returns
*/ */
export function useStorage(key, defaultVal) { export function useStorage(key, defaultVal) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -40,7 +41,7 @@ export function useStorage(key, defaultVal) {
setData(val); setData(val);
} }
} catch (err) { } catch (err) {
console.log("[storage reload]", err.message); kissLog(err, "storage reload");
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -58,7 +59,7 @@ export function useStorage(key, defaultVal) {
await storage.setObj(key, defaultVal); await storage.setObj(key, defaultVal);
} }
} catch (err) { } catch (err) {
console.log("[storage load]", err.message); kissLog(err, "storage load");
} finally { } finally {
setLoading(false); setLoading(false);
} }

View File

@@ -3,6 +3,7 @@ import { useSetting } from "./Setting";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { loadOrFetchSubRules } from "../libs/subRules"; import { loadOrFetchSubRules } from "../libs/subRules";
import { delSubRules } from "../libs/storage"; import { delSubRules } from "../libs/storage";
import { kissLog } from "../libs/log";
/** /**
* 订阅规则 * 订阅规则
@@ -72,7 +73,7 @@ export function useSubRules() {
const rules = await loadOrFetchSubRules(selectedUrl); const rules = await loadOrFetchSubRules(selectedUrl);
setSelectedRules(rules); setSelectedRules(rules);
} catch (err) { } catch (err) {
console.log("[loadOrFetchSubRules]", err); kissLog(err, "loadOrFetchSubRules");
} finally { } finally {
setLoading(false); setLoading(false);
} }

View File

@@ -3,6 +3,7 @@ import { useState } from "react";
import { tryDetectLang } from "../libs"; import { tryDetectLang } from "../libs";
import { apiTranslate } from "../apis"; import { apiTranslate } from "../apis";
import { DEFAULT_TRANS_APIS } from "../config"; import { DEFAULT_TRANS_APIS } from "../config";
import { kissLog } from "../libs/log";
/** /**
* 翻译hook * 翻译hook
@@ -16,19 +17,21 @@ export function useTranslate(q, rule, setting) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [sameLang, setSamelang] = useState(false); const [sameLang, setSamelang] = useState(false);
const { translator, fromLang, toLang } = rule; const { translator, fromLang, toLang, detectRemote, skipLangs = [] } = rule;
useEffect(() => { useEffect(() => {
(async () => { (async () => {
try { try {
setLoading(true); setLoading(true);
const deLang = await tryDetectLang(q, setting.detectRemote); if (!q.replace(/\[(\d+)\]/g, "").trim()) {
const disableLangs = setting.disableLangs || []; setText(q);
if ( setSamelang(false);
deLang && return;
(toLang.includes(deLang) || disableLangs.includes(deLang)) }
) {
const deLang = await tryDetectLang(q, detectRemote === "true");
if (deLang && (toLang.includes(deLang) || skipLangs.includes(deLang))) {
setSamelang(true); setSamelang(true);
} else { } else {
const [trText, isSame] = await apiTranslate({ const [trText, isSame] = await apiTranslate({
@@ -43,12 +46,12 @@ export function useTranslate(q, rule, setting) {
setSamelang(isSame); setSamelang(isSame);
} }
} catch (err) { } catch (err) {
console.log("[translate]", err); kissLog(err, "translate");
} finally { } finally {
setLoading(false); setLoading(false);
} }
})(); })();
}, [q, translator, fromLang, toLang, setting]); }, [q, translator, fromLang, toLang, detectRemote, skipLangs, setting]);
return { text, sameLang, loading }; return { text, sameLang, loading };
} }

View File

@@ -1,58 +0,0 @@
import { STOKEY_WFRULES, KV_WFRULES_KEY } from "../config";
import { useStorage } from "./Storage";
import { trySyncWebfixRules } from "../libs/sync";
import { useCallback } from "react";
import { useSyncMeta } from "./Sync";
const DEFAULT_WFRULES = [];
/**
* 修复规则 hook
* @returns
*/
export function useWebfixRules() {
const { data: list, save } = useStorage(STOKEY_WFRULES, DEFAULT_WFRULES);
const { updateSyncMeta } = useSyncMeta();
const updateRules = useCallback(
async (rules) => {
await save(rules);
await updateSyncMeta(KV_WFRULES_KEY);
trySyncWebfixRules();
},
[save, updateSyncMeta]
);
const add = useCallback(
async (rule) => {
const rules = [...list];
if (rules.map((item) => item.pattern).includes(rule.pattern)) {
return;
}
rules.unshift(rule);
await updateRules(rules);
},
[list, updateRules]
);
const del = useCallback(
async (pattern) => {
let rules = [...list];
rules = rules.filter((item) => item.pattern !== pattern);
await updateRules(rules);
},
[list, updateRules]
);
const put = useCallback(
async (pattern, obj) => {
const rules = [...list];
const rule = rules.find((r) => r.pattern === pattern);
rule && Object.assign(rule, obj);
await updateRules(rules);
},
[list, updateRules]
);
return { list, add, del, put };
}

View File

@@ -1,12 +1,13 @@
import { getMsauth, setMsauth } from "./storage"; import { getMsauth, setMsauth } from "./storage";
import { URL_MICROSOFT_AUTH } from "../config"; import { URL_MICROSOFT_AUTH } from "../config";
import { fetchData } from "./fetch"; import { fetchData } from "./fetch";
import { kissLog } from "./log";
const parseMSToken = (token) => { const parseMSToken = (token) => {
try { try {
return JSON.parse(atob(token.split(".")[1])).exp; return JSON.parse(atob(token.split(".")[1])).exp;
} catch (err) { } catch (err) {
console.log("[parseMSToken]", err); kissLog(err, "parseMSToken");
} }
return 0; return 0;
}; };

View File

@@ -1,5 +1,4 @@
import { isMatch } from "./utils"; import { isMatch } from "./utils";
import { DEFAULT_BLACKLIST } from "../config";
/** /**
* 检查是否在黑名单中 * 检查是否在黑名单中
@@ -7,7 +6,5 @@ import { DEFAULT_BLACKLIST } from "../config";
* @param {*} param1 * @param {*} param1
* @returns * @returns
*/ */
export const isInBlacklist = ( export const isInBlacklist = (href, { blacklist }) =>
href, blacklist.split(/\n|,/).some((url) => isMatch(href, url.trim()));
{ blacklist = DEFAULT_BLACKLIST.join(",\n") }
) => blacklist.split(",").some((url) => isMatch(href, url.trim()));

View File

@@ -8,7 +8,7 @@ function _browser() {
try { try {
return require("webextension-polyfill"); return require("webextension-polyfill");
} catch (err) { } catch (err) {
// console.log("[browser]", err.message); // kissLog(err, "browser");
} }
} }

View File

@@ -11,6 +11,10 @@ import {
} from "../config"; } from "../config";
import { isBg } from "./browser"; import { isBg } from "./browser";
import { newCacheReq, newTransReq } from "./req"; import { newCacheReq, newTransReq } from "./req";
import { kissLog } from "./log";
import { blobToBase64 } from "./utils";
const TIMEOUT = 5000;
/** /**
* 油猴脚本的请求封装 * 油猴脚本的请求封装
@@ -26,6 +30,7 @@ export const fetchGM = async (input, { method = "GET", headers, body } = {}) =>
headers, headers,
data: body, data: body,
// withCredentials: true, // withCredentials: true,
timeout: TIMEOUT,
onload: ({ response, responseHeaders, status, statusText, ...opts }) => { onload: ({ response, responseHeaders, status, statusText, ...opts }) => {
const headers = {}; const headers = {};
responseHeaders.split("\n").forEach((line) => { responseHeaders.split("\n").forEach((line) => {
@@ -86,6 +91,10 @@ export const fetchApi = async ({ input, init, transOpts, apiSetting }) => {
} }
} }
if (AbortSignal?.timeout) {
Object.assign(init, { signal: AbortSignal.timeout(TIMEOUT) });
}
return fetch(input, init); return fetch(input, init);
}; };
@@ -118,7 +127,7 @@ export const fetchData = async (
const cache = await caches.open(CACHE_NAME); const cache = await caches.open(CACHE_NAME);
res = await cache.match(cacheReq); res = await cache.match(cacheReq);
} catch (err) { } catch (err) {
console.log("[cache match]", err.message); kissLog(err, "cache match");
} }
} }
@@ -131,13 +140,14 @@ export const fetchData = async (
} }
if (!res?.ok) { if (!res?.ok) {
const cause = { const msg = {
url: input,
status: res.status, status: res.status,
}; };
if (res.headers.get("Content-Type")?.includes("json")) { if (res.headers.get("Content-Type")?.includes("json")) {
cause.body = await res.json(); msg.response = await res.json();
} }
throw new Error(`response: [${res.status}] ${res.statusText}`, { cause }); throw new Error(JSON.stringify(msg));
} }
// 插入缓存 // 插入缓存
@@ -146,7 +156,7 @@ export const fetchData = async (
const cache = await caches.open(CACHE_NAME); const cache = await caches.open(CACHE_NAME);
await cache.put(cacheReq, res.clone()); await cache.put(cacheReq, res.clone());
} catch (err) { } catch (err) {
console.log("[cache put]", err.message); kissLog(err, "cache put");
} }
} }
} }
@@ -154,6 +164,9 @@ export const fetchData = async (
const contentType = res.headers.get("Content-Type"); const contentType = res.headers.get("Content-Type");
if (contentType?.includes("json")) { if (contentType?.includes("json")) {
return await res.json(); return await res.json();
} else if (contentType?.includes("audio")) {
const blob = await res.blob();
return await blobToBase64(blob);
} }
return await res.text(); return await res.text();
}; };
@@ -171,11 +184,7 @@ export const fetchPolyfill = async (input, opts) => {
// 插件 // 插件
if (isExt && !isBg()) { if (isExt && !isBg()) {
const res = await sendBgMsg(MSG_FETCH, { input, opts }); return await sendBgMsg(MSG_FETCH, { input, opts });
if (res.error) {
throw new Error(res.error, { cause: res.cause });
}
return res.data;
} }
// 油猴/网页/BackgroundPage // 油猴/网页/BackgroundPage
@@ -189,10 +198,7 @@ export const fetchPolyfill = async (input, opts) => {
*/ */
export const updateFetchPool = async (interval, limit) => { export const updateFetchPool = async (interval, limit) => {
if (isExt) { if (isExt) {
const res = await sendBgMsg(MSG_FETCH_LIMIT, { interval, limit }); await sendBgMsg(MSG_FETCH_LIMIT, { interval, limit });
if (res.error) {
throw new Error(res.error);
}
} else { } else {
fetchPool.update(interval, limit); fetchPool.update(interval, limit);
} }
@@ -203,10 +209,7 @@ export const updateFetchPool = async (interval, limit) => {
*/ */
export const clearFetchPool = async () => { export const clearFetchPool = async () => {
if (isExt) { if (isExt) {
const res = await sendBgMsg(MSG_FETCH_CLEAR); await sendBgMsg(MSG_FETCH_CLEAR);
if (res.error) {
throw new Error(res.error);
}
} else { } else {
fetchPool.clear(); fetchPool.clear();
} }

View File

@@ -1,6 +1,7 @@
import { CACHE_NAME } from "../config"; import { CACHE_NAME } from "../config";
import { browser } from "./browser"; import { browser } from "./browser";
import { apiBaiduLangdetect } from "../apis"; import { apiBaiduLangdetect } from "../apis";
import { kissLog } from "./log";
/** /**
* 清除缓存数据 * 清除缓存数据
@@ -9,7 +10,7 @@ export const tryClearCaches = async () => {
try { try {
caches.delete(CACHE_NAME); caches.delete(CACHE_NAME);
} catch (err) { } catch (err) {
console.log("[clean caches]", err.message); kissLog(err, "clean caches");
} }
}; };
@@ -25,7 +26,7 @@ export const tryDetectLang = async (q, useRemote = false) => {
try { try {
lang = await apiBaiduLangdetect(q); lang = await apiBaiduLangdetect(q);
} catch (err) { } catch (err) {
console.log("[detect lang remote]", err.message); kissLog(err, "detect lang remote");
} }
} }
@@ -34,7 +35,7 @@ export const tryDetectLang = async (q, useRemote = false) => {
const res = await browser?.i18n?.detectLanguage(q); const res = await browser?.i18n?.detectLanguage(q);
lang = res?.languages?.[0]?.language; lang = res?.languages?.[0]?.language;
} catch (err) { } catch (err) {
console.log("[detect lang local]", err.message); kissLog(err, "detect lang local");
} }
} }

35
src/libs/injector.js Normal file
View File

@@ -0,0 +1,35 @@
// Function to inject inline JavaScript code
export const injectInlineJs = (code) => {
const el = document.createElement("script");
el.setAttribute("data-source", "KISS-Calendar injectInlineJs");
el.setAttribute("type", "text/javascript");
el.textContent = code;
document.body?.appendChild(el);
};
// Function to inject external JavaScript file
export const injectExternalJs = (src) => {
const el = document.createElement("script");
el.setAttribute("data-source", "KISS-Calendar injectExternalJs");
el.setAttribute("type", "text/javascript");
el.setAttribute("src", src);
document.body?.appendChild(el);
};
// Function to inject internal CSS code
export const injectInternalCss = (styles) => {
const el = document.createElement("style");
el.setAttribute("data-source", "KISS-Calendar injectInternalCss");
el.textContent = styles;
document.head?.appendChild(el);
};
// Function to inject external CSS file
export const injectExternalCss = (href) => {
const el = document.createElement("link");
el.setAttribute("data-source", "KISS-Calendar injectExternalCss");
el.setAttribute("rel", "stylesheet");
el.setAttribute("type", "text/css");
el.setAttribute("href", href);
document.head?.appendChild(el);
};

View File

@@ -7,8 +7,8 @@ import {
import { genEventName, removeEndchar, matchInputStr, sleep } from "./utils"; import { genEventName, removeEndchar, matchInputStr, sleep } from "./utils";
import { stepShortcutRegister } from "./shortcut"; import { stepShortcutRegister } from "./shortcut";
import { apiTranslate } from "../apis"; import { apiTranslate } from "../apis";
import { tryDetectLang } from ".";
import { loadingSvg } from "./svg"; import { loadingSvg } from "./svg";
import { kissLog } from "./log";
function isInputNode(node) { function isInputNode(node) {
return node.nodeName === "INPUT" || node.nodeName === "TEXTAREA"; return node.nodeName === "INPUT" || node.nodeName === "TEXTAREA";
@@ -83,7 +83,7 @@ function removeLoading(node, loadingId) {
/** /**
* 输入框翻译 * 输入框翻译
*/ */
export default function inputTranslate ({ export default function inputTranslate({
inputRule: { inputRule: {
transOpen, transOpen,
triggerShortcut, triggerShortcut,
@@ -95,7 +95,6 @@ export default function inputTranslate ({
transSign, transSign,
} = DEFAULT_INPUT_RULE, } = DEFAULT_INPUT_RULE,
transApis, transApis,
detectRemote,
}) { }) {
if (!transOpen) { if (!transOpen) {
return; return;
@@ -156,11 +155,6 @@ export default function inputTranslate ({
try { try {
addLoading(node, loadingId); addLoading(node, loadingId);
const deLang = await tryDetectLang(text, detectRemote);
if (deLang && toLang.includes(deLang)) {
return;
}
const [trText, isSame] = await apiTranslate({ const [trText, isSame] = await apiTranslate({
translator, translator,
text, text,
@@ -194,7 +188,7 @@ export default function inputTranslate ({
collapseToEnd(node); collapseToEnd(node);
} }
} catch (err) { } catch (err) {
console.log("[translate input]", err.message); kissLog(err, "translate input");
} finally { } finally {
removeLoading(node, loadingId); removeLoading(node, loadingId);
} }

12
src/libs/log.js Normal file
View File

@@ -0,0 +1,12 @@
/**
* 日志函数
* @param {*} msg
* @param {*} type
*/
export const kissLog = (msg, type) => {
let prefix = `[KISS-Translator]`;
if (type) {
prefix += `[${type}]`;
}
console.log(`${prefix} ${msg}`);
};

View File

@@ -1,5 +1,22 @@
import { browser } from "./browser"; import { browser } from "./browser";
/**
* 获取当前tab信息
* @returns
*/
export const getCurTab = async () => {
const [tab] = await browser.tabs.query({
active: true,
lastFocusedWindow: true,
});
return tab;
};
export const getCurTabId = async () => {
const tab = await getCurTab();
return tab.id;
};
/** /**
* 发送消息给background * 发送消息给background
* @param {*} action * @param {*} action
@@ -16,15 +33,6 @@ export const sendBgMsg = (action, args) =>
* @returns * @returns
*/ */
export const sendTabMsg = async (action, args) => { export const sendTabMsg = async (action, args) => {
const tabs = await browser.tabs.query({ active: true, currentWindow: true }); const tabId = await getCurTabId();
return browser.tabs.sendMessage(tabs[0].id, { action, args }); return browser.tabs.sendMessage(tabId, { action, args });
};
/**
* 获取当前tab信息
* @returns
*/
export const getTabInfo = async () => {
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
return tabs[0];
}; };

View File

@@ -1,3 +1,5 @@
import { kissLog } from "./log";
/** /**
* 任务池 * 任务池
* @param {*} fn * @param {*} fn
@@ -35,7 +37,7 @@ export const taskPool = (
const res = await fn({ ...args, ...preArgs }); const res = await fn({ ...args, ...preArgs });
resolve(res); resolve(res);
} catch (err) { } catch (err) {
console.log("[task]", retry, err); kissLog(err, "task");
if (retry < maxRetry) { if (retry < maxRetry) {
const retryTimer = setTimeout(() => { const retryTimer = setTimeout(() => {
clearTimeout(retryTimer); clearTimeout(retryTimer);

View File

@@ -26,7 +26,7 @@ const keyMap = new Map();
// 轮询key // 轮询key
const keyPick = (translator, key = "") => { const keyPick = (translator, key = "") => {
const keys = key const keys = key
.split(",") .split(/\n|,/)
.map((item) => item.trim()) .map((item) => item.trim())
.filter(Boolean); .filter(Boolean);

View File

@@ -6,13 +6,14 @@ import {
OPT_STYLE_ALL, OPT_STYLE_ALL,
OPT_LANGS_FROM, OPT_LANGS_FROM,
OPT_LANGS_TO, OPT_LANGS_TO,
OPT_TIMING_ALL,
GLOBLA_RULE, GLOBLA_RULE,
DEFAULT_SUBRULES_LIST,
DEFAULT_OW_RULE,
} from "../config"; } from "../config";
import { loadOrFetchSubRules } from "./subRules"; import { loadOrFetchSubRules } from "./subRules";
import { getRulesWithDefault, setRules } from "./storage"; import { getRulesWithDefault, setRules } from "./storage";
import { trySyncRules } from "./sync"; import { trySyncRules } from "./sync";
import { FIXER_ALL } from "./webfix";
import { kissLog } from "./log";
/** /**
* 根据href匹配规则 * 根据href匹配规则
@@ -22,11 +23,7 @@ import { trySyncRules } from "./sync";
*/ */
export const matchRule = async ( export const matchRule = async (
href, href,
{ { injectRules, subrulesList, owSubrule }
injectRules = true,
subrulesList = DEFAULT_SUBRULES_LIST,
owSubrule = DEFAULT_OW_RULE,
}
) => { ) => {
const rules = await getRulesWithDefault(); const rules = await getRulesWithDefault();
if (injectRules) { if (injectRules) {
@@ -53,20 +50,56 @@ export const matchRule = async (
rules.splice(-1, 0, ...subRules); rules.splice(-1, 0, ...subRules);
} }
} catch (err) { } catch (err) {
console.log("[load injectRules]", err); kissLog(err, "load injectRules");
} }
} }
const rule = rules.find((r) => const rule = rules.find((r) =>
r.pattern.split(",").some((p) => isMatch(href, p.trim())) r.pattern.split(",").some((p) => isMatch(href, p.trim()))
); );
const globalRule = rules.find((r) => r.pattern === GLOBAL_KEY) || GLOBLA_RULE; const globalRule = {
...GLOBLA_RULE,
...(rules.find((r) => r.pattern === GLOBAL_KEY) || {}),
};
if (!rule) { if (!rule) {
return globalRule; return globalRule;
} }
rule.selector = rule.selector?.trim() || globalRule.selector; [
rule.keepSelector = rule.keepSelector?.trim() || globalRule.keepSelector; "selector",
"keepSelector",
"terms",
"selectStyle",
"parentStyle",
"injectJs",
"injectCss",
"fixerSelector",
].forEach((key) => {
if (!rule[key]?.trim()) {
rule[key] = globalRule[key];
}
});
[
"translator",
"fromLang",
"toLang",
"transOpen",
"transOnly",
"transTiming",
"transTag",
"transTitle",
"detectRemote",
"fixerFunc",
].forEach((key) => {
if (rule[key] === undefined || rule[key] === GLOBAL_KEY) {
rule[key] = globalRule[key];
}
});
if (!rule.skipLangs || rule.skipLangs.length === 0) {
rule.skipLangs = globalRule.skipLangs;
}
if (rule.textStyle === GLOBAL_KEY) { if (rule.textStyle === GLOBAL_KEY) {
rule.textStyle = globalRule.textStyle; rule.textStyle = globalRule.textStyle;
rule.bgColor = globalRule.bgColor; rule.bgColor = globalRule.bgColor;
@@ -75,11 +108,6 @@ export const matchRule = async (
rule.bgColor = rule.bgColor?.trim() || globalRule.bgColor; rule.bgColor = rule.bgColor?.trim() || globalRule.bgColor;
rule.textDiyStyle = rule.textDiyStyle?.trim() || globalRule.textDiyStyle; rule.textDiyStyle = rule.textDiyStyle?.trim() || globalRule.textDiyStyle;
} }
["translator", "fromLang", "toLang", "transOpen"].forEach((key) => {
if (rule[key] === GLOBAL_KEY) {
rule[key] = globalRule[key];
}
});
return rule; return rule;
}; };
@@ -114,6 +142,11 @@ export const checkRules = (rules) => {
pattern, pattern,
selector, selector,
keepSelector, keepSelector,
terms,
selectStyle,
parentStyle,
injectJs,
injectCss,
translator, translator,
fromLang, fromLang,
toLang, toLang,
@@ -121,10 +154,23 @@ export const checkRules = (rules) => {
transOpen, transOpen,
bgColor, bgColor,
textDiyStyle, textDiyStyle,
transOnly,
transTiming,
transTag,
transTitle,
detectRemote,
skipLangs,
fixerSelector,
fixerFunc,
}) => ({ }) => ({
pattern: pattern.trim(), pattern: pattern.trim(),
selector: type(selector) === "string" ? selector : "", selector: type(selector) === "string" ? selector : "",
keepSelector: type(keepSelector) === "string" ? keepSelector : "", keepSelector: type(keepSelector) === "string" ? keepSelector : "",
terms: type(terms) === "string" ? terms : "",
selectStyle: type(selectStyle) === "string" ? selectStyle : "",
parentStyle: type(parentStyle) === "string" ? parentStyle : "",
injectJs: type(injectJs) === "string" ? injectJs : "",
injectCss: type(injectCss) === "string" ? injectCss : "",
bgColor: type(bgColor) === "string" ? bgColor : "", bgColor: type(bgColor) === "string" ? bgColor : "",
textDiyStyle: type(textDiyStyle) === "string" ? textDiyStyle : "", textDiyStyle: type(textDiyStyle) === "string" ? textDiyStyle : "",
translator: matchValue([GLOBAL_KEY, ...OPT_TRANS_ALL], translator), translator: matchValue([GLOBAL_KEY, ...OPT_TRANS_ALL], translator),
@@ -132,6 +178,14 @@ export const checkRules = (rules) => {
toLang: matchValue([GLOBAL_KEY, ...toLangs], toLang), toLang: matchValue([GLOBAL_KEY, ...toLangs], toLang),
textStyle: matchValue([GLOBAL_KEY, ...OPT_STYLE_ALL], textStyle), textStyle: matchValue([GLOBAL_KEY, ...OPT_STYLE_ALL], textStyle),
transOpen: matchValue([GLOBAL_KEY, "true", "false"], transOpen), transOpen: matchValue([GLOBAL_KEY, "true", "false"], transOpen),
transOnly: matchValue([GLOBAL_KEY, "true", "false"], transOnly),
transTiming: matchValue([GLOBAL_KEY, ...OPT_TIMING_ALL], transTiming),
transTag: matchValue([GLOBAL_KEY, "span", "font"], transTag),
transTitle: matchValue([GLOBAL_KEY, "true", "false"], transTitle),
detectRemote: matchValue([GLOBAL_KEY, "true", "false"], detectRemote),
skipLangs: type(skipLangs) === "array" ? skipLangs : [],
fixerSelector: type(fixerSelector) === "string" ? fixerSelector : "",
fixerFunc: matchValue([GLOBAL_KEY, ...FIXER_ALL], fixerFunc),
}) })
); );

View File

@@ -1,14 +1,12 @@
import { import {
STOKEY_SETTING, STOKEY_SETTING,
STOKEY_RULES, STOKEY_RULES,
STOKEY_WFRULES,
STOKEY_WORDS, STOKEY_WORDS,
STOKEY_FAB, STOKEY_FAB,
STOKEY_SYNC, STOKEY_SYNC,
STOKEY_MSAUTH, STOKEY_MSAUTH,
STOKEY_BDAUTH, STOKEY_BDAUTH,
STOKEY_RULESCACHE_PREFIX, STOKEY_RULESCACHE_PREFIX,
STOKEY_WEBFIXCACHE_PREFIX,
DEFAULT_SETTING, DEFAULT_SETTING,
DEFAULT_RULES, DEFAULT_RULES,
DEFAULT_SYNC, DEFAULT_SYNC,
@@ -16,6 +14,7 @@ import {
} from "../config"; } from "../config";
import { isExt, isGm } from "./client"; import { isExt, isGm } from "./client";
import { browser } from "./browser"; import { browser } from "./browser";
import { kissLog } from "./log";
async function set(key, val) { async function set(key, val) {
if (isExt) { if (isExt) {
@@ -86,8 +85,10 @@ export const storage = {
* 设置信息 * 设置信息
*/ */
export const getSetting = () => getObj(STOKEY_SETTING); export const getSetting = () => getObj(STOKEY_SETTING);
export const getSettingWithDefault = async () => export const getSettingWithDefault = async () => ({
(await getSetting()) || DEFAULT_SETTING; ...DEFAULT_SETTING,
...((await getSetting()) || {}),
});
export const setSetting = (val) => setObj(STOKEY_SETTING, val); export const setSetting = (val) => setObj(STOKEY_SETTING, val);
export const updateSetting = (obj) => putObj(STOKEY_SETTING, obj); export const updateSetting = (obj) => putObj(STOKEY_SETTING, obj);
@@ -99,14 +100,6 @@ export const getRulesWithDefault = async () =>
(await getRules()) || DEFAULT_RULES; (await getRules()) || DEFAULT_RULES;
export const setRules = (val) => setObj(STOKEY_RULES, val); export const setRules = (val) => setObj(STOKEY_RULES, val);
/**
* 修复规则列表
*/
export const getWebfixRules = () => getObj(STOKEY_WFRULES);
export const getWebfixRulesWithDefault = async () =>
(await getWebfixRules()) || [];
export const setWebfixRules = (val) => setObj(STOKEY_WFRULES, val);
/** /**
* 词汇列表 * 词汇列表
*/ */
@@ -123,14 +116,6 @@ export const delSubRules = (url) => del(STOKEY_RULESCACHE_PREFIX + url);
export const setSubRules = (url, val) => export const setSubRules = (url, val) =>
setObj(STOKEY_RULESCACHE_PREFIX + url, val); setObj(STOKEY_RULESCACHE_PREFIX + url, val);
/**
* 修复站点
*/
export const getWebfix = (url) => getObj(STOKEY_WEBFIXCACHE_PREFIX + url);
export const getWebfixWithDefault = async () => (await getWebfix()) || [];
export const setWebfix = (url, val) =>
setObj(STOKEY_WEBFIXCACHE_PREFIX + url, val);
/** /**
* fab位置 * fab位置
*/ */
@@ -171,6 +156,6 @@ export const tryInitDefaultData = async () => {
BUILTIN_RULES BUILTIN_RULES
); );
} catch (err) { } catch (err) {
console.log("[init default]", err); kissLog(err, "init default");
} }
}; };

View File

@@ -8,7 +8,7 @@ import {
import { apiFetch } from "../apis"; import { apiFetch } from "../apis";
import { checkRules } from "./rules"; import { checkRules } from "./rules";
import { isAllchar } from "./utils"; import { isAllchar } from "./utils";
import { syncWebfix } from "./webfix"; import { kissLog } from "./log";
/** /**
* 更新缓存同步时间 * 更新缓存同步时间
@@ -42,12 +42,12 @@ export const syncSubRules = async (url) => {
* @returns * @returns
*/ */
export const syncAllSubRules = async (subrulesList) => { export const syncAllSubRules = async (subrulesList) => {
for (let subrules of subrulesList) { for (const subrules of subrulesList) {
try { try {
await syncSubRules(subrules.url); await syncSubRules(subrules.url);
await updateSyncDataCache(subrules.url); await updateSyncDataCache(subrules.url);
} catch (err) { } catch (err) {
console.log(`[sync subrule error]: ${subrules.url}`, err); kissLog(err, `sync subrule error: ${subrules.url}`);
} }
} }
}; };
@@ -66,12 +66,9 @@ export const trySyncAllSubRules = async ({ subrulesList }) => {
// 同步订阅规则 // 同步订阅规则
await syncAllSubRules(subrulesList); await syncAllSubRules(subrulesList);
await updateSync({ subRulesSyncAt: now }); await updateSync({ subRulesSyncAt: now });
// 同步修复规则
await syncWebfix(process.env.REACT_APP_WEBFIXURL);
} }
} catch (err) { } catch (err) {
console.log("[try sync all subrules]", err); kissLog(err, "try sync all subrules");
} }
}; };

View File

@@ -2,7 +2,6 @@ import {
APP_LCNAME, APP_LCNAME,
KV_SETTING_KEY, KV_SETTING_KEY,
KV_RULES_KEY, KV_RULES_KEY,
KV_WFRULES_KEY,
KV_WORDS_KEY, KV_WORDS_KEY,
KV_RULES_SHARE_KEY, KV_RULES_SHARE_KEY,
KV_SALT_SHARE, KV_SALT_SHARE,
@@ -14,16 +13,15 @@ import {
getSettingWithDefault, getSettingWithDefault,
getRulesWithDefault, getRulesWithDefault,
getWordsWithDefault, getWordsWithDefault,
getWebfixRulesWithDefault,
setSetting, setSetting,
setRules, setRules,
setWebfixRules,
setWords, setWords,
} from "./storage"; } from "./storage";
import { apiSyncData } from "../apis"; import { apiSyncData } from "../apis";
import { sha256, removeEndchar } from "./utils"; import { sha256, removeEndchar } from "./utils";
import { createClient, getPatcher } from "webdav"; import { createClient, getPatcher } from "webdav";
import { fetchApi } from "./fetch"; import { fetchApi } from "./fetch";
import { kissLog } from "./log";
getPatcher().patch("request", (opts) => { getPatcher().patch("request", (opts) => {
return fetchApi({ return fetchApi({
@@ -118,7 +116,7 @@ export const trySyncSetting = async () => {
try { try {
await syncSetting(); await syncSetting();
} catch (err) { } catch (err) {
console.log("[sync setting]", err); kissLog(err, "sync setting");
} }
}; };
@@ -137,26 +135,7 @@ export const trySyncRules = async () => {
try { try {
await syncRules(); await syncRules();
} catch (err) { } catch (err) {
console.log("[sync user rules]", err); kissLog(err, "sync user rules");
}
};
/**
* 同步修复规则
* @returns
*/
const syncWebfixRules = async () => {
const res = await syncData(KV_WFRULES_KEY, getWebfixRulesWithDefault);
if (res?.isNew) {
await setWebfixRules(res.value);
}
};
export const trySyncWebfixRules = async () => {
try {
await syncWebfixRules();
} catch (err) {
console.log("[sync user webfix rules]", err);
} }
}; };
@@ -175,7 +154,7 @@ export const trySyncWords = async () => {
try { try {
await syncWords(); await syncWords();
} catch (err) { } catch (err) {
console.log("[sync fav words]", err); kissLog(err, "sync fav words");
} }
}; };
@@ -207,13 +186,11 @@ export const syncShareRules = async ({ rules, syncUrl, syncKey }) => {
export const syncSettingAndRules = async () => { export const syncSettingAndRules = async () => {
await syncSetting(); await syncSetting();
await syncRules(); await syncRules();
await syncWebfixRules();
await syncWords(); await syncWords();
}; };
export const trySyncSettingAndRules = async () => { export const trySyncSettingAndRules = async () => {
await trySyncSetting(); await trySyncSetting();
await trySyncRules(); await trySyncRules();
await trySyncWebfixRules();
await trySyncWords(); await trySyncWords();
}; };

View File

@@ -4,16 +4,27 @@ import {
TRANS_MIN_LENGTH, TRANS_MIN_LENGTH,
TRANS_MAX_LENGTH, TRANS_MAX_LENGTH,
MSG_TRANS_CURRULE, MSG_TRANS_CURRULE,
MSG_INJECT_JS,
MSG_INJECT_CSS,
OPT_STYLE_DASHLINE, OPT_STYLE_DASHLINE,
OPT_STYLE_FUZZY, OPT_STYLE_FUZZY,
SHADOW_KEY, SHADOW_KEY,
OPT_MOUSEKEY_DISABLE, OPT_TIMING_PAGESCROLL,
OPT_MOUSEKEY_MOUSEOVER, OPT_TIMING_PAGEOPEN,
OPT_TIMING_MOUSEOVER,
DEFAULT_TRANS_APIS,
DEFAULT_FETCH_LIMIT,
DEFAULT_FETCH_INTERVAL,
} from "../config"; } from "../config";
import Content from "../views/Content"; import Content from "../views/Content";
import { updateFetchPool, clearFetchPool } from "./fetch"; import { updateFetchPool, clearFetchPool } from "./fetch";
import { debounce, genEventName } from "./utils"; import { debounce, genEventName } from "./utils";
import { runFixer } from "./webfix"; import { runFixer } from "./webfix";
import { apiTranslate } from "../apis";
import { sendBgMsg } from "./msg";
import { isExt } from "./client";
import { injectInlineJs, injectInternalCss } from "./injector";
import { kissLog } from "./log";
/** /**
* 翻译类 * 翻译类
@@ -21,7 +32,6 @@ import { runFixer } from "./webfix";
export class Translator { export class Translator {
_rule = {}; _rule = {};
_setting = {}; _setting = {};
_fixerSetting = null;
_rootNodes = new Set(); _rootNodes = new Set();
_tranNodes = new Map(); _tranNodes = new Map();
_skipNodeNames = [ _skipNodeNames = [
@@ -42,14 +52,17 @@ export class Translator {
]; ];
_eventName = genEventName(); _eventName = genEventName();
_mouseoverNode = null; _mouseoverNode = null;
_keepSelector = [null, null];
_terms = [];
_docTitle = "";
// 显示 // 显示
_interseObserver = new IntersectionObserver( _interseObserver = new IntersectionObserver(
(intersections) => { (entries, observer) => {
intersections.forEach((intersection) => { entries.forEach((entry) => {
if (intersection.isIntersecting) { if (entry.isIntersecting) {
this._render(intersection.target); observer.unobserve(entry.target);
this._interseObserver.unobserve(intersection.target); this._render(entry.target);
} }
}); });
}, },
@@ -93,14 +106,33 @@ export class Translator {
}; };
}; };
constructor(rule, setting, fixerSetting) { _updatePool(translator) {
const { fetchInterval, fetchLimit } = setting; if (!translator) {
return;
}
const {
fetchInterval = DEFAULT_FETCH_INTERVAL,
fetchLimit = DEFAULT_FETCH_LIMIT,
} = this._setting.transApis[translator] || {};
updateFetchPool(fetchInterval, fetchLimit); updateFetchPool(fetchInterval, fetchLimit);
}
constructor(rule, setting) {
this._overrideAttachShadow(); this._overrideAttachShadow();
this._setting = setting; this._setting = setting;
this._rule = rule; this._rule = rule;
this._fixerSetting = fixerSetting;
this._keepSelector = (rule.keepSelector || "")
.split(SHADOW_KEY)
.map((item) => item.trim());
this._terms = (rule.terms || "")
.split(/\n|;/)
.map((item) => item.split(",").map((item) => item.trim()))
.filter(([term]) => Boolean(term));
this._updatePool(rule.translator);
if (rule.transOpen === "true") { if (rule.transOpen === "true") {
this._register(); this._register();
@@ -138,6 +170,7 @@ export class Translator {
updateRule = (obj) => { updateRule = (obj) => {
this.rule = { ...this.rule, ...obj }; this.rule = { ...this.rule, ...obj };
this._updatePool(obj.translator);
}; };
toggle = () => { toggle = () => {
@@ -158,11 +191,25 @@ export class Translator {
this.rule = { ...this.rule, textStyle }; this.rule = { ...this.rule, textStyle };
}; };
translateText = async (text) => {
const { translator, fromLang, toLang } = this._rule;
const apiSetting =
this._setting.transApis?.[translator] || DEFAULT_TRANS_APIS[translator];
const [trText] = await apiTranslate({
text,
translator,
fromLang,
toLang,
apiSetting,
});
return trText;
};
_querySelectorAll = (selector, node) => { _querySelectorAll = (selector, node) => {
try { try {
return Array.from(node.querySelectorAll(selector)); return Array.from(node.querySelectorAll(selector));
} catch (err) { } catch (err) {
console.log(`[querySelectorAll err]: ${selector}`); kissLog(selector, "querySelectorAll err");
} }
return []; return [];
}; };
@@ -234,13 +281,24 @@ export class Translator {
}; };
_register = () => { _register = () => {
if (this._rule.fromLang === this._rule.toLang) { const { fromLang, toLang, injectJs, injectCss, fixerSelector, fixerFunc } =
this._rule;
if (fromLang === toLang) {
return; return;
} }
// webfix // webfix
if (this._fixerSetting) { if (fixerSelector && fixerFunc !== "-") {
runFixer(this._fixerSetting); runFixer(fixerSelector, fixerFunc);
}
// 注入用户JS/CSS
if (isExt) {
injectJs && sendBgMsg(MSG_INJECT_JS, injectJs);
injectCss && sendBgMsg(MSG_INJECT_CSS, injectCss);
} else {
injectJs && injectInlineJs(injectJs);
injectCss && injectInternalCss(injectCss);
} }
// 搜索节点 // 搜索节点
@@ -256,13 +314,18 @@ export class Translator {
}); });
if ( if (
!this._setting.mouseKey || !this._rule.transTiming ||
this._setting.mouseKey === OPT_MOUSEKEY_DISABLE this._rule.transTiming === OPT_TIMING_PAGESCROLL
) { ) {
// 监听节点显示 // 监听节点显示
this._tranNodes.forEach((_, node) => { this._tranNodes.forEach((_, node) => {
this._interseObserver.observe(node); this._interseObserver.observe(node);
}); });
} else if (this._rule.transTiming === OPT_TIMING_PAGEOPEN) {
// 全文直接翻译
this._tranNodes.forEach((_, node) => {
this._render(node);
});
} else { } else {
// 监听鼠标悬停 // 监听鼠标悬停
window.addEventListener("keydown", this._handleKeydown); window.addEventListener("keydown", this._handleKeydown);
@@ -271,6 +334,15 @@ export class Translator {
node.addEventListener("mouseleave", this._handleMouseout); node.addEventListener("mouseleave", this._handleMouseout);
}); });
} }
// 翻译页面标题
if (this._rule.transTitle === "true" && !this._docTitle) {
const title = document.title;
this._docTitle = title;
this.translateText(title).then((trText) => {
document.title = `${trText} | ${title}`;
});
}
}; };
_handleMouseover = (e) => { _handleMouseover = (e) => {
@@ -279,8 +351,8 @@ export class Translator {
return; return;
} }
const key = this._setting.mouseKey.slice(3); const key = this._rule.transTiming.slice(3);
if (this._setting.mouseKey === OPT_MOUSEKEY_MOUSEOVER || e[key]) { if (this._rule.transTiming === OPT_TIMING_MOUSEOVER || e[key]) {
e.target.removeEventListener("mouseenter", this._handleMouseover); e.target.removeEventListener("mouseenter", this._handleMouseover);
e.target.removeEventListener("mouseleave", this._handleMouseout); e.target.removeEventListener("mouseleave", this._handleMouseout);
this._render(e.target); this._render(e.target);
@@ -300,7 +372,7 @@ export class Translator {
_handleKeydown = (e) => { _handleKeydown = (e) => {
// console.log("keydown", e); // console.log("keydown", e);
const key = this._setting.mouseKey.slice(3); const key = this._rule.transTiming.slice(3);
if (e[key] && this._mouseoverNode) { if (e[key] && this._mouseoverNode) {
this._mouseoverNode.removeEventListener( this._mouseoverNode.removeEventListener(
"mouseenter", "mouseenter",
@@ -318,33 +390,45 @@ export class Translator {
}; };
_unRegister = () => { _unRegister = () => {
// 恢复页面标题
if (this._docTitle) {
document.title = this._docTitle;
this._docTitle = "";
}
// 解除节点变化监听 // 解除节点变化监听
this._mutaObserver.disconnect(); this._mutaObserver.disconnect();
// 解除节点显示监听 // 解除节点显示监听
// this._interseObserver.disconnect(); // this._interseObserver.disconnect();
if ( // 移除键盘监听
!this._setting.mouseKey || window.removeEventListener("keydown", this._handleKeydown);
this._setting.mouseKey === OPT_MOUSEKEY_DISABLE
) { this._tranNodes.forEach((innerHTML, node) => {
// 解除节点显示监听 if (
this._tranNodes.forEach((_, node) => { !this._rule.transTiming ||
this._rule.transTiming === OPT_TIMING_PAGESCROLL
) {
// 解除节点显示监听
this._interseObserver.unobserve(node); this._interseObserver.unobserve(node);
// 移除已插入元素 } else if (this._rule.transTiming !== OPT_TIMING_PAGEOPEN) {
node.querySelector(APP_LCNAME)?.remove(); // 移除鼠标悬停监听
});
} else {
// 移除鼠标悬停监听
window.removeEventListener("keydown", this._handleKeydown);
this._tranNodes.forEach((_, node) => {
// node.style.pointerEvents = "none"; // node.style.pointerEvents = "none";
node.removeEventListener("mouseenter", this._handleMouseover); node.removeEventListener("mouseenter", this._handleMouseover);
node.removeEventListener("mouseleave", this._handleMouseout); node.removeEventListener("mouseleave", this._handleMouseout);
// 移除已插入元素 }
// 移除/恢复元素
if (innerHTML && this._rule.transOnly === "true") {
node.innerHTML = innerHTML;
} else {
node.querySelector(APP_LCNAME)?.remove(); node.querySelector(APP_LCNAME)?.remove();
}); }
} });
// 移除用户JS/CSS
this._removeInjector();
// 清空节点集合 // 清空节点集合
this._rootNodes.clear(); this._rootNodes.clear();
@@ -354,11 +438,21 @@ export class Translator {
clearFetchPool(); clearFetchPool();
}; };
_removeInjector = () => {
document
.querySelectorAll(`[data-source^="KISS-Calendar"]`)
?.forEach((el) => el.remove());
};
_reTranslate = debounce(() => { _reTranslate = debounce(() => {
if (this._rule.transOpen === "true") { if (this._rule.transOpen === "true") {
window.removeEventListener("keydown", this._handleKeydown);
this._mutaObserver.disconnect();
this._interseObserver.disconnect();
this._removeInjector();
this._register(); this._register();
} }
}, 500); }, this._setting.transInterval);
_invalidLength = (q) => _invalidLength = (q) =>
!q || !q ||
@@ -370,6 +464,10 @@ export class Translator {
// 已翻译 // 已翻译
if (traEl) { if (traEl) {
if (this._rule.transOnly === "true") {
return;
}
const preText = this._tranNodes.get(el); const preText = this._tranNodes.get(el);
const curText = el.innerText.trim(); const curText = el.innerText.trim();
// const traText = traEl.innerText.trim(); // const traText = traEl.innerText.trim();
@@ -385,55 +483,74 @@ export class Translator {
} }
let q = el.innerText.trim(); let q = el.innerText.trim();
this._tranNodes.set(el, q); if (this._rule.transOnly === "true") {
this._tranNodes.set(el, el.innerHTML);
// 太长或太短 } else {
if (this._invalidLength(q)) { this._tranNodes.set(el, q);
return;
} }
// console.log("---> ", q);
const keepSelector = this._rule.keepSelector || "";
const keeps = []; const keeps = [];
if (keepSelector.trim()) {
// 保留元素
const [matchSelector, subSelector] = this._keepSelector;
if (matchSelector || subSelector) {
let text = ""; let text = "";
el.childNodes.forEach((child) => { el.childNodes.forEach((child) => {
if (child.nodeType === 1 && child.matches(keepSelector)) { if (
child.nodeType === 1 &&
((matchSelector && child.matches(matchSelector)) ||
(subSelector && child.querySelector(subSelector)))
) {
if (child.nodeName === "IMG") { if (child.nodeName === "IMG") {
child.style.cssText += `width: ${child.width}px;`; child.style.cssText += `width: ${child.width}px;`;
child.style.cssText += `height: ${child.height}px;`; child.style.cssText += `height: ${child.height}px;`;
} }
text += `#${keeps.length}#`; text += `[${keeps.length}]`;
keeps.push(child.outerHTML); keeps.push(child.outerHTML);
} else { } else {
text += child.textContent; text += child.textContent;
} }
}); });
// 太长或太短
if (this._invalidLength(text.replace(/#(\d+)#/g, "").trim())) {
return;
}
if (keeps.length > 0) { if (keeps.length > 0) {
q = text; // textContent会保留些无用的换行符严重影响翻译质量
if (q.includes("\n")) {
q = text;
} else {
q = text.replaceAll("\n", " ");
}
} }
} }
// console.log("---> ", q); // 太长或太短
if (this._invalidLength(q.replace(/\[(\d+)\]/g, "").trim())) {
return;
}
// 专业术语
if (this._terms.length > 0) {
for (const term of this._terms) {
const re = new RegExp(term[0], "g");
q = q.replace(re, (t) => {
const text = `[${keeps.length}]`;
keeps.push(`<i class="kiss-trem">${term[1] || t}</i>`);
return text;
});
}
}
traEl = document.createElement(APP_LCNAME); traEl = document.createElement(APP_LCNAME);
traEl.style.visibility = "visible"; traEl.style.visibility = "visible";
// if (this._rule.transOnly === "true") {
// el.innerHTML = "";
// }
const { selectStyle, parentStyle } = this._rule;
el.appendChild(traEl); el.appendChild(traEl);
el.style.cssText += el.style.cssText += selectStyle;
"-webkit-line-clamp: unset; max-height: none; height: auto;";
if (el.parentElement) { if (el.parentElement) {
el.parentElement.style.cssText += el.parentElement.style.cssText += parentStyle;
"-webkit-line-clamp: unset; max-height: none; height: auto;";
} }
const root = createRoot(traEl); const root = createRoot(traEl);
root.render(<Content q={q} keeps={keeps} translator={this} />); root.render(<Content q={q} keeps={keeps} translator={this} $el={el} />);
}; };
} }

View File

@@ -233,3 +233,16 @@ export const isValidWord = (str) => {
const regex = /^[a-zA-Z-]+$/; const regex = /^[a-zA-Z-]+$/;
return regex.test(str); return regex.test(str);
}; };
/**
* blob转为base64
* @param {*} blob
* @returns
*/
export const blobToBase64 = (blob) => {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.readAsDataURL(blob);
});
};

View File

@@ -1,61 +1,24 @@
import { isMatch } from "./utils";
import { getWebfix, setWebfix, getWebfixRulesWithDefault } from "./storage";
import { apiFetch } from "../apis";
/** /**
* 修复程序类型 * 修复程序类型
*/ */
const FIXER_BR = "br"; export const FIXER_NONE = "-";
const FIXER_BN = "bn"; export const FIXER_BR = "br";
const FIXER_BR_DIV = "brToDiv"; export const FIXER_BN = "bn";
const FIXER_BN_DIV = "bnToDiv"; export const FIXER_BR_DIV = "brToDiv";
const FIXER_FONTSIZE = "fontSize"; export const FIXER_BN_DIV = "bnToDiv";
export const FIXER_ALL = [ export const FIXER_ALL = [
FIXER_NONE,
FIXER_BR, FIXER_BR,
FIXER_BN, FIXER_BN,
FIXER_BR_DIV, FIXER_BR_DIV,
FIXER_BN_DIV, FIXER_BN_DIV,
// FIXER_FONTSIZE,
];
/**
* 需要修复的站点列表
* - pattern 匹配网址
* - selector 需要修复的选择器
* - rootSelector 需要监听的选择器,可留空
* - fixer 修复函数,可针对不同网址,选用不同修复函数
*/
const DEFAULT_SITES = [
{
pattern: "www.phoronix.com",
selector: ".content",
rootSelector: "",
fixer: FIXER_BR,
},
{
pattern: "t.me/s/",
selector: ".tgme_widget_message_text",
rootSelector: ".tgme_channel_history",
fixer: FIXER_BR,
},
{
pattern: "baidu.com",
selector: "html",
rootSelector: "",
fixer: FIXER_FONTSIZE,
},
{
pattern: "chat.openai.com",
selector: "div[data-testid^=conversation-turn] .items-start > div",
rootSelector: "",
fixer: FIXER_BN,
},
]; ];
/** /**
* 修复过的标记 * 修复过的标记
*/ */
const fixedSign = "kissfixed"; const fixedSign = "kiss-fixed";
/** /**
* 采用 `br` 换行网站的修复函数 * 采用 `br` 换行网站的修复函数
@@ -69,8 +32,8 @@ function brFixer(node, tag = "p") {
} }
node.setAttribute(fixedSign, "true"); node.setAttribute(fixedSign, "true");
var gapTags = ["BR", "WBR"]; const gapTags = ["BR", "WBR"];
var newlineTags = [ const newlineTags = [
"DIV", "DIV",
"UL", "UL",
"OL", "OL",
@@ -85,9 +48,10 @@ function brFixer(node, tag = "p") {
"HR", "HR",
"PRE", "PRE",
"TABLE", "TABLE",
"BLOCKQUOTE",
]; ];
var html = ""; let html = "";
node.childNodes.forEach(function (child, index) { node.childNodes.forEach(function (child, index) {
if (index === 0) { if (index === 0) {
html += `<${tag} class="kiss-p">`; html += `<${tag} class="kiss-p">`;
@@ -99,8 +63,8 @@ function brFixer(node, tag = "p") {
html += `</${tag}>${child.outerHTML}<${tag} class="kiss-p">`; html += `</${tag}>${child.outerHTML}<${tag} class="kiss-p">`;
} else if (child.outerHTML) { } else if (child.outerHTML) {
html += child.outerHTML; html += child.outerHTML;
} else if (child.nodeValue) { } else if (child.textContent) {
html += child.nodeValue; html += child.textContent;
} }
if (index === node.childNodes.length - 1) { if (index === node.childNodes.length - 1) {
@@ -134,25 +98,6 @@ function bnDivFixer(node) {
return bnFixer(node, "div"); return bnFixer(node, "div");
} }
/**
* 修复字体大小问题,如 baidu.com
* @param {*} node
*/
function fontSizeFixer(node) {
node.style.cssText += "font-size:1em;";
}
/**
* 修复程序映射
*/
const fixerMap = {
[FIXER_BR]: brFixer,
[FIXER_BN]: bnFixer,
[FIXER_BR_DIV]: brDivFixer,
[FIXER_BN_DIV]: bnDivFixer,
[FIXER_FONTSIZE]: fontSizeFixer,
};
/** /**
* 查找、监听节点,并执行修复函数 * 查找、监听节点,并执行修复函数
* @param {*} selector * @param {*} selector
@@ -160,7 +105,7 @@ const fixerMap = {
* @param {*} rootSelector * @param {*} rootSelector
*/ */
function run(selector, fixer, rootSelector) { function run(selector, fixer, rootSelector) {
var mutaObserver = new MutationObserver(function (mutations) { const mutaObserver = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) { mutations.forEach(function (mutation) {
mutation.addedNodes.forEach(function (addNode) { mutation.addedNodes.forEach(function (addNode) {
if (addNode && addNode.querySelectorAll) { if (addNode && addNode.querySelectorAll) {
@@ -172,7 +117,7 @@ function run(selector, fixer, rootSelector) {
}); });
}); });
var rootNodes = [document]; let rootNodes = [document];
if (rootSelector) { if (rootSelector) {
rootNodes = document.querySelectorAll(rootSelector); rootNodes = document.querySelectorAll(rootSelector);
} }
@@ -189,67 +134,25 @@ function run(selector, fixer, rootSelector) {
} }
/** /**
* 同步远程数据 * 修复程序映射
* @param {*} url
* @returns
*/ */
export const syncWebfix = async (url) => { const fixerMap = {
const sites = await apiFetch(url); [FIXER_BR]: brFixer,
await setWebfix(url, sites); [FIXER_BN]: bnFixer,
return sites; [FIXER_BR_DIV]: brDivFixer,
}; [FIXER_BN_DIV]: bnDivFixer,
/**
* 从缓存或远程加载修复站点
* @param {*} url
* @returns
*/
export const loadOrFetchWebfix = async (url) => {
try {
let sites = await getWebfix(url);
if (sites?.length) {
return sites;
}
return syncWebfix(url);
} catch (err) {
console.log("[load webfix]", err.message);
return DEFAULT_SITES;
}
}; };
/** /**
* 执行fixer * 执行fixer
* @param {*} param0 * @param {*} param0
*/ */
export async function runFixer({ selector, fixer, rootSelector }) { export function runFixer(selector, fixer = "-", rootSelector) {
try { try {
run(selector, fixerMap[fixer], rootSelector); if (Object.keys(fixerMap).includes(fixer)) {
run(selector, fixerMap[fixer], rootSelector);
}
} catch (err) { } catch (err) {
console.error(`[kiss-webfix run]: ${err.message}`); console.error(`[kiss-webfix run]: ${err.message}`);
} }
} }
/**
* 匹配fixer配置
*/
export async function matchFixer(href, { injectWebfix }) {
if (!injectWebfix) {
return null;
}
try {
const userSites = await getWebfixRulesWithDefault();
const subSites = await loadOrFetchWebfix(process.env.REACT_APP_WEBFIXURL);
const sites = [...userSites, ...subSites];
for (var i = 0; i < sites.length; i++) {
var site = sites[i];
if (isMatch(href, site.pattern) && fixerMap[site.fixer]) {
return site;
}
}
} catch (err) {
console.error(`[kiss-webfix match]: ${err.message}`);
}
return null;
}

View File

@@ -5,7 +5,7 @@ import { BUILTIN_RULES } from "./config/rules";
(() => { (() => {
// rules // rules
try { try {
const data = JSON.stringify(BUILTIN_RULES, null, " "); const data = JSON.stringify(BUILTIN_RULES, null, 2);
const file = path.resolve( const file = path.resolve(
__dirname, __dirname,
"../build/web/kiss-translator-rules.json" "../build/web/kiss-translator-rules.json"

View File

@@ -21,6 +21,7 @@ import {
} from "../../config"; } from "../../config";
import { shortcutRegister } from "../../libs/shortcut"; import { shortcutRegister } from "../../libs/shortcut";
import { sendIframeMsg } from "../../libs/iframe"; import { sendIframeMsg } from "../../libs/iframe";
import { kissLog } from "../../libs/log";
export default function Action({ translator, fab }) { export default function Action({ translator, fab }) {
const fabWidth = 40; const fabWidth = 40;
@@ -95,11 +96,11 @@ export default function Action({ translator, fab }) {
// 注册菜单 // 注册菜单
try { try {
const menuCommandIds = []; const menuCommandIds = [];
const { contextMenus = true } = translator.setting; const { contextMenuType } = translator.setting;
contextMenus && contextMenuType !== 0 &&
menuCommandIds.push( menuCommandIds.push(
GM.registerMenuCommand( GM.registerMenuCommand(
"Toggle Translate (Alt+Q)", "Toggle Translate",
(event) => { (event) => {
translator.toggle(); translator.toggle();
sendIframeMsg(MSG_TRANS_TOGGLE); sendIframeMsg(MSG_TRANS_TOGGLE);
@@ -108,7 +109,7 @@ export default function Action({ translator, fab }) {
"Q" "Q"
), ),
GM.registerMenuCommand( GM.registerMenuCommand(
"Toggle Style (Alt+C)", "Toggle Style",
(event) => { (event) => {
translator.toggleStyle(); translator.toggleStyle();
sendIframeMsg(MSG_TRANS_TOGGLE_STYLE); sendIframeMsg(MSG_TRANS_TOGGLE_STYLE);
@@ -117,7 +118,7 @@ export default function Action({ translator, fab }) {
"C" "C"
), ),
GM.registerMenuCommand( GM.registerMenuCommand(
"Open Menu (Alt+K)", "Open Menu",
(event) => { (event) => {
setShowPopup((pre) => !pre); setShowPopup((pre) => !pre);
}, },
@@ -138,7 +139,7 @@ export default function Action({ translator, fab }) {
}); });
}; };
} catch (err) { } catch (err) {
console.log("[registerMenuCommand]", err); kissLog(err, "registerMenuCommand");
} }
}, [translator]); }, [translator]);

View File

@@ -2,7 +2,7 @@ import { loadingSvg } from "../../libs/svg";
export default function LoadingIcon() { export default function LoadingIcon() {
return ( return (
<div <span
style={{ style={{
display: "inline-block", display: "inline-block",
width: "1.2em", width: "1.2em",

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from "react"; import { useState, useEffect, useMemo } from "react";
import LoadingIcon from "./LoadingIcon"; import LoadingIcon from "./LoadingIcon";
import { import {
OPT_STYLE_LINE, OPT_STYLE_LINE,
@@ -11,113 +11,84 @@ import {
OPT_STYLE_DIY, OPT_STYLE_DIY,
DEFAULT_COLOR, DEFAULT_COLOR,
MSG_TRANS_CURRULE, MSG_TRANS_CURRULE,
TRANS_NEWLINE_LENGTH,
} from "../../config"; } from "../../config";
import { useTranslate } from "../../hooks/Translate"; import { useTranslate } from "../../hooks/Translate";
import { styled } from "@mui/material/styles"; import { styled, css } from "@mui/material/styles";
import { APP_LCNAME } from "../../config";
const LineSpan = styled("span")` const LINE_STYLES = {
opacity: 0.6; [OPT_STYLE_LINE]: "solid",
-webkit-opacity: 0.6; [OPT_STYLE_DOTLINE]: "dotted",
text-decoration-line: underline; [OPT_STYLE_DASHLINE]: "dashed",
text-decoration-style: ${(props) => props.$lineStyle}; [OPT_STYLE_WAVYLINE]: "wavy",
text-decoration-color: ${(props) => props.$lineColor}; };
text-decoration-thickness: 2px;
text-underline-offset: 0.3em; const StyledSpan = styled("span")`
-webkit-text-decoration-line: underline; ${({ textStyle, textDiyStyle, bgColor }) => {
-webkit-text-decoration-style: ${(props) => props.$lineStyle}; switch (textStyle) {
-webkit-text-decoration-color: ${(props) => props.$lineColor}; case OPT_STYLE_LINE: // 下划线
-webkit-text-decoration-thickness: 2px; case OPT_STYLE_DOTLINE: // 点状线
-webkit-text-underline-offset: 0.3em; case OPT_STYLE_DASHLINE: // 虚线
&:hover { case OPT_STYLE_WAVYLINE: // 波浪线
opacity: 1; return css`
-webkit-opacity: 1; opacity: 0.6;
} -webkit-opacity: 0.6;
text-decoration-line: underline;
text-decoration-style: ${LINE_STYLES[textStyle]};
text-decoration-color: ${bgColor};
text-decoration-thickness: 2px;
text-underline-offset: 0.3em;
-webkit-text-decoration-line: underline;
-webkit-text-decoration-style: ${LINE_STYLES[textStyle]};
-webkit-text-decoration-color: ${bgColor};
-webkit-text-decoration-thickness: 2px;
-webkit-text-underline-offset: 0.3em;
&:hover {
opacity: 1;
-webkit-opacity: 1;
}
`;
case OPT_STYLE_FUZZY: // 模糊
return css`
filter: blur(0.2em);
-webkit-filter: blur(0.2em);
&:hover {
filter: none;
-webkit-filter: none;
}
`;
case OPT_STYLE_HIGHLIGHT: // 高亮
return css`
color: #fff;
background-color: ${bgColor || DEFAULT_COLOR};
`;
case OPT_STYLE_BLOCKQUOTE: // 引用
return css`
opacity: 0.6;
-webkit-opacity: 0.6;
display: block;
padding: 0 0.75em;
border-left: 0.25em solid ${bgColor || DEFAULT_COLOR};
&:hover {
opacity: 1;
-webkit-opacity: 1;
}
`;
case OPT_STYLE_DIY: // 自定义
return textDiyStyle;
default:
return ``;
}
}}
`; `;
const BlockquoteSpan = styled("span")` export default function Content({ q, keeps, translator, $el }) {
opacity: 0.6;
-webkit-opacity: 0.6;
display: block;
padding: 0 0.75em;
border-left: 0.25em solid ${(props) => props.$lineColor};
&:hover {
opacity: 1;
-webkit-opacity: 1;
}
`;
const FuzzySpan = styled("span")`
filter: blur(0.2em);
-webkit-filter: blur(0.2em);
&:hover {
filter: none;
-webkit-filter: none;
}
`;
const HighlightSpan = styled("span")`
color: #fff;
background-color: ${(props) => props.$bgColor};
`;
const DiySpan = styled("span")`
${(props) => props.$diyStyle}
`;
function StyledSpan({ textStyle, textDiyStyle, bgColor, children }) {
switch (textStyle) {
case OPT_STYLE_LINE: // 下划线
return (
<LineSpan $lineStyle="solid" $lineColor={bgColor}>
{children}
</LineSpan>
);
case OPT_STYLE_DOTLINE: // 点状线
return (
<LineSpan $lineStyle="dotted" $lineColor={bgColor}>
{children}
</LineSpan>
);
case OPT_STYLE_DASHLINE: // 虚线
return (
<LineSpan $lineStyle="dashed" $lineColor={bgColor}>
{children}
</LineSpan>
);
case OPT_STYLE_WAVYLINE: // 波浪线
return (
<LineSpan $lineStyle="wavy" $lineColor={bgColor}>
{children}
</LineSpan>
);
case OPT_STYLE_FUZZY: // 模糊
return <FuzzySpan>{children}</FuzzySpan>;
case OPT_STYLE_HIGHLIGHT: // 高亮
return (
<HighlightSpan $bgColor={bgColor || DEFAULT_COLOR}>
{children}
</HighlightSpan>
);
case OPT_STYLE_BLOCKQUOTE: // 引用
return (
<BlockquoteSpan $lineColor={bgColor || DEFAULT_COLOR}>
{children}
</BlockquoteSpan>
);
case OPT_STYLE_DIY: // 自定义
return <DiySpan $diyStyle={textDiyStyle}>{children}</DiySpan>;
default:
return <span>{children}</span>;
}
}
export default function Content({ q, keeps, translator }) {
const [rule, setRule] = useState(translator.rule); const [rule, setRule] = useState(translator.rule);
const { text, sameLang, loading } = useTranslate(q, rule, translator.setting); const { text, sameLang, loading } = useTranslate(q, rule, translator.setting);
const { textStyle, bgColor = "", textDiyStyle = "" } = rule; const { transOpen, textStyle, bgColor, textDiyStyle, transOnly, transTag } =
rule;
const { newlineLength = TRANS_NEWLINE_LENGTH } = translator.setting; const { newlineLength } = translator.setting;
const handleKissEvent = (e) => { const handleKissEvent = (e) => {
const { action, args } = e.detail; const { action, args } = e.detail;
@@ -136,10 +107,27 @@ export default function Content({ q, keeps, translator }) {
}; };
}, [translator.eventName]); }, [translator.eventName]);
const gap = useMemo(() => {
if (transOnly === "true") {
return "";
}
return q.length >= newlineLength ? <br /> : " ";
}, [q, transOnly, newlineLength]);
const styles = useMemo(
() => ({
textStyle,
textDiyStyle,
bgColor,
as: transTag,
}),
[textStyle, textDiyStyle, bgColor, transTag]
);
if (loading) { if (loading) {
return ( return (
<> <>
{q.length >= newlineLength ? <br /> : " "} {gap}
<LoadingIcon /> <LoadingIcon />
</> </>
); );
@@ -149,24 +137,36 @@ export default function Content({ q, keeps, translator }) {
return; return;
} }
if (
transOnly === "true" &&
transOpen === "true" &&
$el.querySelector(APP_LCNAME)
) {
Array.from($el.childNodes).forEach((el) => {
if (el.localName !== APP_LCNAME) {
el.remove();
}
});
}
if (keeps.length > 0) {
return (
<>
{gap}
<StyledSpan
{...styles}
dangerouslySetInnerHTML={{
__html: text.replace(/\[(\d+)\]/g, (_, p) => keeps[parseInt(p)]),
}}
/>
</>
);
}
return ( return (
<> <>
{q.length >= newlineLength ? <br /> : " "} {gap}
<StyledSpan <StyledSpan {...styles}>{text}</StyledSpan>
textStyle={textStyle}
textDiyStyle={textDiyStyle}
bgColor={bgColor}
>
{keeps.length > 0 ? (
<span
dangerouslySetInnerHTML={{
__html: text.replace(/#(\d+)#/g, (_, p) => keeps[parseInt(p)]),
}}
/>
) : (
text
)}
</StyledSpan>
</> </>
); );
} }

View File

@@ -14,6 +14,8 @@ import {
OPT_TRANS_CLOUDFLAREAI, OPT_TRANS_CLOUDFLAREAI,
OPT_TRANS_CUSTOMIZE, OPT_TRANS_CUSTOMIZE,
URL_KISS_PROXY, URL_KISS_PROXY,
DEFAULT_FETCH_LIMIT,
DEFAULT_FETCH_INTERVAL,
} from "../../config"; } from "../../config";
import { useState } from "react"; import { useState } from "react";
import { useI18n } from "../../hooks/I18n"; import { useI18n } from "../../hooks/I18n";
@@ -28,6 +30,7 @@ import { useApi } from "../../hooks/Api";
import { apiTranslate } from "../../apis"; import { apiTranslate } from "../../apis";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Link from "@mui/material/Link"; import Link from "@mui/material/Link";
import { limitNumber } from "../../libs/utils";
function TestButton({ translator, api }) { function TestButton({ translator, api }) {
const i18n = useI18n(); const i18n = useI18n();
@@ -50,16 +53,22 @@ function TestButton({ translator, api }) {
alert.success(i18n("test_success")); alert.success(i18n("test_success"));
} catch (err) { } catch (err) {
// alert.error(`${i18n("test_failed")}: ${err.message}`); // alert.error(`${i18n("test_failed")}: ${err.message}`);
let msg = err.message;
try {
msg = JSON.stringify(JSON.parse(err.message), null, 2);
} catch (err) {
// skip
}
alert.error( alert.error(
<> <>
<div>{`${i18n("test_failed")}: ${err.message}`}</div> <div>{i18n("test_failed")}</div>
<pre <pre
style={{ style={{
maxWidth: 400, maxWidth: 400,
overflow: "auto", overflow: "auto",
}} }}
> >
{JSON.stringify(err.cause || {}, null, 2)} {msg}
</pre> </pre>
</> </>
); );
@@ -82,10 +91,26 @@ function TestButton({ translator, api }) {
function ApiFields({ translator }) { function ApiFields({ translator }) {
const i18n = useI18n(); const i18n = useI18n();
const { api, updateApi, resetApi } = useApi(translator); const { api, updateApi, resetApi } = useApi(translator);
const { url = "", key = "", model = "", prompt = "" } = api; const {
url = "",
key = "",
model = "",
prompt = "",
fetchLimit = DEFAULT_FETCH_LIMIT,
fetchInterval = DEFAULT_FETCH_INTERVAL,
} = api;
const handleChange = (e) => { const handleChange = (e) => {
const { name, value } = e.target; let { name, value } = e.target;
switch (name) {
case "fetchLimit":
value = limitNumber(value, 1, 100);
break;
case "fetchInterval":
value = limitNumber(value, 0, 5000);
break;
default:
}
updateApi({ updateApi({
[name]: value, [name]: value,
}); });
@@ -124,11 +149,14 @@ function ApiFields({ translator }) {
onChange={handleChange} onChange={handleChange}
multiline={mulkeysTranslators.includes(translator)} multiline={mulkeysTranslators.includes(translator)}
helperText={ helperText={
mulkeysTranslators.includes(translator) ? i18n("mulkeys_help") : "" mulkeysTranslators.includes(translator)
? i18n("mulkeys_help")
: ""
} }
/> />
</> </>
)} )}
{(translator === OPT_TRANS_OPENAI || translator === OPT_TRANS_GEMINI) && ( {(translator === OPT_TRANS_OPENAI || translator === OPT_TRANS_GEMINI) && (
<> <>
<TextField <TextField
@@ -149,22 +177,38 @@ function ApiFields({ translator }) {
</> </>
)} )}
<TextField
size="small"
label={i18n("fetch_limit")}
type="number"
name="fetchLimit"
value={fetchLimit}
onChange={handleChange}
/>
<TextField
size="small"
label={i18n("fetch_interval")}
type="number"
name="fetchInterval"
value={fetchInterval}
onChange={handleChange}
/>
<Stack direction="row" spacing={2}> <Stack direction="row" spacing={2}>
<TestButton translator={translator} api={api} /> <TestButton translator={translator} api={api} />
{!buildinTranslators.includes(translator) && ( <Button
<Button size="small"
size="small" variant="outlined"
variant="outlined" onClick={() => {
onClick={() => { resetApi();
resetApi(); }}
}} >
> {i18n("restore_default")}
{i18n("restore_default")} </Button>
</Button>
)}
</Stack> </Stack>
{translator === OPT_TRANS_CUSTOMIZE && ( {translator.startsWith(OPT_TRANS_CUSTOMIZE) && (
<pre>{i18n("custom_api_help")}</pre> <pre>{i18n("custom_api_help")}</pre>
)} )}
</Stack> </Stack>

View File

@@ -18,6 +18,7 @@ import UploadButton from "./UploadButton";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import ClearAllIcon from "@mui/icons-material/ClearAll"; import ClearAllIcon from "@mui/icons-material/ClearAll";
import { isValidWord } from "../../libs/utils"; import { isValidWord } from "../../libs/utils";
import { kissLog } from "../../libs/log";
function DictField({ word }) { function DictField({ word }) {
const [dictResult, setDictResult] = useState(null); const [dictResult, setDictResult] = useState(null);
@@ -35,7 +36,7 @@ function DictField({ word }) {
fromLang: "en", fromLang: "en",
toLang: "zh-CN", toLang: "zh-CN",
}); });
setDictResult(dictRes[2].dict_result); dictRes[2].type === 1 && setDictResult(JSON.parse(dictRes[2].result));
} catch (err) { } catch (err) {
setError(err.message); setError(err.message);
} finally { } finally {
@@ -93,7 +94,7 @@ export default function FavWords() {
.filter(isValidWord); .filter(isValidWord);
await mergeWords(newWords); await mergeWords(newWords);
} catch (err) { } catch (err) {
console.log("[import rules]", err); kissLog(err, "import rules");
} }
}; };

View File

@@ -11,7 +11,6 @@ import DesignServicesIcon from "@mui/icons-material/DesignServices";
import { useI18n } from "../../hooks/I18n"; import { useI18n } from "../../hooks/I18n";
import SyncIcon from "@mui/icons-material/Sync"; import SyncIcon from "@mui/icons-material/Sync";
import ApiIcon from "@mui/icons-material/Api"; import ApiIcon from "@mui/icons-material/Api";
import SendTimeExtensionIcon from "@mui/icons-material/SendTimeExtension";
import InputIcon from "@mui/icons-material/Input"; import InputIcon from "@mui/icons-material/Input";
import SelectAllIcon from "@mui/icons-material/SelectAll"; import SelectAllIcon from "@mui/icons-material/SelectAll";
import EventNoteIcon from "@mui/icons-material/EventNote"; import EventNoteIcon from "@mui/icons-material/EventNote";
@@ -65,12 +64,6 @@ export default function Navigator(props) {
url: "/sync", url: "/sync",
icon: <SyncIcon />, icon: <SyncIcon />,
}, },
{
id: "webfix",
label: i18n("patch_setting"),
url: "/webfix",
icon: <SendTimeExtensionIcon />,
},
{ {
id: "words", id: "words",
label: i18n("favorite_words"), label: i18n("favorite_words"),

View File

@@ -7,6 +7,7 @@ import Alert from "@mui/material/Alert";
import { import {
GLOBAL_KEY, GLOBAL_KEY,
DEFAULT_RULE, DEFAULT_RULE,
GLOBLA_RULE,
OPT_LANGS_FROM, OPT_LANGS_FROM,
OPT_LANGS_TO, OPT_LANGS_TO,
OPT_TRANS_ALL, OPT_TRANS_ALL,
@@ -15,6 +16,9 @@ import {
OPT_STYLE_USE_COLOR, OPT_STYLE_USE_COLOR,
URL_KISS_RULES_NEW_ISSUE, URL_KISS_RULES_NEW_ISSUE,
OPT_SYNCTYPE_WORKER, OPT_SYNCTYPE_WORKER,
OPT_TIMING_PAGESCROLL,
DEFAULT_TRANS_TAG,
OPT_TIMING_ALL,
} from "../../config"; } from "../../config";
import { useState, useEffect, useMemo } from "react"; import { useState, useEffect, useMemo } from "react";
import { useI18n } from "../../hooks/I18n"; import { useI18n } from "../../hooks/I18n";
@@ -50,11 +54,17 @@ import HelpButton from "./HelpButton";
import { useSyncCaches } from "../../hooks/Sync"; import { useSyncCaches } from "../../hooks/Sync";
import DownloadButton from "./DownloadButton"; import DownloadButton from "./DownloadButton";
import UploadButton from "./UploadButton"; import UploadButton from "./UploadButton";
import { FIXER_ALL } from "../../libs/webfix";
import AddIcon from "@mui/icons-material/Add";
import EditIcon from "@mui/icons-material/Edit";
import CancelIcon from "@mui/icons-material/Cancel";
import SaveIcon from "@mui/icons-material/Save";
import { kissLog } from "../../libs/log";
function RuleFields({ rule, rules, setShow, setKeyword }) { function RuleFields({ rule, rules, setShow, setKeyword }) {
const initFormValues = rule || { const initFormValues = {
...DEFAULT_RULE, ...(rule?.pattern === "*" ? GLOBLA_RULE : DEFAULT_RULE),
transOpen: "true", ...(rule || {}),
}; };
const editMode = !!rule; const editMode = !!rule;
@@ -62,10 +72,16 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
const [disabled, setDisabled] = useState(editMode); const [disabled, setDisabled] = useState(editMode);
const [errors, setErrors] = useState({}); const [errors, setErrors] = useState({});
const [formValues, setFormValues] = useState(initFormValues); const [formValues, setFormValues] = useState(initFormValues);
const [showMore, setShowMore] = useState(!rules);
const { const {
pattern, pattern,
selector, selector,
keepSelector = "", keepSelector = "",
terms = "",
selectStyle = "",
parentStyle = "",
injectJs = "",
injectCss = "",
translator, translator,
fromLang, fromLang,
toLang, toLang,
@@ -73,6 +89,14 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
transOpen, transOpen,
bgColor, bgColor,
textDiyStyle, textDiyStyle,
transOnly = "false",
transTiming = OPT_TIMING_PAGESCROLL,
transTag = DEFAULT_TRANS_TAG,
transTitle = "false",
detectRemote = "false",
skipLangs = [],
fixerSelector = "",
fixerFunc = "-",
} = formValues; } = formValues;
const hasSamePattern = (str) => { const hasSamePattern = (str) => {
@@ -310,10 +334,206 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
value={textDiyStyle} value={textDiyStyle}
disabled={disabled} disabled={disabled}
onChange={handleChange} onChange={handleChange}
maxRows={10}
multiline multiline
/> />
)} )}
{showMore && (
<>
<Box>
<Grid container spacing={2} columns={12}>
<Grid item xs={12} sm={6} md={3} lg={2}>
<TextField
select
size="small"
fullWidth
name="transOnly"
value={transOnly}
label={i18n("show_only_translations")}
disabled={disabled}
onChange={handleChange}
>
{GlobalItem}
<MenuItem value={"false"}>{i18n("disable")}</MenuItem>
<MenuItem value={"true"}>{i18n("enable")}</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={3} lg={2}>
<TextField
select
size="small"
fullWidth
name="transTiming"
value={transTiming}
label={i18n("translate_timing")}
disabled={disabled}
onChange={handleChange}
>
{GlobalItem}
{OPT_TIMING_ALL.map((item) => (
<MenuItem key={item} value={item}>
{i18n(item)}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={3} lg={2}>
<TextField
select
size="small"
fullWidth
name="transTag"
value={transTag}
label={i18n("translation_element_tag")}
disabled={disabled}
onChange={handleChange}
>
{GlobalItem}
<MenuItem value={"span"}>{`<span>`}</MenuItem>
<MenuItem value={"font"}>{`<font>`}</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={3} lg={2}>
<TextField
select
size="small"
fullWidth
name="transTitle"
value={transTitle}
label={i18n("translate_page_title")}
disabled={disabled}
onChange={handleChange}
>
{GlobalItem}
<MenuItem value={"false"}>{i18n("disable")}</MenuItem>
<MenuItem value={"true"}>{i18n("enable")}</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={3} lg={2}>
<TextField
select
size="small"
fullWidth
name="detectRemote"
value={detectRemote}
label={i18n("detect_lang_remote")}
disabled={disabled}
onChange={handleChange}
>
{GlobalItem}
<MenuItem value={"false"}>{i18n("disable")}</MenuItem>
<MenuItem value={"true"}>{i18n("enable")}</MenuItem>
</TextField>
</Grid>
</Grid>
</Box>
<TextField
select
size="small"
label={i18n("skip_langs")}
helperText={i18n("skip_langs_helper")}
name="skipLangs"
value={skipLangs}
disabled={disabled}
onChange={handleChange}
SelectProps={{
multiple: true,
}}
>
{OPT_LANGS_TO.map(([langKey, langName]) => (
<MenuItem key={langKey} value={langKey}>
{langName}
</MenuItem>
))}
</TextField>
<TextField
size="small"
label={i18n("terms")}
helperText={i18n("terms_helper")}
name="terms"
value={terms}
disabled={disabled}
onChange={handleChange}
multiline
/>
<TextField
size="small"
label={i18n("fixer_selector")}
name="fixerSelector"
value={fixerSelector}
disabled={disabled}
onChange={handleChange}
multiline
/>
<TextField
select
size="small"
name="fixerFunc"
value={fixerFunc}
label={i18n("fixer_function")}
helperText={i18n("fixer_function_helper")}
disabled={disabled}
onChange={handleChange}
>
{GlobalItem}
{FIXER_ALL.map((item) => (
<MenuItem key={item} value={item}>
{item}
</MenuItem>
))}
</TextField>
<TextField
size="small"
label={i18n("selector_style")}
helperText={i18n("selector_style_helper")}
name="selectStyle"
value={selectStyle}
disabled={disabled}
onChange={handleChange}
maxRows={10}
multiline
/>
<TextField
size="small"
label={i18n("selector_parent_style")}
helperText={i18n("selector_style_helper")}
name="parentStyle"
value={parentStyle}
disabled={disabled}
onChange={handleChange}
maxRows={10}
multiline
/>
<TextField
size="small"
label={i18n("inject_css")}
helperText={i18n("inject_css_helper")}
name="injectCss"
value={injectCss}
disabled={disabled}
onChange={handleChange}
maxRows={10}
multiline
/>
<TextField
size="small"
label={i18n("inject_js")}
helperText={i18n("inject_js_helper")}
name="injectJs"
value={injectJs}
disabled={disabled}
onChange={handleChange}
maxRows={10}
multiline
/>
</>
)}
{rules && {rules &&
(editMode ? ( (editMode ? (
// 编辑 // 编辑
@@ -327,6 +547,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
e.preventDefault(); e.preventDefault();
setDisabled(false); setDisabled(false);
}} }}
startIcon={<EditIcon />}
> >
{i18n("edit")} {i18n("edit")}
</Button> </Button>
@@ -338,35 +559,87 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
e.preventDefault(); e.preventDefault();
rules.del(rule.pattern); rules.del(rule.pattern);
}} }}
startIcon={<DeleteIcon />}
> >
{i18n("delete")} {i18n("delete")}
</Button> </Button>
)} )}
{!showMore && (
<Button
size="small"
variant="text"
onClick={() => {
setShowMore(true);
}}
startIcon={<ExpandMoreIcon />}
>
{i18n("more")}
</Button>
)}
</> </>
) : ( ) : (
<> <>
<Button size="small" variant="contained" type="submit"> <Button
size="small"
variant="contained"
type="submit"
startIcon={<SaveIcon />}
>
{i18n("save")} {i18n("save")}
</Button> </Button>
<Button <Button
size="small" size="small"
variant="outlined" variant="outlined"
onClick={handleCancel} onClick={handleCancel}
startIcon={<CancelIcon />}
> >
{i18n("cancel")} {i18n("cancel")}
</Button> </Button>
{!showMore && (
<Button
size="small"
variant="text"
onClick={() => {
setShowMore(true);
}}
startIcon={<ExpandMoreIcon />}
>
{i18n("more")}
</Button>
)}
</> </>
)} )}
</Stack> </Stack>
) : ( ) : (
// 添加 // 添加
<Stack direction="row" spacing={2}> <Stack direction="row" spacing={2}>
<Button size="small" variant="contained" type="submit"> <Button
size="small"
variant="contained"
type="submit"
startIcon={<SaveIcon />}
>
{i18n("save")} {i18n("save")}
</Button> </Button>
<Button size="small" variant="outlined" onClick={handleCancel}> <Button
size="small"
variant="outlined"
onClick={handleCancel}
startIcon={<CancelIcon />}
>
{i18n("cancel")} {i18n("cancel")}
</Button> </Button>
{!showMore && (
<Button
size="small"
variant="text"
onClick={() => {
setShowMore(true);
}}
>
{i18n("more")}
</Button>
)}
</Stack> </Stack>
))} ))}
</Stack> </Stack>
@@ -429,7 +702,7 @@ function ShareButton({ rules, injectRules, selectedUrl }) {
window.open(url, "_blank"); window.open(url, "_blank");
} catch (err) { } catch (err) {
alert.warning(i18n("error_got_some_wrong")); alert.warning(i18n("error_got_some_wrong"));
console.log("[share rules]", err); kissLog(err, "share rules");
} }
}; };
@@ -459,7 +732,7 @@ function UserRules({ subRules }) {
try { try {
await rules.merge(JSON.parse(data)); await rules.merge(JSON.parse(data));
} catch (err) { } catch (err) {
console.log("[import rules]", err); kissLog(err, "import rules");
} }
}; };
@@ -496,6 +769,7 @@ function UserRules({ subRules }) {
e.preventDefault(); e.preventDefault();
setShowAdd(true); setShowAdd(true);
}} }}
startIcon={<AddIcon />}
> >
{i18n("add")} {i18n("add")}
</Button> </Button>
@@ -591,7 +865,7 @@ function SubRulesItem({
await delSubRules(url); await delSubRules(url);
await deleteDataCache(url); await deleteDataCache(url);
} catch (err) { } catch (err) {
console.log("[del subrules]", err); kissLog(err, "del subrules");
} }
}; };
@@ -604,7 +878,7 @@ function SubRulesItem({
} }
await updateDataCache(url); await updateDataCache(url);
} catch (err) { } catch (err) {
console.log("[sync sub rules]", err); kissLog(err, "sync sub rules");
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -683,7 +957,7 @@ function SubRulesEdit({ subList, addSub, updateDataCache }) {
setShowInput(false); setShowInput(false);
setInputText(""); setInputText("");
} catch (err) { } catch (err) {
console.log("[fetch rules]", err); kissLog(err, "fetch rules");
setInputError(i18n("error_fetch_url")); setInputError(i18n("error_fetch_url"));
} finally { } finally {
setLoading(false); setLoading(false);
@@ -711,6 +985,7 @@ function SubRulesEdit({ subList, addSub, updateDataCache }) {
e.preventDefault(); e.preventDefault();
setShowInput(true); setShowInput(true);
}} }}
startIcon={<AddIcon />}
> >
{i18n("add")} {i18n("add")}
</Button> </Button>
@@ -735,10 +1010,16 @@ function SubRulesEdit({ subList, addSub, updateDataCache }) {
variant="contained" variant="contained"
onClick={handleSave} onClick={handleSave}
disabled={loading} disabled={loading}
startIcon={<SaveIcon />}
> >
{i18n("save")} {i18n("save")}
</Button> </Button>
<Button size="small" variant="outlined" onClick={handleCancel}> <Button
size="small"
variant="outlined"
onClick={handleCancel}
startIcon={<CancelIcon />}
>
{i18n("cancel")} {i18n("cancel")}
</Button> </Button>
</Stack> </Stack>
@@ -826,6 +1107,8 @@ export default function Rules() {
{i18n("rules_warn_1")} {i18n("rules_warn_1")}
<br /> <br />
{i18n("rules_warn_2")} {i18n("rules_warn_2")}
<br />
{i18n("rules_warn_3")}
</Alert> </Alert>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}> <Box sx={{ borderBottom: 1, borderColor: "divider" }}>

View File

@@ -17,20 +17,20 @@ import {
UI_LANGS, UI_LANGS,
TRANS_NEWLINE_LENGTH, TRANS_NEWLINE_LENGTH,
CACHE_NAME, CACHE_NAME,
OPT_MOUSEKEY_ALL,
OPT_MOUSEKEY_DISABLE,
OPT_SHORTCUT_TRANSLATE, OPT_SHORTCUT_TRANSLATE,
OPT_SHORTCUT_STYLE, OPT_SHORTCUT_STYLE,
OPT_SHORTCUT_POPUP, OPT_SHORTCUT_POPUP,
OPT_SHORTCUT_SETTING, OPT_SHORTCUT_SETTING,
OPT_LANGS_TO,
DEFAULT_BLACKLIST, DEFAULT_BLACKLIST,
DEFAULT_CSPLIST,
MSG_CONTEXT_MENUS, MSG_CONTEXT_MENUS,
MSG_UPDATE_CSP,
} from "../../config"; } from "../../config";
import { useShortcut } from "../../hooks/Shortcut"; import { useShortcut } from "../../hooks/Shortcut";
import ShortcutInput from "./ShortcutInput"; import ShortcutInput from "./ShortcutInput";
import { useFab } from "../../hooks/Fab"; import { useFab } from "../../hooks/Fab";
import { sendBgMsg } from "../../libs/msg"; import { sendBgMsg } from "../../libs/msg";
import { kissLog } from "../../libs/log";
function ShortcutItem({ action, label }) { function ShortcutItem({ action, label }) {
const { shortcut, setShortcut } = useShortcut(action); const { shortcut, setShortcut } = useShortcut(action);
@@ -55,6 +55,9 @@ export default function Settings() {
case "fetchInterval": case "fetchInterval":
value = limitNumber(value, 0, 5000); value = limitNumber(value, 0, 5000);
break; break;
case "transInterval":
value = limitNumber(value, 100, 5000);
break;
case "minLength": case "minLength":
value = limitNumber(value, 1, 100); value = limitNumber(value, 1, 100);
break; break;
@@ -67,8 +70,11 @@ export default function Settings() {
case "touchTranslate": case "touchTranslate":
value = limitNumber(value, 0, 4); value = limitNumber(value, 0, 4);
break; break;
case "contextMenus": case "contextMenuType":
isExt && sendBgMsg(MSG_CONTEXT_MENUS, { contextMenus: value }); isExt && sendBgMsg(MSG_CONTEXT_MENUS, value);
break;
case "csplist":
isExt && sendBgMsg(MSG_UPDATE_CSP, value);
break; break;
default: default:
} }
@@ -82,24 +88,21 @@ export default function Settings() {
caches.delete(CACHE_NAME); caches.delete(CACHE_NAME);
alert.success(i18n("clear_success")); alert.success(i18n("clear_success"));
} catch (err) { } catch (err) {
console.log("[clear cache]", err); kissLog(err, "clear cache");
} }
}; };
const { const {
uiLang, uiLang,
fetchLimit,
fetchInterval,
minLength, minLength,
maxLength, maxLength,
clearCache, clearCache,
newlineLength = TRANS_NEWLINE_LENGTH, newlineLength = TRANS_NEWLINE_LENGTH,
mouseKey = OPT_MOUSEKEY_DISABLE, contextMenuType = 1,
detectRemote = false,
contextMenus = true,
touchTranslate = 2, touchTranslate = 2,
blacklist = DEFAULT_BLACKLIST.join(",\n"), blacklist = DEFAULT_BLACKLIST.join(",\n"),
disableLangs = [], csplist = DEFAULT_CSPLIST.join(",\n"),
transInterval = 500,
} = setting; } = setting;
const { isHide = false } = fab || {}; const { isHide = false } = fab || {};
@@ -122,24 +125,6 @@ export default function Settings() {
</Select> </Select>
</FormControl> </FormControl>
<TextField
size="small"
label={i18n("fetch_limit")}
type="number"
name="fetchLimit"
defaultValue={fetchLimit}
onChange={handleChange}
/>
<TextField
size="small"
label={i18n("fetch_interval")}
type="number"
name="fetchInterval"
defaultValue={fetchInterval}
onChange={handleChange}
/>
<TextField <TextField
size="small" size="small"
label={i18n("min_translate_length")} label={i18n("min_translate_length")}
@@ -167,21 +152,14 @@ export default function Settings() {
onChange={handleChange} onChange={handleChange}
/> />
<FormControl size="small"> <TextField
<InputLabel>{i18n("mouseover_translation")}</InputLabel> size="small"
<Select label={i18n("translate_interval")}
name="mouseKey" type="number"
value={mouseKey} name="transInterval"
label={i18n("mouseover_translation")} defaultValue={transInterval}
onChange={handleChange} onChange={handleChange}
> />
{OPT_MOUSEKEY_ALL.map((item) => (
<MenuItem key={item} value={item}>
{i18n(item)}
</MenuItem>
))}
</Select>
</FormControl>
<FormControl size="small"> <FormControl size="small">
<InputLabel>{i18n("touch_translate_shortcut")}</InputLabel> <InputLabel>{i18n("touch_translate_shortcut")}</InputLabel>
@@ -215,50 +193,19 @@ export default function Settings() {
</FormControl> </FormControl>
<FormControl size="small"> <FormControl size="small">
<InputLabel>{i18n("add_context_menus")}</InputLabel> <InputLabel>{i18n("context_menus")}</InputLabel>
<Select <Select
name="contextMenus" name="contextMenuType"
value={contextMenus} value={contextMenuType}
label={i18n("add_context_menus")} label={i18n("context_menus")}
onChange={handleChange} onChange={handleChange}
> >
<MenuItem value={false}>{i18n("disable")}</MenuItem> <MenuItem value={0}>{i18n("hide_context_menus")}</MenuItem>
<MenuItem value={true}>{i18n("enable")}</MenuItem> <MenuItem value={1}>{i18n("simple_context_menus")}</MenuItem>
<MenuItem value={2}>{i18n("secondary_context_menus")}</MenuItem>
</Select> </Select>
</FormControl> </FormControl>
<FormControl size="small">
<InputLabel>{i18n("detect_lang_remote")}</InputLabel>
<Select
name="detectRemote"
value={detectRemote}
label={i18n("detect_lang_remote")}
onChange={handleChange}
>
<MenuItem value={false}>{i18n("disable")}</MenuItem>
<MenuItem value={true}>{i18n("enable")}</MenuItem>
</Select>
<FormHelperText>{i18n("detect_lang_remote_help")}</FormHelperText>
</FormControl>
<FormControl size="small">
<InputLabel>{i18n("disable_langs")}</InputLabel>
<Select
multiple
name="disableLangs"
value={disableLangs}
label={i18n("disable_langs")}
onChange={handleChange}
>
{OPT_LANGS_TO.map(([langKey, langName]) => (
<MenuItem key={langKey} value={langKey}>
{langName}
</MenuItem>
))}
</Select>
<FormHelperText>{i18n("disable_langs_helper")}</FormHelperText>
</FormControl>
{isExt ? ( {isExt ? (
<> <>
<FormControl size="small"> <FormControl size="small">
@@ -278,6 +225,18 @@ export default function Settings() {
</Link> </Link>
</FormHelperText> </FormHelperText>
</FormControl> </FormControl>
<TextField
size="small"
label={i18n("disabled_csplist")}
helperText={
i18n("pattern_helper") + " " + i18n("disabled_csplist_helper")
}
name="csplist"
defaultValue={csplist}
onChange={handleChange}
multiline
/>
</> </>
) : ( ) : (
<> <>

View File

@@ -19,6 +19,7 @@ import { useAlert } from "../../hooks/Alert";
import SyncIcon from "@mui/icons-material/Sync"; import SyncIcon from "@mui/icons-material/Sync";
import CircularProgress from "@mui/material/CircularProgress"; import CircularProgress from "@mui/material/CircularProgress";
import { useSetting } from "../../hooks/Setting"; import { useSetting } from "../../hooks/Setting";
import { kissLog } from "../../libs/log";
export default function SyncSetting() { export default function SyncSetting() {
const i18n = useI18n(); const i18n = useI18n();
@@ -43,7 +44,7 @@ export default function SyncSetting() {
await reloadSetting(); await reloadSetting();
alert.success(i18n("sync_success")); alert.success(i18n("sync_success"));
} catch (err) { } catch (err) {
console.log("[sync all]", err); kissLog(err, "sync all");
alert.error(i18n("sync_failed")); alert.error(i18n("sync_failed"));
} finally { } finally {
setLoading(false); setLoading(false);

View File

@@ -50,6 +50,7 @@ export default function Tranbox() {
btnOffsetX, btnOffsetX,
btnOffsetY, btnOffsetY,
hideTranBtn = false, hideTranBtn = false,
hideClickAway = false,
} = tranboxSetting; } = tranboxSetting;
return ( return (
@@ -160,6 +161,18 @@ export default function Tranbox() {
<MenuItem value={true}>{i18n("hide")}</MenuItem> <MenuItem value={true}>{i18n("hide")}</MenuItem>
</TextField> </TextField>
<TextField
select
size="small"
name="hideClickAway"
value={hideClickAway}
label={i18n("hide_click_away")}
onChange={handleChange}
>
<MenuItem value={false}>{i18n("disable")}</MenuItem>
<MenuItem value={true}>{i18n("enable")}</MenuItem>
</TextField>
{!isExt && ( {!isExt && (
<ShortcutInput <ShortcutInput
value={tranboxShortcut} value={tranboxShortcut}

View File

@@ -1,357 +0,0 @@
import Stack from "@mui/material/Stack";
import TextField from "@mui/material/TextField";
import { useCallback, useEffect, useState } from "react";
import { useI18n } from "../../hooks/I18n";
import Typography from "@mui/material/Typography";
import Accordion from "@mui/material/Accordion";
import AccordionSummary from "@mui/material/AccordionSummary";
import AccordionDetails from "@mui/material/AccordionDetails";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import Alert from "@mui/material/Alert";
import Box from "@mui/material/Box";
import FormControlLabel from "@mui/material/FormControlLabel";
import Switch from "@mui/material/Switch";
import { useSetting } from "../../hooks/Setting";
import CircularProgress from "@mui/material/CircularProgress";
import { syncWebfix, loadOrFetchWebfix, FIXER_ALL } from "../../libs/webfix";
import Button from "@mui/material/Button";
import SyncIcon from "@mui/icons-material/Sync";
import { useAlert } from "../../hooks/Alert";
import HelpButton from "./HelpButton";
import { URL_KISS_RULES_NEW_ISSUE } from "../../config";
import MenuItem from "@mui/material/MenuItem";
import { useWebfixRules } from "../../hooks/WebfixRules";
function WebfixFields({ rule, webfix, setShow }) {
const editMode = !!rule;
const initFormValues = rule || {
pattern: "",
selector: "",
rootSelector: "",
fixer: FIXER_ALL[0],
};
const i18n = useI18n();
const [disabled, setDisabled] = useState(editMode);
const [errors, setErrors] = useState({});
const [formValues, setFormValues] = useState(initFormValues);
const { pattern, selector, rootSelector, fixer } = formValues;
const hasSamePattern = (str) => {
for (const item of webfix.list || []) {
if (item.pattern === str && rule?.pattern !== str) {
return true;
}
}
return false;
};
const handleFocus = (e) => {
e.preventDefault();
const { name } = e.target;
setErrors((pre) => ({ ...pre, [name]: "" }));
};
const handleChange = (e) => {
e.preventDefault();
const { name, value } = e.target;
setFormValues((pre) => ({ ...pre, [name]: value }));
};
const handleCancel = (e) => {
e.preventDefault();
if (editMode) {
setDisabled(true);
} else {
setShow(false);
}
setFormValues(initFormValues);
};
const handleSubmit = (e) => {
e.preventDefault();
const errors = {};
if (!pattern.trim()) {
errors.pattern = i18n("error_cant_be_blank");
}
if (hasSamePattern(pattern)) {
errors.pattern = i18n("error_duplicate_values");
}
if (!selector.trim()) {
errors.selector = i18n("error_cant_be_blank");
}
if (Object.keys(errors).length > 0) {
setErrors(errors);
return;
}
if (editMode) {
// 编辑
setDisabled(true);
webfix.put(rule.pattern, formValues);
} else {
// 添加
webfix.add(formValues);
setShow(false);
setFormValues(initFormValues);
}
};
return (
<form onSubmit={handleSubmit}>
<Stack spacing={3}>
<TextField
size="small"
label={i18n("pattern")}
error={!!errors.pattern}
helperText={errors.pattern}
name="pattern"
value={pattern}
disabled={disabled}
onChange={handleChange}
onFocus={handleFocus}
multiline
/>
<TextField
size="small"
label={i18n("root_selector")}
error={!!errors.rootSelector}
helperText={errors.rootSelector}
name="rootSelector"
value={rootSelector}
disabled={disabled}
onChange={handleChange}
onFocus={handleFocus}
multiline
/>
<TextField
size="small"
label={i18n("selector")}
error={!!errors.selector}
helperText={errors.selector}
name="selector"
value={selector}
disabled={disabled}
onChange={handleChange}
onFocus={handleFocus}
multiline
/>
<TextField
select
size="small"
name="fixer"
value={fixer}
label={i18n("fixer_function")}
disabled={disabled}
onChange={handleChange}
>
{FIXER_ALL.map((item) => (
<MenuItem key={item} value={item}>
{item}
</MenuItem>
))}
</TextField>
{webfix &&
(editMode ? (
// 编辑
<Stack direction="row" spacing={2}>
{disabled ? (
<>
<Button
size="small"
variant="contained"
onClick={(e) => {
e.preventDefault();
setDisabled(false);
}}
>
{i18n("edit")}
</Button>
<Button
size="small"
variant="outlined"
onClick={(e) => {
e.preventDefault();
webfix.del(rule.pattern);
}}
>
{i18n("delete")}
</Button>
</>
) : (
<>
<Button size="small" variant="contained" type="submit">
{i18n("save")}
</Button>
<Button
size="small"
variant="outlined"
onClick={handleCancel}
>
{i18n("cancel")}
</Button>
</>
)}
</Stack>
) : (
// 添加
<Stack direction="row" spacing={2}>
<Button size="small" variant="contained" type="submit">
{i18n("save")}
</Button>
<Button size="small" variant="outlined" onClick={handleCancel}>
{i18n("cancel")}
</Button>
</Stack>
))}
</Stack>
</form>
);
}
function WebfixAccordion({ rule, webfix }) {
const [expanded, setExpanded] = useState(false);
const handleChange = (e) => {
setExpanded((pre) => !pre);
};
return (
<Accordion expanded={expanded} onChange={handleChange}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography
sx={{
opacity: webfix ? 1 : 0.5,
overflowWrap: "anywhere",
}}
>
{rule.pattern}
</Typography>
</AccordionSummary>
<AccordionDetails>
{expanded && <WebfixFields rule={rule} webfix={webfix} />}
</AccordionDetails>
</Accordion>
);
}
export default function Webfix() {
const [loading, setLoading] = useState(false);
const [sites, setSites] = useState([]);
const i18n = useI18n();
const alert = useAlert();
const { setting, updateSetting } = useSetting();
const [showAdd, setShowAdd] = useState(false);
const webfix = useWebfixRules();
const loadSites = useCallback(async () => {
const sites = await loadOrFetchWebfix(process.env.REACT_APP_WEBFIXURL);
setSites(sites);
}, []);
const handleSyncTest = async (e) => {
e.preventDefault();
try {
setLoading(true);
await syncWebfix(process.env.REACT_APP_WEBFIXURL);
await loadSites();
alert.success(i18n("sync_success"));
} catch (err) {
console.log("[sync webfix]", err);
alert.error(i18n("sync_failed"));
} finally {
setLoading(false);
}
};
useEffect(() => {
(async () => {
try {
setLoading(true);
await loadSites();
} catch (err) {
console.log("[load webfix]", err.message);
} finally {
setLoading(false);
}
})();
}, [loadSites]);
return (
<Box>
<Stack spacing={3}>
<Alert severity="info">{i18n("patch_setting_help")}</Alert>
<Stack
direction="row"
alignItems="center"
spacing={2}
useFlexGap
flexWrap="wrap"
>
<Button
size="small"
variant="contained"
disabled={showAdd}
onClick={(e) => {
e.preventDefault();
setShowAdd(true);
}}
>
{i18n("add")}
</Button>
<Button
size="small"
variant="outlined"
disabled={loading}
onClick={handleSyncTest}
startIcon={<SyncIcon />}
>
{i18n("sync_now")}
</Button>
<HelpButton url={URL_KISS_RULES_NEW_ISSUE} />
<FormControlLabel
control={
<Switch
size="small"
checked={!!setting.injectWebfix}
onChange={() => {
updateSetting({
injectWebfix: !setting.injectWebfix,
});
}}
/>
}
label={i18n("inject_webfix")}
/>
</Stack>
{showAdd && <WebfixFields webfix={webfix} setShow={setShowAdd} />}
{webfix.list?.length > 0 && (
<Box>
{webfix.list.map((rule) => (
<WebfixAccordion key={rule.pattern} rule={rule} webfix={webfix} />
))}
</Box>
)}
{setting.injectWebfix && (
<Box>
{loading ? (
<center>
<CircularProgress size={16} />
</center>
) : (
sites.map((rule) => (
<WebfixAccordion key={rule.pattern} rule={rule} />
))
)}
</Box>
)}
</Stack>
</Box>
);
}

View File

@@ -18,7 +18,6 @@ import Stack from "@mui/material/Stack";
import { adaptScript } from "../../libs/gm"; import { adaptScript } from "../../libs/gm";
import Alert from "@mui/material/Alert"; import Alert from "@mui/material/Alert";
import Apis from "./Apis"; import Apis from "./Apis";
import Webfix from "./Webfix";
import InputSetting from "./InputSetting"; import InputSetting from "./InputSetting";
import Tranbox from "./Tranbox"; import Tranbox from "./Tranbox";
import FavWords from "./FavWords"; import FavWords from "./FavWords";
@@ -41,7 +40,7 @@ export default function Options() {
setError( setError(
`The version of the local script(v${version}) is not the latest version(v${process.env.REACT_APP_VERSION}). 本地脚本之版本(v${version})非最新版(v${process.env.REACT_APP_VERSION})。` `The version of the local script(v${version}) is not the latest version(v${process.env.REACT_APP_VERSION}). 本地脚本之版本(v${version})非最新版(v${process.env.REACT_APP_VERSION})。`
); );
break; return;
} }
if (eventName) { if (eventName) {
@@ -49,9 +48,6 @@ export default function Options() {
adaptScript(eventName); adaptScript(eventName);
} }
// 同步数据
await trySyncSettingAndRules();
setReady(true);
break; break;
} }
@@ -59,16 +55,16 @@ export default function Options() {
setError( setError(
"Time out. Please confirm whether to install or enable KISS Translator GreaseMonkey script? 连接超时,请检查是否安装或启用简约翻译油猴脚本。" "Time out. Please confirm whether to install or enable KISS Translator GreaseMonkey script? 连接超时,请检查是否安装或启用简约翻译油猴脚本。"
); );
break; return;
} }
await sleep(1000); await sleep(1000);
} }
} else {
// 同步数据
await trySyncSettingAndRules();
setReady(true);
} }
// 同步数据
await trySyncSettingAndRules();
setReady(true);
})(); })();
}, []); }, []);
@@ -125,7 +121,6 @@ export default function Options() {
<Route path="tranbox" element={<Tranbox />} /> <Route path="tranbox" element={<Tranbox />} />
<Route path="apis" element={<Apis />} /> <Route path="apis" element={<Apis />} />
<Route path="sync" element={<SyncSetting />} /> <Route path="sync" element={<SyncSetting />} />
<Route path="webfix" element={<Webfix />} />
<Route path="words" element={<FavWords />} /> <Route path="words" element={<FavWords />} />
<Route path="about" element={<About />} /> <Route path="about" element={<About />} />
</Route> </Route>

View File

@@ -5,7 +5,7 @@ import MenuItem from "@mui/material/MenuItem";
import FormControlLabel from "@mui/material/FormControlLabel"; import FormControlLabel from "@mui/material/FormControlLabel";
import Switch from "@mui/material/Switch"; import Switch from "@mui/material/Switch";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import { sendBgMsg, sendTabMsg, getTabInfo } from "../../libs/msg"; import { sendBgMsg, sendTabMsg, getCurTab } from "../../libs/msg";
import { browser } from "../../libs/browser"; import { browser } from "../../libs/browser";
import { isExt } from "../../libs/client"; import { isExt } from "../../libs/client";
import { useI18n } from "../../hooks/I18n"; import { useI18n } from "../../hooks/I18n";
@@ -18,6 +18,7 @@ import {
MSG_TRANS_PUTRULE, MSG_TRANS_PUTRULE,
MSG_OPEN_OPTIONS, MSG_OPEN_OPTIONS,
MSG_SAVE_RULE, MSG_SAVE_RULE,
MSG_COMMAND_SHORTCUTS,
OPT_TRANS_ALL, OPT_TRANS_ALL,
OPT_LANGS_FROM, OPT_LANGS_FROM,
OPT_LANGS_TO, OPT_LANGS_TO,
@@ -26,10 +27,12 @@ import {
import { sendIframeMsg } from "../../libs/iframe"; import { sendIframeMsg } from "../../libs/iframe";
import { saveRule } from "../../libs/rules"; import { saveRule } from "../../libs/rules";
import { tryClearCaches } from "../../libs"; import { tryClearCaches } from "../../libs";
import { kissLog } from "../../libs/log";
export default function Popup({ setShowPopup, translator: tran }) { export default function Popup({ setShowPopup, translator: tran }) {
const i18n = useI18n(); const i18n = useI18n();
const [rule, setRule] = useState(tran?.rule); const [rule, setRule] = useState(tran?.rule);
const [commands, setCommands] = useState({});
const handleOpenSetting = () => { const handleOpenSetting = () => {
if (!tran) { if (!tran) {
@@ -53,7 +56,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
sendIframeMsg(MSG_TRANS_TOGGLE); sendIframeMsg(MSG_TRANS_TOGGLE);
} }
} catch (err) { } catch (err) {
console.log("[toggle trans]", err); kissLog(err, "toggle trans");
} }
}; };
@@ -69,7 +72,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
sendIframeMsg(MSG_TRANS_PUTRULE, { [name]: value }); sendIframeMsg(MSG_TRANS_PUTRULE, { [name]: value });
} }
} catch (err) { } catch (err) {
console.log("[update rule]", err); kissLog(err, "update rule");
} }
}; };
@@ -81,17 +84,17 @@ export default function Popup({ setShowPopup, translator: tran }) {
try { try {
let href = window.location.href; let href = window.location.href;
if (!tran) { if (!tran) {
const tab = await getTabInfo(); const tab = await getCurTab();
href = tab.url; href = tab.url;
} }
const newRule = { ...rule, pattern: href }; const newRule = { ...rule, pattern: href.split("/")[2] };
if (isExt && tran) { if (isExt && tran) {
sendBgMsg(MSG_SAVE_RULE, newRule); sendBgMsg(MSG_SAVE_RULE, newRule);
} else { } else {
saveRule(newRule); saveRule(newRule);
} }
} catch (err) { } catch (err) {
console.log("[save rule]", err); kissLog(err, "save rule");
} }
}; };
@@ -106,7 +109,31 @@ export default function Popup({ setShowPopup, translator: tran }) {
setRule(res.data); setRule(res.data);
} }
} catch (err) { } catch (err) {
console.log("[query rule]", err); kissLog(err, "query rule");
}
})();
}, [tran]);
useEffect(() => {
(async () => {
try {
const commands = {};
if (isExt) {
const res = await sendBgMsg(MSG_COMMAND_SHORTCUTS);
res.forEach(({ name, shortcut }) => {
commands[name] = shortcut;
});
} else {
const shortcuts = tran.setting.shortcuts;
if (shortcuts) {
Object.entries(shortcuts).forEach(([key, val]) => {
commands[key] = val.join("+");
});
}
}
setCommands(commands);
} catch (err) {
kissLog(err, "query cmds");
} }
})(); })();
}, [tran]); }, [tran]);
@@ -153,7 +180,11 @@ export default function Popup({ setShowPopup, translator: tran }) {
onChange={handleTransToggle} onChange={handleTransToggle}
/> />
} }
label={i18n("translate_alt")} label={
commands["toggleTranslate"]
? `${i18n("translate_alt")}(${commands["toggleTranslate"]})`
: i18n("translate_alt")
}
/> />
</Stack> </Stack>
@@ -211,7 +242,11 @@ export default function Popup({ setShowPopup, translator: tran }) {
size="small" size="small"
value={textStyle} value={textStyle}
name="textStyle" name="textStyle"
label={i18n("text_style_alt")} label={
commands["toggleStyle"]
? `${i18n("text_style_alt")}(${commands["toggleStyle"]})`
: i18n("text_style_alt")
}
onChange={handleChange} onChange={handleChange}
> >
{OPT_STYLE_ALL.map((item) => ( {OPT_STYLE_ALL.map((item) => (

View File

@@ -0,0 +1,29 @@
import IconButton from "@mui/material/IconButton";
import VolumeUpIcon from "@mui/icons-material/VolumeUp";
import { useTextAudio } from "../../hooks/Audio";
export default function AudioBtn({ text, lan = "uk" }) {
const { error, ready, playing, onPlay } = useTextAudio(text, lan);
if (error || !ready) {
return (
<IconButton disabled>
<VolumeUpIcon />
</IconButton>
);
}
if (playing) {
return (
<IconButton color="primary">
<VolumeUpIcon />
</IconButton>
);
}
return (
<IconButton onClick={onPlay}>
<VolumeUpIcon />
</IconButton>
);
}

View File

@@ -1,16 +1,12 @@
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Chip from "@mui/material/Chip";
import Stack from "@mui/material/Stack"; import Stack from "@mui/material/Stack";
import FavBtn from "./FavBtn"; import FavBtn from "./FavBtn";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import AudioBtn from "./AudioBtn";
const exchangeMap = { const phonicMap = {
word_third: "第三人称单数", en_phonic: ["英", "uk"],
word_ing: "现在分词", us_phonic: ["美", "en"],
word_done: "过去式",
word_past: "过去分词",
word_pl: "复数",
word_proto: "词源",
}; };
export default function DictCont({ dictResult }) { export default function DictCont({ dictResult }) {
@@ -26,40 +22,32 @@ export default function DictCont({ dictResult }) {
alignItems="flex-start" alignItems="flex-start"
> >
<Typography variant="subtitle1" style={{ fontWeight: "bold" }}> <Typography variant="subtitle1" style={{ fontWeight: "bold" }}>
{dictResult.simple_means?.word_name} {dictResult.src}
</Typography> </Typography>
<FavBtn word={dictResult.simple_means?.word_name} /> <FavBtn word={dictResult.src} />
</Stack> </Stack>
{dictResult.simple_means?.symbols?.map(({ ph_en, ph_am, parts }, idx) => ( <Typography component="div">
<Typography key={idx} component="div"> <Stack direction="row">
{(ph_en || ph_am) && ( {dictResult.voice
<Typography>{`英 /${ph_en || ""}/ 美 /${ph_am || ""}/`}</Typography> ?.map(Object.entries)
)} .map((item) => item[0])
<ul style={{ margin: "0.5em 0" }}> .map(([key, val]) => (
{parts.map(({ part, means }, idx) => ( <span>
<li key={idx}> <span>{`${phonicMap[key]?.[0] || key} ${val}`}</span>
{part ? `[${part}] ${means.join("; ")}` : means.join("; ")} <AudioBtn text={dictResult.src} lan={phonicMap[key]?.[1]} />
</li> </span>
))} ))}
</ul> </Stack>
</Typography> <ul style={{ margin: "0.5em 0" }}>
))} {dictResult.content[0].mean.map(({ pre, cont }, idx) => (
<li key={idx}>
<Typography> {pre && `[${pre}] `}
{Object.entries(dictResult.simple_means?.exchange || {}) {Object.keys(cont).join("; ")}
.map(([key, val]) => `${exchangeMap[key] || key}: ${val.join(", ")}`) </li>
.join("; ")}
</Typography>
<Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap>
{Object.values(dictResult.simple_means?.tags || {})
.flat()
.filter((item) => item)
.map((item) => (
<Chip label={item} size="small" />
))} ))}
</Stack> </ul>
</Typography>
</Box> </Box>
); );
} }

View File

@@ -0,0 +1,65 @@
import Box from "@mui/material/Box";
import Chip from "@mui/material/Chip";
import Stack from "@mui/material/Stack";
import FavBtn from "./FavBtn";
import Typography from "@mui/material/Typography";
const exchangeMap = {
word_third: "第三人称单数",
word_ing: "现在分词",
word_done: "过去式",
word_past: "过去分词",
word_pl: "复数",
word_proto: "词源",
};
export default function DictCont({ dictResult }) {
if (!dictResult) {
return;
}
return (
<Box>
<Stack
direction="row"
justifyContent="space-between"
alignItems="flex-start"
>
<Typography variant="subtitle1" style={{ fontWeight: "bold" }}>
{dictResult.simple_means?.word_name}
</Typography>
<FavBtn word={dictResult.simple_means?.word_name} />
</Stack>
{dictResult.simple_means?.symbols?.map(({ ph_en, ph_am, parts }, idx) => (
<Typography key={idx} component="div">
{(ph_en || ph_am) && (
<Typography>{`英 /${ph_en || ""}/ 美 /${ph_am || ""}/`}</Typography>
)}
<ul style={{ margin: "0.5em 0" }}>
{parts.map(({ part, means }, idx) => (
<li key={idx}>
{part ? `[${part}] ${means.join("; ")}` : means.join("; ")}
</li>
))}
</ul>
</Typography>
))}
<Typography>
{Object.entries(dictResult.simple_means?.exchange || {})
.map(([key, val]) => `${exchangeMap[key] || key}: ${val.join(", ")}`)
.join("; ")}
</Typography>
<Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap>
{Object.values(dictResult.simple_means?.tags || {})
.flat()
.filter((item) => item)
.map((item) => (
<Chip label={item} size="small" />
))}
</Stack>
</Box>
);
}

View File

@@ -148,6 +148,7 @@ export default function DraggableResizable({
}, },
onChangeSize, onChangeSize,
onChangePosition, onChangePosition,
...props
}) { }) {
const lineWidth = 4; const lineWidth = 4;
const [position, setPosition] = useState(defaultPosition); const [position, setPosition] = useState(defaultPosition);
@@ -182,6 +183,7 @@ export default function DraggableResizable({
gridTemplateRows: `${lineWidth * 2}px auto ${lineWidth * 2}px`, gridTemplateRows: `${lineWidth * 2}px auto ${lineWidth * 2}px`,
zIndex: 2147483647, zIndex: 2147483647,
}} }}
{...props}
> >
<Pointer <Pointer
direction="TopLeft" direction="TopLeft"

View File

@@ -3,6 +3,7 @@ import FavoriteIcon from "@mui/icons-material/Favorite";
import FavoriteBorderIcon from "@mui/icons-material/FavoriteBorder"; import FavoriteBorderIcon from "@mui/icons-material/FavoriteBorder";
import { useState } from "react"; import { useState } from "react";
import { useFavWords } from "../../hooks/FavWords"; import { useFavWords } from "../../hooks/FavWords";
import { kissLog } from "../../libs/log";
export default function FavBtn({ word }) { export default function FavBtn({ word }) {
const { favWords, toggleFav } = useFavWords(); const { favWords, toggleFav } = useFavWords();
@@ -13,7 +14,7 @@ export default function FavBtn({ word }) {
setLoading(true); setLoading(true);
await toggleFav(word); await toggleFav(word);
} catch (err) { } catch (err) {
console.log("[set fav]", err); kissLog(err, "set fav");
} finally { } finally {
setLoading(false); setLoading(false);
} }

View File

@@ -0,0 +1,17 @@
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
export default function SugCont({ sugs }) {
return (
<Box>
{sugs.map(({ k, v }) => (
<Typography component="div" key={k}>
<Typography>{k}</Typography>
<ul style={{ margin: "0" }}>
<li>{v}</li>
</ul>
</Typography>
))}
</Box>
);
}

View File

@@ -102,17 +102,12 @@ function TranForm({ text, setText, tranboxSetting, transApis }) {
fullWidth fullWidth
multiline multiline
value={editMode ? editText : text} value={editMode ? editText : text}
disabled={!editMode}
onChange={(e) => { onChange={(e) => {
setEditText(e.target.value); setEditText(e.target.value);
}} }}
onClick={() => { onFocus={() => {
setEditMode(true); setEditMode(true);
setEditText(text); setEditText(text);
const timer = setTimeout(() => {
clearTimeout(timer);
inputRef.current?.focus();
}, 100);
}} }}
onBlur={() => { onBlur={() => {
setEditMode(false); setEditMode(false);
@@ -180,6 +175,7 @@ export default function TranBox({
header={<Header setShowPopup={setShowBox} />} header={<Header setShowPopup={setShowBox} />}
onChangeSize={setBoxSize} onChangeSize={setBoxSize}
onChangePosition={setBoxPosition} onChangePosition={setBoxPosition}
onClick={(e) => e.stopPropagation()}
> >
<Divider /> <Divider />
<TranForm <TranForm

View File

@@ -6,10 +6,11 @@ import Stack from "@mui/material/Stack";
import { useI18n } from "../../hooks/I18n"; import { useI18n } from "../../hooks/I18n";
import { DEFAULT_TRANS_APIS, OPT_TRANS_BAIDU } from "../../config"; import { DEFAULT_TRANS_APIS, OPT_TRANS_BAIDU } from "../../config";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { apiTranslate, apiBaiduLangdetect } from "../../apis"; import { apiTranslate, apiBaiduLangdetect, apiBaiduSuggest } from "../../apis";
import { isValidWord } from "../../libs/utils"; import { isValidWord } from "../../libs/utils";
import CopyBtn from "./CopyBtn"; import CopyBtn from "./CopyBtn";
import DictCont from "./DictCont"; import DictCont from "./DictCont";
import SugCont from "./SugCont";
export default function TranCont({ export default function TranCont({
text, text,
@@ -26,6 +27,7 @@ export default function TranCont({
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(""); const [error, setError] = useState("");
const [dictResult, setDictResult] = useState(null); const [dictResult, setDictResult] = useState(null);
const [sugs, setSugs] = useState([]);
useEffect(() => { useEffect(() => {
(async () => { (async () => {
@@ -34,6 +36,7 @@ export default function TranCont({
setTrText(""); setTrText("");
setError(""); setError("");
setDictResult(null); setDictResult(null);
setSugs([]);
// 互译 // 互译
if (toLang !== toLang2 && toLang2 !== "none") { if (toLang !== toLang2 && toLang2 !== "none") {
@@ -45,6 +48,7 @@ export default function TranCont({
} }
} }
// 翻译
const apiSetting = const apiSetting =
transApis[translator] || DEFAULT_TRANS_APIS[translator]; transApis[translator] || DEFAULT_TRANS_APIS[translator];
const tranRes = await apiTranslate({ const tranRes = await apiTranslate({
@@ -59,7 +63,8 @@ export default function TranCont({
// 词典 // 词典
if (isValidWord(text) && toLang.startsWith("zh")) { if (isValidWord(text) && toLang.startsWith("zh")) {
if (fromLang === "en" && translator === OPT_TRANS_BAIDU) { if (fromLang === "en" && translator === OPT_TRANS_BAIDU) {
setDictResult(tranRes[2].dict_result); tranRes[2].type === 1 &&
setDictResult(JSON.parse(tranRes[2].result));
} else { } else {
const dictRes = await apiTranslate({ const dictRes = await apiTranslate({
text, text,
@@ -67,9 +72,15 @@ export default function TranCont({
fromLang: "en", fromLang: "en",
toLang: "zh-CN", toLang: "zh-CN",
}); });
setDictResult(dictRes[2].dict_result); dictRes[2].type === 1 &&
setDictResult(JSON.parse(dictRes[2].result));
} }
} }
// 建议
if (text.length < 20) {
setSugs(await apiBaiduSuggest(text));
}
} catch (err) { } catch (err) {
setError(err.message); setError(err.message);
} finally { } finally {
@@ -117,6 +128,7 @@ export default function TranCont({
{loading && <CircularProgress size={24} />} {loading && <CircularProgress size={24} />}
{error && <Alert severity="error">{error}</Alert>} {error && <Alert severity="error">{error}</Alert>}
{dictResult && <DictCont dictResult={dictResult} />} {dictResult && <DictCont dictResult={dictResult} />}
{sugs.length > 0 && <SugCont sugs={sugs} />}
</> </>
); );
} }

View File

@@ -6,8 +6,13 @@ import { sleep, limitNumber } from "../../libs/utils";
import { isGm, isExt } from "../../libs/client"; import { isGm, isExt } from "../../libs/client";
import { MSG_OPEN_TRANBOX, DEFAULT_TRANBOX_SHORTCUT } from "../../config"; import { MSG_OPEN_TRANBOX, DEFAULT_TRANBOX_SHORTCUT } from "../../config";
import { isMobile } from "../../libs/mobile"; import { isMobile } from "../../libs/mobile";
import { kissLog } from "../../libs/log";
export default function Slection({ contextMenus, tranboxSetting, transApis }) { export default function Slection({
contextMenuType,
tranboxSetting,
transApis,
}) {
const boxWidth = limitNumber(window.innerWidth, 300, 600); const boxWidth = limitNumber(window.innerWidth, 300, 600);
const boxHeight = limitNumber(window.innerHeight, 200, 400); const boxHeight = limitNumber(window.innerHeight, 200, 400);
@@ -106,10 +111,10 @@ export default function Slection({ contextMenus, tranboxSetting, transApis }) {
// 注册菜单 // 注册菜单
try { try {
const menuCommandIds = []; const menuCommandIds = [];
contextMenus && contextMenuType !== 0 &&
menuCommandIds.push( menuCommandIds.push(
GM.registerMenuCommand( GM.registerMenuCommand(
"Translate Selected Text (Alt+S)", "Translate Selected Text",
(event) => { (event) => {
handleTranbox(); handleTranbox();
}, },
@@ -123,9 +128,21 @@ export default function Slection({ contextMenus, tranboxSetting, transApis }) {
}); });
}; };
} catch (err) { } catch (err) {
console.log("[registerMenuCommand]", err); kissLog(err, "registerMenuCommand");
} }
}, [handleTranbox, contextMenus]); }, [handleTranbox, contextMenuType]);
useEffect(() => {
if (tranboxSetting.hideClickAway) {
const handleHideBox = () => {
setShowBox(false);
};
window.addEventListener("click", handleHideBox);
return () => {
window.removeEventListener("click", handleHideBox);
};
}
}, [tranboxSetting.hideClickAway]);
return ( return (
<> <>