Compare commits
334 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39b3b00117 | ||
|
|
763019f0c5 | ||
|
|
d743271be8 | ||
|
|
992dad26aa | ||
|
|
9bd0e67474 | ||
|
|
5767a4afb2 | ||
|
|
9e4c510684 | ||
|
|
16607fb069 | ||
|
|
e047a06432 | ||
|
|
9e09fd898a | ||
|
|
799c32a871 | ||
|
|
483f33b5c9 | ||
|
|
d444fd4fba | ||
|
|
0aae93ba2e | ||
|
|
9608bea3bf | ||
|
|
a038a1ecdc | ||
|
|
c82cdd7f8f | ||
|
|
0c6d5c3c61 | ||
|
|
900426f359 | ||
|
|
1d760fc93a | ||
|
|
61571e0f61 | ||
|
|
c9eb423c89 | ||
|
|
45b294a121 | ||
|
|
3a3f1fabe1 | ||
|
|
03177a09b3 | ||
|
|
cae391f62b | ||
|
|
2a5e9db079 | ||
|
|
650d6e8b41 | ||
|
|
1daf134b31 | ||
|
|
e1dfa35c6c | ||
|
|
73f80692d3 | ||
|
|
42c7dae495 | ||
|
|
b2a1309caa | ||
|
|
94bf5f9580 | ||
|
|
704ebdc9d7 | ||
|
|
165da4e559 | ||
|
|
d3e3b484bf | ||
|
|
192f8faa5b | ||
|
|
579d5cb0a3 | ||
|
|
07fca5b9af | ||
|
|
866a63ab6c | ||
|
|
97b4935bc4 | ||
|
|
30129abef3 | ||
|
|
24d904b32c | ||
|
|
5f0ce57ead | ||
|
|
51f58d095a | ||
|
|
d22e3838c4 | ||
|
|
adbb421b7b | ||
|
|
eaa47af269 | ||
|
|
a6cb5544f8 | ||
|
|
9e91faa660 | ||
|
|
8636fadc72 | ||
|
|
0621957592 | ||
|
|
8ec06b0c84 | ||
|
|
d47f8d7ee9 | ||
|
|
24f8959525 | ||
|
|
983740578b | ||
|
|
b5f79ed7cd | ||
|
|
bbb0e79d4e | ||
|
|
471dc05897 | ||
|
|
7a772d2459 | ||
|
|
1d92421960 | ||
|
|
aeaaf429d7 | ||
|
|
84432e98ae | ||
|
|
77c6102de7 | ||
|
|
ab5dd82169 | ||
|
|
23e7b69dc5 | ||
|
|
3dc8f393f2 | ||
|
|
8a2144f263 | ||
|
|
c1c59caa10 | ||
|
|
d27ebd90b6 | ||
|
|
467745c1e9 | ||
|
|
537378a038 | ||
|
|
4b5ed30e5b | ||
|
|
52b7f6a225 | ||
|
|
f31675d8a2 | ||
|
|
dd46a8450c | ||
|
|
b0843f7d66 | ||
|
|
daadc0195c | ||
|
|
298dec6957 | ||
|
|
bf39d85dfa | ||
|
|
30a9de25a8 | ||
|
|
af1ecf0bd4 | ||
|
|
fe55a2cd3c | ||
|
|
5a33d4e57e | ||
|
|
7f46a9023c | ||
|
|
dfd943b621 | ||
|
|
7007d0d922 | ||
|
|
601678500d | ||
|
|
9bfb504381 | ||
|
|
8a03b0cf15 | ||
|
|
11ba89de0a | ||
|
|
bac7f62eea | ||
|
|
eef90ea02b | ||
|
|
0650df534a | ||
|
|
9ef8c8b823 | ||
|
|
d7e08da0b2 | ||
|
|
ef361e0798 | ||
|
|
6855332092 | ||
|
|
121d523e02 | ||
|
|
42a375c4c7 | ||
|
|
a1dd705d97 | ||
|
|
71f90b36ca | ||
|
|
37facdc3c1 | ||
|
|
66b4f547ff | ||
|
|
d27b9c7f2d | ||
|
|
278ff9c6bc | ||
|
|
d6fe1ce9d7 | ||
|
|
0bfa5256b8 | ||
|
|
72ccfc8aec | ||
|
|
d117c5dc10 | ||
|
|
9312783f44 | ||
|
|
e5b16ebfd3 | ||
|
|
5d1d65c2d3 | ||
|
|
9ca1309cec | ||
|
|
a03afc05f5 | ||
|
|
0198963584 | ||
|
|
58e745d967 | ||
|
|
377e347d68 | ||
|
|
bac0704d3d | ||
|
|
d2ff46edf6 | ||
|
|
f908372b4e | ||
|
|
5d44ff4913 | ||
|
|
4c9aa66048 | ||
|
|
b6a09b99ab | ||
|
|
3a0dcb1a52 | ||
|
|
5015503b4c | ||
|
|
16423feea4 | ||
|
|
9703514698 | ||
|
|
de7a97fb76 | ||
|
|
319aaf8132 | ||
|
|
74bc58ba91 | ||
|
|
d622db0d7c | ||
|
|
de1ddf2362 | ||
|
|
32c0fc860b | ||
|
|
1938f432dd | ||
|
|
a5cfb0ca1d | ||
|
|
a172234fb0 | ||
|
|
63f989b31a | ||
|
|
2ae5d01d5c | ||
|
|
130f1deed1 | ||
|
|
5880d85b48 | ||
|
|
9455670e80 | ||
|
|
e369321c66 | ||
|
|
efc51b0d46 | ||
|
|
d6f3b23b88 | ||
|
|
0a4fa7b9f8 | ||
|
|
2b3e4a8d25 | ||
|
|
bf3a16f96d | ||
|
|
b416e72820 | ||
|
|
ca84bdb227 | ||
|
|
148a4e97a6 | ||
|
|
a13493ebc2 | ||
|
|
ce4ac79e5f | ||
|
|
8f76ea49e7 | ||
|
|
923d3293cd | ||
|
|
7379ff8d15 | ||
|
|
18ebec350d | ||
|
|
3b0cbc53aa | ||
|
|
f00e8ffa4d | ||
|
|
d6f7aad1c3 | ||
|
|
092ea6e836 | ||
|
|
d565e2464a | ||
|
|
2f5d875c47 | ||
|
|
fdb2ddc5f7 | ||
|
|
7a12c5315a | ||
|
|
60d788288d | ||
|
|
dc3c510d57 | ||
|
|
ec6a49f01e | ||
|
|
2b9bfbc20d | ||
|
|
06a51df834 | ||
|
|
6fa183dc56 | ||
|
|
b3cb4049ed | ||
|
|
602b51b1f5 | ||
|
|
a83039577c | ||
|
|
1c77a289a6 | ||
|
|
6278b9124d | ||
|
|
f94cafdbcc | ||
|
|
e13da2caba | ||
|
|
d833fa8dfd | ||
|
|
c921cc59b9 | ||
|
|
7a2c594324 | ||
|
|
0eeb9c2fbf | ||
|
|
ac921cd5a0 | ||
|
|
3ea14c1687 | ||
|
|
6e927473b9 | ||
|
|
1d9e9c1b7d | ||
|
|
54a6189b0c | ||
|
|
85aa9f39c2 | ||
|
|
bda83ce76e | ||
|
|
9ee4c20250 | ||
|
|
fbc70e43e3 | ||
|
|
bc4b4a2171 | ||
|
|
96f9bf6f6f | ||
|
|
9f0986536a | ||
|
|
f668aa7acd | ||
|
|
fc50f4784a | ||
|
|
1fa58cad31 | ||
|
|
469e62557c | ||
|
|
75830aaea7 | ||
|
|
61ef5df559 | ||
|
|
14b5ba9c4c | ||
|
|
9e9c56a3b4 | ||
|
|
3a79f55614 | ||
|
|
af25ee5c11 | ||
|
|
6dd581d5e2 | ||
|
|
746ec019c4 | ||
|
|
2b70f28b0b | ||
|
|
45127646e8 | ||
|
|
83e9c1dd97 | ||
|
|
2eabb7d5ac | ||
|
|
9d4c596b4b | ||
|
|
cc38ab6c45 | ||
|
|
586fa09c7b | ||
|
|
0c45bc5ea8 | ||
|
|
9d9c0633f0 | ||
|
|
47f9635b10 | ||
|
|
68088f5e17 | ||
|
|
77a37cc6df | ||
|
|
420e59bf81 | ||
|
|
dbc5135301 | ||
|
|
8c7d6bb552 | ||
|
|
2b5c1952c0 | ||
|
|
85a82618e5 | ||
|
|
0280ac34c3 | ||
|
|
439900154b | ||
|
|
4a6e902684 | ||
|
|
71bbd2e54a | ||
|
|
3083d8e147 | ||
|
|
e74883e9c2 | ||
|
|
0816a9d167 | ||
|
|
4b3e91fa84 | ||
|
|
0973a0b60e | ||
|
|
de5f61126d | ||
|
|
0c20ca761f | ||
|
|
4bce56207e | ||
|
|
dca54e0033 | ||
|
|
309646bf1d | ||
|
|
18b9961b39 | ||
|
|
1e51ff17f2 | ||
|
|
63b5f707e2 | ||
|
|
30efb6ee7a | ||
|
|
61b017618a | ||
|
|
1e0397adc9 | ||
|
|
48b34bf95f | ||
|
|
d5fc69e210 | ||
|
|
59f9dd697f | ||
|
|
c9d72323f1 | ||
|
|
e87f7f3abe | ||
|
|
82ebbcb6d6 | ||
|
|
2db11070c5 | ||
|
|
5efd2517e7 | ||
|
|
c0ba654678 | ||
|
|
546a5a549b | ||
|
|
cbf02c34e3 | ||
|
|
74a7258f10 | ||
|
|
1006c044bc | ||
|
|
ef4ea719f3 | ||
|
|
8b34afe69f | ||
|
|
01292af298 | ||
|
|
cff8b2fe39 | ||
|
|
2cb20b5cc0 | ||
|
|
8f2aed18fe | ||
|
|
d85831cc9a | ||
|
|
55dc3a5556 | ||
|
|
591afe08bd | ||
|
|
748f2002ab | ||
|
|
d2d18a2384 | ||
|
|
35f4fa6aa7 | ||
|
|
66fc2d22ed | ||
|
|
16cf9ee1ed | ||
|
|
d9d97bf14c | ||
|
|
dc811bd3c7 | ||
|
|
b939d1849a | ||
|
|
beca31f55d | ||
|
|
c7df103950 | ||
|
|
4bf7972ad5 | ||
|
|
534eaed1ed | ||
|
|
7e014e7385 | ||
|
|
34adb2660b | ||
|
|
b6bc165cf0 | ||
|
|
bdd5ed7fc7 | ||
|
|
95d19417c3 | ||
|
|
30ebebdd71 | ||
|
|
e9c557776d | ||
|
|
535a43b698 | ||
|
|
59752ed4aa | ||
|
|
b3e7b8f3f1 | ||
|
|
c4e9365512 | ||
|
|
7d3972d3a8 | ||
|
|
52ca4306fd | ||
|
|
da368ee612 | ||
|
|
22c50e7765 | ||
|
|
7bc39dd1bc | ||
|
|
c80ead6116 | ||
|
|
67e76e4009 | ||
|
|
b213218a30 | ||
|
|
c629a1252c | ||
|
|
64d2481e93 | ||
|
|
e7d6a6add8 | ||
|
|
edc25f7da4 | ||
|
|
5bff84ace1 | ||
|
|
f8bfcba317 | ||
|
|
013a05201b | ||
|
|
433e811821 | ||
|
|
df4cfc0fbc | ||
|
|
1bfb465fd6 | ||
|
|
d5d5ec3fef | ||
|
|
0a32f94d32 | ||
|
|
8067f34ce6 | ||
|
|
214c189a7c | ||
|
|
1f67afc8d8 | ||
|
|
7d4af27919 | ||
|
|
2d651abfdd | ||
|
|
6e06fe79cd | ||
|
|
6093577591 | ||
|
|
4b23ee733f | ||
|
|
46428b7c7f | ||
|
|
6805340a9a | ||
|
|
df36ca8d8b | ||
|
|
fe13de7c30 | ||
|
|
b00f636b72 | ||
|
|
8d074e63e1 | ||
|
|
37989b0089 | ||
|
|
477361eb40 | ||
|
|
94288b5dc3 | ||
|
|
84de1e0f12 | ||
|
|
06f93c1c10 | ||
|
|
450283b80a | ||
|
|
44aeed03a6 | ||
|
|
fa4569415d | ||
|
|
a341bf30ba | ||
|
|
34a7354c84 | ||
|
|
21b5dfbe98 |
12
.env
12
.env
@@ -2,28 +2,18 @@ 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.10
|
REACT_APP_VERSION=1.9.2
|
||||||
|
|
||||||
REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator
|
REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator
|
||||||
|
|
||||||
REACT_APP_OPTIONSPAGE=https://fishjar.github.io/kiss-translator/options.html
|
REACT_APP_OPTIONSPAGE=https://fishjar.github.io/kiss-translator/options.html
|
||||||
REACT_APP_OPTIONSPAGE2=https://kiss-translator.rayjar.com/options
|
|
||||||
REACT_APP_OPTIONSPAGE_DEV=http://localhost:3000/options.html
|
REACT_APP_OPTIONSPAGE_DEV=http://localhost:3000/options.html
|
||||||
|
|
||||||
REACT_APP_LOGOURL=https://fishjar.github.io/kiss-translator/images/logo192.png
|
REACT_APP_LOGOURL=https://fishjar.github.io/kiss-translator/images/logo192.png
|
||||||
REACT_APP_LOGOURL2=https://kiss-translator.rayjar.com/images/logo192.png
|
|
||||||
|
|
||||||
REACT_APP_RULESURL=https://fishjar.github.io/kiss-rules/kiss-rules.json
|
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_VERSIONFILE2=https://kiss-translator.rayjar.com/version.txt
|
|
||||||
|
|
||||||
REACT_APP_USERSCRIPT_DOWNLOADURL=https://fishjar.github.io/kiss-translator/kiss-translator.user.js
|
REACT_APP_USERSCRIPT_DOWNLOADURL=https://fishjar.github.io/kiss-translator/kiss-translator.user.js
|
||||||
REACT_APP_USERSCRIPT_DOWNLOADURL2=https://kiss-translator.rayjar.com/kiss-translator.user.js
|
|
||||||
|
|
||||||
REACT_APP_USERSCRIPT_IOS_DOWNLOADURL=https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js
|
REACT_APP_USERSCRIPT_IOS_DOWNLOADURL=https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js
|
||||||
REACT_APP_USERSCRIPT_IOS_DOWNLOADURL2=https://kiss-translator.rayjar.com/kiss-translator-ios-safari.user.js
|
|
||||||
|
|||||||
37
.github/workflows/release.yml
vendored
37
.github/workflows/release.yml
vendored
@@ -7,28 +7,28 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: pnpm/action-setup@v2
|
- uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
version: 8.7.6
|
version: latest
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: "18.17.0"
|
node-version: latest
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
- run: pnpm install
|
- run: pnpm install
|
||||||
- run: pnpm build
|
- run: pnpm build+zip
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: build-artifacts
|
name: build-artifacts
|
||||||
path: build
|
path: build
|
||||||
deploy-web:
|
deploy-web:
|
||||||
needs: build
|
needs: build
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/download-artifact@v3
|
- uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: build-artifacts
|
name: build-artifacts
|
||||||
path: build
|
path: build
|
||||||
@@ -37,7 +37,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
folder: build/web
|
folder: build/web
|
||||||
create-release:
|
create-release:
|
||||||
runs-on: ubuntu-22.04
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
upload_url: ${{ steps.create-release.outputs.upload_url }}
|
upload_url: ${{ steps.create-release.outputs.upload_url }}
|
||||||
steps:
|
steps:
|
||||||
@@ -54,18 +55,14 @@ jobs:
|
|||||||
needs: [build, create-release]
|
needs: [build, create-release]
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
client: ["chrome", "edge", "firefox", "userscript"]
|
client: ["chrome", "edge", "firefox", "userscript", "thunderbird"]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/download-artifact@v3
|
- uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: build-artifacts
|
name: build-artifacts
|
||||||
path: build
|
path: build
|
||||||
- name: Zip Release
|
|
||||||
run: |
|
|
||||||
cd build
|
|
||||||
zip -r ${{ matrix.client }}.zip ${{ matrix.client }}
|
|
||||||
- uses: actions/upload-release-asset@v1
|
- uses: actions/upload-release-asset@v1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
4
.prettierignore
Normal file
4
.prettierignore
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
build
|
||||||
|
public
|
||||||
|
package.json
|
||||||
24
.prettierrc
Normal file
24
.prettierrc
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"arrowParens": "always",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"htmlWhitespaceSensitivity": "css",
|
||||||
|
"insertPragma": false,
|
||||||
|
"singleAttributePerLine": false,
|
||||||
|
"bracketSameLine": false,
|
||||||
|
"jsxBracketSameLine": false,
|
||||||
|
"jsxSingleQuote": false,
|
||||||
|
"printWidth": 80,
|
||||||
|
"proseWrap": "preserve",
|
||||||
|
"quoteProps": "as-needed",
|
||||||
|
"requirePragma": false,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": false,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"useTabs": false,
|
||||||
|
"embeddedLanguageFormatting": "auto",
|
||||||
|
"vueIndentScriptAndStyle": false,
|
||||||
|
"experimentalTernaries": false,
|
||||||
|
"parser": "babel"
|
||||||
|
}
|
||||||
145
README.en.md
145
README.en.md
@@ -1,6 +1,8 @@
|
|||||||
# KISS Translator
|
# KISS Translator
|
||||||
|
|
||||||
A simple [bilingual translation extension & Greasemonkey script](https://github.com/fishjar/kiss-translator).
|
English | [简体中文](README.md)
|
||||||
|
|
||||||
|
A simple, open source [bilingual translation extension & Greasemonkey script](https://github.com/fishjar/kiss-translator).
|
||||||
|
|
||||||
[kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f)
|
[kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f)
|
||||||
|
|
||||||
@@ -9,15 +11,24 @@ A simple [bilingual translation extension & Greasemonkey script](https://github.
|
|||||||
- [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
|
||||||
|
- [x] Firefox
|
||||||
|
- [x] Kiwi (Android)
|
||||||
|
- [x] Orion (iOS)
|
||||||
- [ ] Safari
|
- [ ] Safari
|
||||||
|
- [x] Safari (Mac)
|
||||||
|
- [x] Thunderbird
|
||||||
- [x] Supports multiple translation services
|
- [x] Supports multiple translation services
|
||||||
- [x] Google/Microsoft/DeepL/OpenAI/CloudflareAI/Baidu/Tencent
|
- [x] Google/Microsoft
|
||||||
|
- [x] Baidu/Tencent/Volcengine
|
||||||
|
- [x] OpenAI/Gemini/Claude/Ollama/DeepSeek/CloudflareAI
|
||||||
|
- [x] DeepL/DeepLX/NiuTrans
|
||||||
- [x] Custom translation interface
|
- [x] Custom translation interface
|
||||||
- [x] Covers common translation scenarios
|
- [x] Covers common translation scenarios
|
||||||
- [x] Web bilingual translation
|
- [x] Web bilingual translation
|
||||||
- [x] Input box translation
|
- [x] Input box translation
|
||||||
- [x] Seletction translation
|
- [x] Seletction translation
|
||||||
|
- [x] Favorite Words
|
||||||
- [x] Mouseover translation
|
- [x] Mouseover translation
|
||||||
- [x] YouTube subtitle translation
|
- [x] YouTube subtitle translation
|
||||||
- [x] Cross-client data synchronization
|
- [x] Cross-client data synchronization
|
||||||
@@ -25,12 +36,13 @@ A simple [bilingual translation extension & Greasemonkey script](https://github.
|
|||||||
- [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
|
||||||
- `Alt+C` Toggle Styles
|
- `Alt+C` Toggle Styles
|
||||||
- `Alt+K` Open Setting Popup
|
- `Alt+K` Open Setting Popup
|
||||||
- `Alt+B` Open Translate Popup
|
- `Alt+S` Open Translate Popup / Translate Selected Text
|
||||||
- `Alt+O` Open Options Page
|
- `Alt+O` Open Options Page
|
||||||
- `Alt+I` Input Box Translation
|
- `Alt+I` Input Box Translation
|
||||||
|
|
||||||
@@ -38,18 +50,22 @@ A simple [bilingual translation extension & Greasemonkey script](https://github.
|
|||||||
|
|
||||||
> Note: For the following reasons, it is recommended to use browser extensions first
|
> Note: For the following reasons, it is recommended to use browser extensions first
|
||||||
>
|
>
|
||||||
> - Browser extension can use local language recognition
|
> - Browser extensions have more complete functions (local language recognition, context menu, etc.)
|
||||||
> - Grease Monkey script will encounter more usage problems
|
> - Grease Monkey script will encounter more usage problems (cross domain issues, script conflicts, etc.)
|
||||||
|
|
||||||
- [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] Safari (Mac) Compiled by a third party, not verified, obtained by yourself: https://www.nodeloc.com/t/topic/54245
|
||||||
|
- [x] Thunderbird [Download address](https://github.com/fishjar/kiss-translator/releases)
|
||||||
- [x] GreaseMonkey Script
|
- [x] GreaseMonkey Script
|
||||||
- [x] Chrome/Edge/Firefox ([Tampermonkey](https://www.tampermonkey.net/)/[Violentmonkey](https://violentmonkey.github.io/)) [Installation link 1](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)、 [Installation link 2](https://kiss-translator.rayjar.com/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 1](https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js)、 [Installation link 2](https://kiss-translator.rayjar.com/kiss-translator.user-ios-safari.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
|
||||||
|
|
||||||
@@ -60,22 +76,115 @@ A simple [bilingual translation extension & Greasemonkey script](https://github.
|
|||||||
- 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.
|
||||||
- Minimalistic Dictionary Plugin: [https://github.com/fishjar/kiss-dictionary](https://github.com/fishjar/kiss-dictionary)
|
|
||||||
- A word-marking translation plug-in used with this project.
|
## Frequently Asked Questions
|
||||||
- Supports query of English words, sentences and Chinese characters.
|
|
||||||
- Supports history records and word collections.
|
### How to Turn Off Automatic Translation
|
||||||
|
|
||||||
|
You can achieve this through `Rules Setting` with the following methods:
|
||||||
|
|
||||||
|
- Personal Rules: RULES-> Global Rule -> Translate Switch -> Disaabled
|
||||||
|
- Subscription Rules: SUBSCRIBE -> Select the third option `kiss-rules-off.json`
|
||||||
|
- Override Subscription Rules: OVERWRITE -> Translate Switch -> Disaabled
|
||||||
|
- Add a Personal Rule for a Specific Website: Translate Switch -> Disaabled
|
||||||
|
|
||||||
|
### How to Set Keyboard Shortcuts
|
||||||
|
|
||||||
|
Set this in the extension management page, for example:
|
||||||
|
|
||||||
|
- chrome [chrome://extensions/shortcuts](chrome://extensions/shortcuts)
|
||||||
|
- firefox [about:addons](about:addons)
|
||||||
|
|
||||||
|
### How to Turn Off Selection Translation
|
||||||
|
|
||||||
|
Set this in the `Rules Setting`: RULES -> Global Rule -> If translate selected -> Disable
|
||||||
|
|
||||||
|
### How to Set it to Show Only the Translation
|
||||||
|
|
||||||
|
Set this in the `Rules Setting`: RULES -> Global Rule -> Show Only Translations -> Enable
|
||||||
|
|
||||||
|
### How to Set Mouse Hover Translation
|
||||||
|
|
||||||
|
Set this in the `Rules Setting`: RULES -> Global Rule -> TTrigger Mode
|
||||||
|
|
||||||
|
### Why are some web pages not fully translated?
|
||||||
|
|
||||||
|
This extension's webpage translation is based on CSS selectors. Generic rules cannot adapt to all websites, and sometimes you need to manually add site-specific rules. If you don't know how to write rules, you can seek help here:
|
||||||
|
https://github.com/fishjar/kiss-rules/issues
|
||||||
|
|
||||||
|
### What is the priority order of rule settings?
|
||||||
|
|
||||||
|
Personal Rules > Override Subscription Rules > Subscription Rules > Global Rules
|
||||||
|
|
||||||
|
Among these, Global Rules have the lowest priority but are very important as they serve as the default rules.
|
||||||
|
|
||||||
|
### Why are YouTube subtitles translated in broken sentences?
|
||||||
|
|
||||||
|
This extension has no special development for video content. Support for YouTube is also treated as regular webpage translation. Auto-generated subtitles are streamed and output progressively, resulting in poorer support.
|
||||||
|
|
||||||
|
To disable this extension's subtitle translation, add a rule. Reference: https://github.com/fishjar/kiss-translator/issues/62
|
||||||
|
|
||||||
|
### Local Ollama interface cannot be used
|
||||||
|
|
||||||
|
If encountering a 403 error, refer to: https://github.com/fishjar/kiss-translator/issues/174
|
||||||
|
|
||||||
|
### Custom API doesn't work in Tampermonkey scripts
|
||||||
|
|
||||||
|
Tampermonkey scripts require adding domains to the whitelist; otherwise, requests cannot be sent.
|
||||||
|
|
||||||
|
### How to Set Up Hook Functions for Custom Interfaces
|
||||||
|
|
||||||
|
The custom interface feature is highly flexible and can theoretically integrate with any translation interface.
|
||||||
|
|
||||||
|
Example of a Request Hook function:
|
||||||
|
|
||||||
|
```js
|
||||||
|
/**
|
||||||
|
* Request Hook
|
||||||
|
* @param {string} text Text to be translated
|
||||||
|
* @param {string} from Source language
|
||||||
|
* @param {string} to Target language
|
||||||
|
* @param {string} url Translation interface URL
|
||||||
|
* @param {string} key Translation interface API key
|
||||||
|
* @returns {Array[string, object]} [Interface URL, request object]
|
||||||
|
*/
|
||||||
|
(text, from, to, url, key) => [url, {
|
||||||
|
headers: {
|
||||||
|
"Content-type": "application/json",
|
||||||
|
"Authorization": `Bearer ${key}`
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
body: { text, to },
|
||||||
|
}]
|
||||||
|
```
|
||||||
|
|
||||||
|
Example of a Response Hook function:
|
||||||
|
|
||||||
|
```js
|
||||||
|
* Response Hook
|
||||||
|
* @param {string} res JSON data returned by the interface
|
||||||
|
* @param {string} text Text to be translated
|
||||||
|
* @param {string} from Source language
|
||||||
|
* @param {string} to Target language
|
||||||
|
* @returns {Array[string, boolean]} [Translated text, whether target language is same as source]
|
||||||
|
* Note: If the second return value is true (target language same as source),
|
||||||
|
* the translation will not be displayed on the page,
|
||||||
|
* If the parameters are incomplete, it is recommended to return false directly
|
||||||
|
*/
|
||||||
|
(res, text, from, to) => [res.text, to === res.src]
|
||||||
|
```
|
||||||
|
|
||||||
|
For more custom interface examples, refer to: [custom-api.md](https://github.com/fishjar/kiss-translator/blob/master/custom-api.md)
|
||||||
|
|
||||||
## Development Guidelines
|
## Development Guidelines
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/fishjar/kiss-translator.git
|
git clone https://github.com/fishjar/kiss-translator.git
|
||||||
cd kiss-translator
|
cd kiss-translator
|
||||||
|
git checkout dev # Submit a PR suggestion to push to the dev branch
|
||||||
pnpm install
|
pnpm install
|
||||||
pnpm build
|
pnpm build
|
||||||
```
|
```
|
||||||
@@ -83,3 +192,7 @@ pnpm build
|
|||||||
## Discussion
|
## Discussion
|
||||||
|
|
||||||
- Join [Telegram Group](https://t.me/+RRCu_4oNwrM2NmFl)
|
- Join [Telegram Group](https://t.me/+RRCu_4oNwrM2NmFl)
|
||||||
|
|
||||||
|
## Appreciate
|
||||||
|
|
||||||
|

|
||||||
|
|||||||
146
README.md
146
README.md
@@ -1,6 +1,8 @@
|
|||||||
# 简约翻译
|
# 简约翻译
|
||||||
|
|
||||||
一个简约的 [双语对照翻译扩展 & 油猴脚本](https://github.com/fishjar/kiss-translator)。
|
[English](README.en.md) | 简体中文
|
||||||
|
|
||||||
|
一个简约、开源的 [双语对照翻译扩展 & 油猴脚本](https://github.com/fishjar/kiss-translator)。
|
||||||
|
|
||||||
[kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f)
|
[kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f)
|
||||||
|
|
||||||
@@ -9,15 +11,24 @@
|
|||||||
- [x] 保持简约
|
- [x] 保持简约
|
||||||
- [x] 开放源代码
|
- [x] 开放源代码
|
||||||
- [x] 适配常见浏览器
|
- [x] 适配常见浏览器
|
||||||
- [x] Chrome/Edge/Firefox/Kiwi
|
- [x] Chrome/Edge
|
||||||
|
- [x] Firefox
|
||||||
|
- [x] Kiwi (Android)
|
||||||
|
- [x] Orion (iOS)
|
||||||
- [ ] Safari
|
- [ ] Safari
|
||||||
|
- [x] Safari (Mac)
|
||||||
|
- [x] Thunderbird
|
||||||
- [x] 支持多种翻译服务
|
- [x] 支持多种翻译服务
|
||||||
- [x] Google/Microsoft/DeepL/OpenAI/CloudflareAI/Baidu/Tencent
|
- [x] Google/Microsoft
|
||||||
|
- [x] Baidu/Tencent/Volcengine
|
||||||
|
- [x] OpenAI/Gemini/Claude/Ollama/DeepSeek/CloudflareAI
|
||||||
|
- [x] DeepL/DeepLX/NiuTrans
|
||||||
- [x] 自定义翻译接口
|
- [x] 自定义翻译接口
|
||||||
- [x] 覆盖常见翻译场景
|
- [x] 覆盖常见翻译场景
|
||||||
- [x] 网页双语对照翻译
|
- [x] 网页双语对照翻译
|
||||||
- [x] 输入框翻译
|
- [x] 输入框翻译
|
||||||
- [x] 划词翻译
|
- [x] 划词翻译
|
||||||
|
- [x] 收藏词汇
|
||||||
- [x] 鼠标悬停翻译
|
- [x] 鼠标悬停翻译
|
||||||
- [x] YouTube 字幕翻译
|
- [x] YouTube 字幕翻译
|
||||||
- [x] 跨客户端数据同步
|
- [x] 跨客户端数据同步
|
||||||
@@ -25,12 +36,13 @@
|
|||||||
- [x] WebDAV
|
- [x] WebDAV
|
||||||
- [x] 自定义翻译规则
|
- [x] 自定义翻译规则
|
||||||
- [x] 规则订阅/规则分享
|
- [x] 规则订阅/规则分享
|
||||||
|
- [x] 自定义专业术语
|
||||||
- [x] 自定义译文样式
|
- [x] 自定义译文样式
|
||||||
- [x] 自定义快捷键
|
- [x] 自定义快捷键
|
||||||
- `Alt+Q` 开启翻译
|
- `Alt+Q` 开启翻译
|
||||||
- `Alt+C` 切换样式
|
- `Alt+C` 切换样式
|
||||||
- `Alt+K` 打开设置弹窗
|
- `Alt+K` 打开设置弹窗
|
||||||
- `Alt+B` 打开翻译弹窗
|
- `Alt+S` 打开翻译弹窗/翻译选中文字
|
||||||
- `Alt+O` 打开设置页面
|
- `Alt+O` 打开设置页面
|
||||||
- `Alt+I` 输入框翻译
|
- `Alt+I` 输入框翻译
|
||||||
|
|
||||||
@@ -38,18 +50,22 @@
|
|||||||
|
|
||||||
> 注:基于以下原因,建议优先使用浏览器扩展
|
> 注:基于以下原因,建议优先使用浏览器扩展
|
||||||
>
|
>
|
||||||
> - 浏览器扩展可以使用本地的语言识别
|
> - 浏览器扩展的功能更完整(本地语言识别、右键菜单等)
|
||||||
> - 油猴脚本会遇到更多使用上的问题
|
> - 油猴脚本会遇到更多使用上的问题(跨域问题、脚本冲突等)
|
||||||
|
|
||||||
- [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] Safari (Mac) 第三方编译,未作验证,自行获取: https://www.nodeloc.com/t/topic/54245
|
||||||
|
- [x] Thunderbird [下载地址](https://github.com/fishjar/kiss-translator/releases)
|
||||||
- [x] 油猴脚本
|
- [x] 油猴脚本
|
||||||
- [x] Chrome/Edge/Firefox ([Tampermonkey](https://www.tampermonkey.net/)/[Violentmonkey](https://violentmonkey.github.io/)) [安装链接 1](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)、 [安装链接 2](https://kiss-translator.rayjar.com/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)) [安装链接 1](https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js)、 [安装链接 2](https://kiss-translator.rayjar.com/kiss-translator.user-ios-safari.js)
|
- [x] iOS Safari ([Userscripts Safari](https://github.com/quoid/userscripts)) [安装链接](https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js)
|
||||||
|
|
||||||
## 关联项目
|
## 关联项目
|
||||||
|
|
||||||
@@ -60,22 +76,114 @@
|
|||||||
- 社区订阅规则: [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)
|
||||||
- 如果访问某个翻译接口遇到网络问题,这个代理服务也许可以帮你到你。
|
- 如果访问某个翻译接口遇到网络问题,这个代理服务也许可以帮到你。
|
||||||
- 自己部署,自己管理。
|
- 自己部署,自己管理。
|
||||||
- 简约词典插件: [https://github.com/fishjar/kiss-dictionary](https://github.com/fishjar/kiss-dictionary)
|
|
||||||
- 搭配本项目一起使用的划词翻译插件。
|
## 常见问题
|
||||||
- 支持英文单词、句子、汉字的查询。
|
|
||||||
- 支持历史记录、单词收藏。
|
### 如何关闭自动翻译
|
||||||
|
|
||||||
|
通过规则设置,以下方法均可实现:
|
||||||
|
|
||||||
|
- 个人规则:全局规则 -> 开启翻译 -> 默认关闭
|
||||||
|
- 订阅规则:选择第三个 `kiss-rules-off.json`
|
||||||
|
- 覆写订阅规则:开启翻译 -> 默认关闭
|
||||||
|
- 添加一条针对某个网站的个人规则:开启翻译 -> 默认关闭
|
||||||
|
|
||||||
|
### 如何设置快捷键
|
||||||
|
|
||||||
|
在插件管理那里设置,例如:
|
||||||
|
|
||||||
|
- chrome [chrome://extensions/shortcuts](chrome://extensions/shortcuts)
|
||||||
|
- firefox [about:addons](about:addons)
|
||||||
|
|
||||||
|
### 如何关闭划词翻译
|
||||||
|
|
||||||
|
通过规则设置:个人规则 -> 全局规则 -> 是否启用划词翻译 -> 禁用
|
||||||
|
|
||||||
|
### 如何设置仅显示译文
|
||||||
|
|
||||||
|
通过规则设置:个人规则 -> 全局规则 -> 仅显示译文 -> 启用
|
||||||
|
|
||||||
|
### 如何设置鼠标悬停翻译
|
||||||
|
|
||||||
|
通过规则设置:个人规则 -> 全局规则 -> 触发方式
|
||||||
|
|
||||||
|
### 为什么有些网页翻译不全
|
||||||
|
|
||||||
|
本插件的网页翻译是基于CSS选择器的,通用规则不能适配所有网页,有时需要自行添加相应网站的单独规则。如果不会写规则,可以到这里求助: https://github.com/fishjar/kiss-rules/issues
|
||||||
|
|
||||||
|
### 规则设置的优先级是如何的
|
||||||
|
|
||||||
|
个人规则 > 覆写订阅规则 > 订阅规则 > 全局规则
|
||||||
|
|
||||||
|
其中全局规则优先级最低,但非常重要,相当于默认规则。
|
||||||
|
|
||||||
|
### 为什么油管字幕一句话会断开翻译
|
||||||
|
|
||||||
|
本插件目前没有针对视频做特殊开发,对油管的支持也是当做网页翻译看待,自动生成字幕是流式生成并输出的,所以支持较差。
|
||||||
|
|
||||||
|
如果需要关闭本插件的字幕翻译,增加一条规则即可,参考:https://github.com/fishjar/kiss-translator/issues/62
|
||||||
|
|
||||||
|
### 本地的Ollama接口不能使用
|
||||||
|
|
||||||
|
如果出现403的情况,参考:https://github.com/fishjar/kiss-translator/issues/174
|
||||||
|
|
||||||
|
### 填写的接口在油猴脚本不能使用
|
||||||
|
|
||||||
|
油猴脚本需要增加域名白名单,否则不能发出请求。
|
||||||
|
|
||||||
|
### 如何设置自定义接口的hook函数
|
||||||
|
|
||||||
|
自定义接口功能非常灵活,理论可以接入任何翻译接口。
|
||||||
|
|
||||||
|
Request Hook 函数示例如下:
|
||||||
|
|
||||||
|
```js
|
||||||
|
/**
|
||||||
|
* Request Hook
|
||||||
|
* @param {string} text 需要翻译的原文
|
||||||
|
* @param {string} from 原文语言
|
||||||
|
* @param {string} to 译文语言
|
||||||
|
* @param {string} url 翻译接口地址
|
||||||
|
* @param {string} key 翻译接口密钥
|
||||||
|
* @returns {Array[string, object]} [接口地址, 请求参数对象]
|
||||||
|
*/
|
||||||
|
(text, from, to, url, key) => [url, {
|
||||||
|
headers: {
|
||||||
|
"Content-type": "application/json",
|
||||||
|
"Authorization": `Bearer ${key}`
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
body: { text, to },
|
||||||
|
}]
|
||||||
|
```
|
||||||
|
|
||||||
|
Response Hook 函数示例如下:
|
||||||
|
|
||||||
|
```js
|
||||||
|
/**
|
||||||
|
* Request Hook
|
||||||
|
* @param {string} res 接口返回的json数据
|
||||||
|
* @param {string} text 需要翻译的原文
|
||||||
|
* @param {string} from 原文语言
|
||||||
|
* @param {string} to 译文语言
|
||||||
|
* @returns {Array[string, boolean]} [译文, 译文语言与原文语言是否相同]
|
||||||
|
* 注:如果返回值第二个值为true(译文语言与原文语言相同)则译文不会在页面显示,
|
||||||
|
* 参数不全的情况建议直接返回false
|
||||||
|
*/
|
||||||
|
(res, text, from, to) => [res.text, to === res.src]
|
||||||
|
```
|
||||||
|
|
||||||
|
更多的自定义接口示例,请参考: [custom-api.md](https://github.com/fishjar/kiss-translator/blob/master/custom-api.md)
|
||||||
|
|
||||||
## 开发指引
|
## 开发指引
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/fishjar/kiss-translator.git
|
git clone https://github.com/fishjar/kiss-translator.git
|
||||||
cd kiss-translator
|
cd kiss-translator
|
||||||
|
git checkout dev # 提交PR建议推送到dev分支
|
||||||
pnpm install
|
pnpm install
|
||||||
pnpm build
|
pnpm build
|
||||||
```
|
```
|
||||||
@@ -83,3 +191,7 @@ pnpm build
|
|||||||
## 交流
|
## 交流
|
||||||
|
|
||||||
- 加入 [Telegram 群](https://t.me/+RRCu_4oNwrM2NmFl)
|
- 加入 [Telegram 群](https://t.me/+RRCu_4oNwrM2NmFl)
|
||||||
|
|
||||||
|
## 赞赏
|
||||||
|
|
||||||
|

|
||||||
|
|||||||
@@ -85,18 +85,21 @@ const userscriptWebpack = (config, env) => {
|
|||||||
// @updateURL ${process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}
|
// @updateURL ${process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}
|
||||||
// @grant GM.xmlHttpRequest
|
// @grant GM.xmlHttpRequest
|
||||||
// @grant GM.registerMenuCommand
|
// @grant GM.registerMenuCommand
|
||||||
|
// @grant GM.unregisterMenuCommand
|
||||||
// @grant GM.setValue
|
// @grant GM.setValue
|
||||||
// @grant GM.getValue
|
// @grant GM.getValue
|
||||||
// @grant GM.deleteValue
|
// @grant GM.deleteValue
|
||||||
// @grant GM.info
|
// @grant GM.info
|
||||||
// @grant unsafeWindow
|
// @grant unsafeWindow
|
||||||
// @connect translate.googleapis.com
|
// @connect translate.googleapis.com
|
||||||
|
// @connect translate-pa.googleapis.com
|
||||||
// @connect api-edge.cognitive.microsofttranslator.com
|
// @connect api-edge.cognitive.microsofttranslator.com
|
||||||
// @connect edge.microsoft.com
|
// @connect edge.microsoft.com
|
||||||
// @connect api-free.deepl.com
|
// @connect api-free.deepl.com
|
||||||
// @connect api.deepl.com
|
// @connect api.deepl.com
|
||||||
// @connect www2.deepl.com
|
// @connect www2.deepl.com
|
||||||
// @connect api.openai.com
|
// @connect api.openai.com
|
||||||
|
// @connect generativelanguage.googleapis.com
|
||||||
// @connect openai.azure.com
|
// @connect openai.azure.com
|
||||||
// @connect workers.dev
|
// @connect workers.dev
|
||||||
// @connect github.io
|
// @connect github.io
|
||||||
@@ -106,10 +109,10 @@ const userscriptWebpack = (config, env) => {
|
|||||||
// @connect dav.jianguoyun.com
|
// @connect dav.jianguoyun.com
|
||||||
// @connect fanyi.baidu.com
|
// @connect fanyi.baidu.com
|
||||||
// @connect transmart.qq.com
|
// @connect transmart.qq.com
|
||||||
// @connect localhost:3000
|
// @connect niutrans.com
|
||||||
// @connect 127.0.0.1:3000
|
// @connect translate.volcengine.com
|
||||||
// @connect localhost:1188
|
// @connect localhost
|
||||||
// @connect 127.0.0.1:1188
|
// @connect 127.0.0.1
|
||||||
// @run-at document-end
|
// @run-at document-end
|
||||||
// ==/UserScript==
|
// ==/UserScript==
|
||||||
|
|
||||||
|
|||||||
346
custom-api.md
Normal file
346
custom-api.md
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
# 自定义接口示例
|
||||||
|
|
||||||
|
以下示例为网友提供,仅供学习参考。
|
||||||
|
|
||||||
|
## 本地运行 Seed-X-PPO-7B 量化模型
|
||||||
|
|
||||||
|
> 由网友 emptyghost6 提供,来源:https://linux.do/t/topic/828257
|
||||||
|
|
||||||
|
URL
|
||||||
|
|
||||||
|
```sh
|
||||||
|
http://localhost:8000/v1/completions
|
||||||
|
```
|
||||||
|
|
||||||
|
Request Hook
|
||||||
|
|
||||||
|
```js
|
||||||
|
(text, from, to, url, key) => {
|
||||||
|
// 模型支持的语言代码到完整名称的映射
|
||||||
|
const langFullNameMap = {
|
||||||
|
ar: 'Arabic', fr: 'French', ms: 'Malay', ru: 'Russian',
|
||||||
|
cs: 'Czech', hr: 'Croatian', nb: 'Norwegian Bokmal', sv: 'Swedish',
|
||||||
|
da: 'Danish', hu: 'Hungarian', nl: 'Dutch', th: 'Thai',
|
||||||
|
de: 'German', id: 'Indonesian', no: 'Norwegian', tr: 'Turkish',
|
||||||
|
en: 'English', it: 'Italian', pl: 'Polish', uk: 'Ukrainian',
|
||||||
|
es: 'Spanish', ja: 'Japanese', pt: 'Portuguese', vi: 'Vietnamese',
|
||||||
|
fi: 'Finnish', ko: 'Korean', ro: 'Romanian', zh: 'Chinese'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 将 Hook 系统的语言代码转换为模型 API 支持的代码
|
||||||
|
const getModelLangCode = (lang) => {
|
||||||
|
if (lang === 'zh-CN' || lang === 'zh-TW') return 'zh';
|
||||||
|
return lang;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sourceLangCode = getModelLangCode(from);
|
||||||
|
const targetLangCode = getModelLangCode(to);
|
||||||
|
|
||||||
|
const sourceLangName = langFullNameMap[sourceLangCode] || from;
|
||||||
|
const targetLangName = langFullNameMap[targetLangCode] || to;
|
||||||
|
|
||||||
|
const prompt = `Translate it to ${targetLangName}:\n${text} <${targetLangCode}>`;
|
||||||
|
|
||||||
|
// 构建请求体对象
|
||||||
|
const bodyObject = {
|
||||||
|
model: "./ByteDance-Seed/Seed-X-PPO-7B-AWQ-Int4",
|
||||||
|
prompt: prompt,
|
||||||
|
max_tokens: 2048,
|
||||||
|
temperature: 0.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 返回最终的请求配置
|
||||||
|
return [url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
// 关键改动:将 JavaScript 对象转换为 JSON 字符串
|
||||||
|
body: JSON.stringify(bodyObject),
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response Hook
|
||||||
|
|
||||||
|
```js
|
||||||
|
(res, text, from, to) => {
|
||||||
|
// 检查返回是否有效
|
||||||
|
if (res && res.choices && res.choices.length > 0 && res.choices[0].text) {
|
||||||
|
|
||||||
|
// 提取译文并去除可能存在的前后空格
|
||||||
|
const translatedText = res.choices[0].text.trim();
|
||||||
|
|
||||||
|
// 比较原文与译文,相同为 true,否则为 false。
|
||||||
|
const areTextsIdentical = text.trim() === translatedText;
|
||||||
|
|
||||||
|
// 返回数组:[翻译后的文本, 是否与原文相同]
|
||||||
|
return [translatedText, areTextsIdentical];
|
||||||
|
}
|
||||||
|
// 如果响应格式不正确或没有结果,则抛出错误
|
||||||
|
throw new Error("Invalid API response format or no translation found.");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 接入 openrouter
|
||||||
|
|
||||||
|
> 由网友 Rick Sanchez 提供
|
||||||
|
|
||||||
|
URL
|
||||||
|
|
||||||
|
```sh
|
||||||
|
https://openrouter.ai/api/v1/chat/completions
|
||||||
|
```
|
||||||
|
|
||||||
|
Request Hook
|
||||||
|
|
||||||
|
```js
|
||||||
|
(text, from, to, url, key) => [url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Bearer ${key}`,
|
||||||
|
"Content-type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
"model": "deepseek/deepseek-chat-v3-0324:free", //可自定义你的模型
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": //可自定义你的提示词
|
||||||
|
`You are a professional ${to} native translator. Your task is to produce a fluent, natural, and culturally appropriate translation of the following text from ${from} to ${to}, fully conveying the meaning, tone, and nuance of the original.
|
||||||
|
|
||||||
|
## Translation Rules
|
||||||
|
1. Output only the final polished translation — no explanations, intermediate drafts, or notes.
|
||||||
|
2. Translate in a way that reads naturally to a native ${to} audience, adapting idioms, cultural references, and tone when necessary.
|
||||||
|
3. Preserve proper nouns, technical terms, brand names, and URLs exactly as in the original text unless a widely accepted ${to} equivalent exists.
|
||||||
|
4. Keep any formatting (Markdown, HTML tags, bullet points, numbering) intact and positioned naturally within the translation.
|
||||||
|
5. Adapt humor, metaphors, and figurative language to culturally relevant forms in ${to} while keeping the original intent.
|
||||||
|
6. Maintain the same level of formality or informality as the original.
|
||||||
|
|
||||||
|
Source Text: ${text}
|
||||||
|
|
||||||
|
Translated Text:`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}]
|
||||||
|
```
|
||||||
|
|
||||||
|
Response Hook
|
||||||
|
|
||||||
|
```js
|
||||||
|
(res, text, from, to) => [
|
||||||
|
res.choices?.[0]?.message?.content ?? "",
|
||||||
|
false
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 接入 gemini-2.5-flash, 关闭思考模式, 去审查
|
||||||
|
|
||||||
|
> 由网友 Rick Sanchez 提供
|
||||||
|
|
||||||
|
URL
|
||||||
|
|
||||||
|
```sh
|
||||||
|
https://generativelanguage.googleapis.com/v1beta/models
|
||||||
|
```
|
||||||
|
|
||||||
|
Request Hook
|
||||||
|
|
||||||
|
```js
|
||||||
|
(text, from, to, url, key) => [`${url}/gemini-2.5-flash:generateContent?key=${key}`, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
"generationConfig": {
|
||||||
|
"temperature": 0.8,
|
||||||
|
"thinkingConfig": {
|
||||||
|
"thinkingBudget": 0, //gemini-2.5-flash设为0关闭思考模式
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"safetySettings": [
|
||||||
|
{
|
||||||
|
"category": "HARM_CATEGORY_HARASSMENT",
|
||||||
|
"threshold": "BLOCK_NONE",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "HARM_CATEGORY_HATE_SPEECH",
|
||||||
|
"threshold": "BLOCK_NONE",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
||||||
|
"threshold": "BLOCK_NONE",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
|
||||||
|
"threshold": "BLOCK_NONE",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"contents": [{
|
||||||
|
"parts": [{
|
||||||
|
"text": `自定义提示词`
|
||||||
|
}]
|
||||||
|
}],
|
||||||
|
}),
|
||||||
|
}]
|
||||||
|
```
|
||||||
|
|
||||||
|
Response Hook
|
||||||
|
|
||||||
|
```js
|
||||||
|
(res, text, from, to) => [
|
||||||
|
res.candidates?.[0]?.content?.parts?.[0]?.text ?? "",
|
||||||
|
false
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 接入 Qwen-MT
|
||||||
|
|
||||||
|
> 由网友 atom 提供
|
||||||
|
|
||||||
|
URL
|
||||||
|
|
||||||
|
```sh
|
||||||
|
https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions
|
||||||
|
```
|
||||||
|
|
||||||
|
Request Hook
|
||||||
|
|
||||||
|
```js
|
||||||
|
(text, from, to, url, key) => {
|
||||||
|
const mapLanguageCode = (lang) => ({
|
||||||
|
'zh-CN': 'zh',
|
||||||
|
'zh-TW': 'zh_tw',
|
||||||
|
})[lang] || lang;
|
||||||
|
|
||||||
|
const targetLang = mapLanguageCode(to);
|
||||||
|
|
||||||
|
return [
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": `Bearer ${key}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
"model": "qwen-mt-turbo",
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": text
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"translation_options": {
|
||||||
|
"source_lang": "auto",
|
||||||
|
"target_lang": targetLang
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response Hook
|
||||||
|
|
||||||
|
```js
|
||||||
|
(res, text, from, to) => [res.choices?.[0]?.message?.content ?? "", false]
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 接入 deepl 接口
|
||||||
|
|
||||||
|
> 来源: https://github.com/fishjar/kiss-translator/issues/101#issuecomment-2123786236
|
||||||
|
|
||||||
|
Request Hook
|
||||||
|
|
||||||
|
```js
|
||||||
|
(text, from, to, url, key) => [
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-type": "application/json",
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
text,
|
||||||
|
target_lang: "ZH",
|
||||||
|
source_lang: "auto",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Response Hook
|
||||||
|
|
||||||
|
```js
|
||||||
|
(res, text, from, to) => [res.data, "ZH" === res.source_lang]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 接入智谱AI大模型
|
||||||
|
|
||||||
|
> 来源: https://github.com/fishjar/kiss-translator/issues/205#issuecomment-2642422679
|
||||||
|
|
||||||
|
Request Hook
|
||||||
|
|
||||||
|
```js
|
||||||
|
(text, from, to, url, key) => [url, {
|
||||||
|
"method": "POST",
|
||||||
|
"headers": {
|
||||||
|
"Content-type": "application/json",
|
||||||
|
"Authorization": key
|
||||||
|
},
|
||||||
|
"body": JSON.stringify({
|
||||||
|
"model": "glm-4-flash",
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"role":"system",
|
||||||
|
"content": "You are a professional, authentic machine translation engine. You only return the translated text, without any explanations."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": `Translate the following text into ${to}. If translation is unnecessary (e.g. proper nouns, codes, etc.), return the original text. NO explanations. NO notes:\n\n ${text} `
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 接入谷歌新接口
|
||||||
|
|
||||||
|
> 由网友 Bush2021 提供,来源:https://github.com/fishjar/kiss-translator/issues/225#issuecomment-2810950717
|
||||||
|
|
||||||
|
URL
|
||||||
|
|
||||||
|
```sh
|
||||||
|
https://translate-pa.googleapis.com/v1/translateHtml
|
||||||
|
```
|
||||||
|
|
||||||
|
KEY
|
||||||
|
|
||||||
|
```sh
|
||||||
|
AIzaSyATBXajvzQLTDHEQbcpq0Ihe0vWDHmO520
|
||||||
|
```
|
||||||
|
|
||||||
|
Request Hook
|
||||||
|
|
||||||
|
```js
|
||||||
|
(text, from, to, url, key) => [url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json+protobuf",
|
||||||
|
"X-Goog-API-Key": key
|
||||||
|
},
|
||||||
|
body: JSON.stringify([[[text], from || "auto", to], "wt_lib"])
|
||||||
|
}]
|
||||||
|
```
|
||||||
|
|
||||||
|
Response Hook
|
||||||
|
|
||||||
|
```js
|
||||||
|
(res, text, from, to) => [res?.[0]?.join(" ") || "Translation unavailable", to === res?.[1]?.[0]]
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
33
package.json
33
package.json
@@ -1,36 +1,41 @@
|
|||||||
{
|
{
|
||||||
"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.10",
|
"version": "1.9.2",
|
||||||
"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.15.15",
|
||||||
"@mui/material": "^5.11.12",
|
"@mui/lab": "5.0.0-alpha.170",
|
||||||
|
"@mui/material": "^5.15.15",
|
||||||
"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",
|
||||||
|
"sval": "^0.5.2",
|
||||||
"webdav": "^5.3.0",
|
"webdav": "^5.3.0",
|
||||||
"webextension-polyfill": "^0.10.0"
|
"webextension-polyfill": "^0.10.0"
|
||||||
},
|
},
|
||||||
"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:thunderbird": "rm -rf build/thunderbird && BUILD_PATH=./build/thunderbird REACT_APP_CLIENT=thunderbird react-app-rewired build && rm build/thunderbird/content.html && cp ./build/thunderbird/manifest.thunderbird.json ./build/thunderbird/manifest.json && rm build/*/manifest.thunderbird.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 format && pnpm build:chrome && pnpm build:edge && pnpm build:thunderbird && pnpm build:firefox && pnpm build:web && pnpm build:userscript-ios && pnpm build:userscript && pnpm build:rules",
|
||||||
"deploy:web": "wrangler pages deploy ./build/web --project-name kiss-translator",
|
"zip": "cd build && rm -f *.zip && zip -r chrome.zip chrome && zip -r edge.zip edge && zip -r userscript.zip userscript && (cd firefox && zip -r ../firefox.zip .) && (cd thunderbird && zip -r ../thunderbird.zip .)",
|
||||||
|
"build+zip": "pnpm build && pnpm zip",
|
||||||
|
"format": "prettier --write \"**/*.{js,json,html}\"",
|
||||||
"test": "react-app-rewired test",
|
"test": "react-app-rewired test",
|
||||||
"eject": "react-scripts eject"
|
"eject": "react-scripts eject"
|
||||||
},
|
},
|
||||||
@@ -42,7 +47,8 @@
|
|||||||
"globals": {
|
"globals": {
|
||||||
"GM": true,
|
"GM": true,
|
||||||
"unsafeWindow": true,
|
"unsafeWindow": true,
|
||||||
"globalThis": true
|
"globalThis": true,
|
||||||
|
"messenger": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
@@ -58,10 +64,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"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",
|
||||||
|
"prettier": "3.6.2",
|
||||||
"react-app-rewired": "^2.2.1"
|
"react-app-rewired": "^2.2.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12365
pnpm-lock.yaml
generated
12365
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -13,5 +13,8 @@
|
|||||||
},
|
},
|
||||||
"open_options": {
|
"open_options": {
|
||||||
"message": "Open Options"
|
"message": "Open Options"
|
||||||
|
},
|
||||||
|
"open_tranbox": {
|
||||||
|
"message": "Translate Popup/Selected"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,5 +13,8 @@
|
|||||||
},
|
},
|
||||||
"open_options": {
|
"open_options": {
|
||||||
"message": "打开设置"
|
"message": "打开设置"
|
||||||
|
},
|
||||||
|
"open_tranbox": {
|
||||||
|
"message": "翻译弹窗/选中文字"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
// (() => {
|
// (() => {
|
||||||
// var shadow = document.querySelector("#shadow1");
|
// var shadow = document.querySelector("#shadow1");
|
||||||
// var root = shadow.attachShadow({ mode: "open" });
|
// var root = shadow.attachShadow({ mode: "open" });
|
||||||
@@ -54,8 +54,8 @@
|
|||||||
// }, 1000);
|
// }, 1000);
|
||||||
|
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
var el = document.querySelector("h2>p>span");
|
var el = document.querySelector('h2>p>span');
|
||||||
el.innerText = "hello world";
|
el.innerText = 'hello world';
|
||||||
}, 1000);
|
}, 1000);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -64,30 +64,67 @@
|
|||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<div id="root">
|
<div id="root">
|
||||||
|
<p>You need to enable <code>JavaScript</code> to run <span>this app.</span></p>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
<div id="content">
|
<div id="content">
|
||||||
<p>You need to enable JavaScript to run <span>this app.</span></p>
|
<p>You need to enable JavaScript to run <span>this app.</span></p>
|
||||||
The <span>embargo</span> has just lifted to confirm that AmpereOne is
|
The <span>embargo</span> has just lifted to confirm that AmpereOne is coming to
|
||||||
coming to Google Cloud with the C3A instances.
|
Google Cloud with the C3A instances.
|
||||||
<br />
|
<br />
|
||||||
But these upcoming instances for now are only in private preview form.
|
But these upcoming instances for now are only in private preview form.
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
Needless to say I also haven't had any AmpereOne access to check out the
|
Needless to say I also haven't had any AmpereOne access to check out the
|
||||||
performance and power efficiency of these new Arm server processors from
|
performance and power efficiency of these new Arm server processors from Ampere
|
||||||
Ampere Computing.
|
Computing.
|
||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
<h2>
|
<h2>
|
||||||
<p>
|
<p>
|
||||||
<span
|
<span>React is a JavaScript library for building user interfaces.</span>
|
||||||
>React is a JavaScript library for building user interfaces.</span
|
|
||||||
>
|
|
||||||
</p>
|
</p>
|
||||||
</h2>
|
</h2>
|
||||||
<hr />
|
<hr />
|
||||||
<input id="input1" style="width: 80%;" />
|
<input id="input1" style="width: 80%" />
|
||||||
<hr />
|
<hr />
|
||||||
<textarea id="textarea1" style="width: 80%;">test</textarea>
|
<textarea id="textarea1" style="width: 80%">test</textarea>
|
||||||
<hr />
|
<hr />
|
||||||
<div id="addtitle"></div>
|
<div id="addtitle"></div>
|
||||||
<h2>Shadow 1</h2>
|
<h2>Shadow 1</h2>
|
||||||
@@ -127,15 +164,47 @@
|
|||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<h2>
|
<h2>
|
||||||
React Server Components (or RSC) is a new application architecture
|
React Server Components (or RSC) is a new application architecture designed by the
|
||||||
designed by the React team.
|
React team.
|
||||||
</h2>
|
</h2>
|
||||||
<iframe
|
<iframe
|
||||||
id="iframe1"
|
id="iframe1"
|
||||||
width="800px"
|
width="800px"
|
||||||
height="600px"
|
height="600px"
|
||||||
src="http://localhost:3000/index.html"
|
src="http://localhost:3000/index.html"></iframe>
|
||||||
></iframe>
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<h2>We’ve first shared our research on RSC in an introductory talk and an RFC.</h2>
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
@@ -169,52 +238,10 @@
|
|||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<h2>
|
<h2>
|
||||||
We’ve first shared our research on RSC in an introductory talk and an
|
To recap them, we are introducing a new kind of component—Server Components—that
|
||||||
RFC.
|
run ahead of time and are excluded from your JavaScript bundle.
|
||||||
</h2>
|
</h2>
|
||||||
<br />
|
<iframe id="iframe2" width="800px" height="600px" src="https://react.dev/"></iframe>
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<h2>
|
|
||||||
To recap them, we are introducing a new kind of component—Server
|
|
||||||
Components—that run ahead of time and are excluded from your JavaScript
|
|
||||||
bundle.
|
|
||||||
</h2>
|
|
||||||
<iframe
|
|
||||||
id="iframe2"
|
|
||||||
width="800px"
|
|
||||||
height="600px"
|
|
||||||
src="https://react.dev/"
|
|
||||||
></iframe>
|
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
@@ -249,15 +276,14 @@
|
|||||||
<br />
|
<br />
|
||||||
<div class="cont cont1">
|
<div class="cont cont1">
|
||||||
<h2>
|
<h2>
|
||||||
Server Components can run during the build, letting you read from the
|
Server Components can run during the build, letting you read from the filesystem
|
||||||
filesystem or fetch static content.
|
or fetch static content.
|
||||||
</h2>
|
</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
They can also run on the server, letting you access your data layer
|
They can also run on the server, letting you access your data layer without
|
||||||
without having to build an API. You can pass data by props from
|
having to build an API. You can pass data by props from Server Components to
|
||||||
Server Components to the interactive Client Components in the
|
the interactive Client Components in the browser.
|
||||||
browser.
|
|
||||||
</li>
|
</li>
|
||||||
<li>以声明式编写 UI,可以让你的代码更加可靠,且方便调试。</li>
|
<li>以声明式编写 UI,可以让你的代码更加可靠,且方便调试。</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -276,14 +302,14 @@
|
|||||||
<br />
|
<br />
|
||||||
<div class="cont cont2">
|
<div class="cont cont2">
|
||||||
<h2>
|
<h2>
|
||||||
Since our last update, we have merged the React Server Components RFC
|
Since our last update, we have merged the React Server Components RFC to ratify
|
||||||
to ratify the proposal.
|
the proposal.
|
||||||
</h2>
|
</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
RSC combines the simple “request/response” mental model of
|
RSC combines the simple “request/response” mental model of server-centric
|
||||||
server-centric Multi-Page Apps with the seamless interactivity of
|
Multi-Page Apps with the seamless interactivity of client-centric Single-Page
|
||||||
client-centric Single-Page Apps, giving you the best of both worlds.
|
Apps, giving you the best of both worlds.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
React 使创建交互式 UI
|
React 使创建交互式 UI
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
|
|||||||
@@ -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.10",
|
"version": "1.9.2",
|
||||||
"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",
|
||||||
@@ -28,6 +28,12 @@
|
|||||||
},
|
},
|
||||||
"description": "__MSG_toggle_translate__"
|
"description": "__MSG_toggle_translate__"
|
||||||
},
|
},
|
||||||
|
"openTranbox": {
|
||||||
|
"suggested_key": {
|
||||||
|
"default": "Alt+S"
|
||||||
|
},
|
||||||
|
"description": "__MSG_open_tranbox__"
|
||||||
|
},
|
||||||
"toggleStyle": {
|
"toggleStyle": {
|
||||||
"suggested_key": {
|
"suggested_key": {
|
||||||
"default": "Alt+C"
|
"default": "Alt+C"
|
||||||
@@ -35,13 +41,16 @@
|
|||||||
"description": "__MSG_toggle_style__"
|
"description": "__MSG_toggle_style__"
|
||||||
},
|
},
|
||||||
"openOptions": {
|
"openOptions": {
|
||||||
"suggested_key": {
|
|
||||||
"default": "Alt+O"
|
|
||||||
},
|
|
||||||
"description": "__MSG_open_options__"
|
"description": "__MSG_open_options__"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"permissions": ["<all_urls>", "storage"],
|
"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",
|
||||||
|
|||||||
@@ -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.10",
|
"version": "1.9.2",
|
||||||
"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",
|
||||||
@@ -29,6 +29,12 @@
|
|||||||
},
|
},
|
||||||
"description": "__MSG_toggle_translate__"
|
"description": "__MSG_toggle_translate__"
|
||||||
},
|
},
|
||||||
|
"openTranbox": {
|
||||||
|
"suggested_key": {
|
||||||
|
"default": "Alt+S"
|
||||||
|
},
|
||||||
|
"description": "__MSG_open_tranbox__"
|
||||||
|
},
|
||||||
"toggleStyle": {
|
"toggleStyle": {
|
||||||
"suggested_key": {
|
"suggested_key": {
|
||||||
"default": "Alt+C"
|
"default": "Alt+C"
|
||||||
@@ -36,13 +42,10 @@
|
|||||||
"description": "__MSG_toggle_style__"
|
"description": "__MSG_toggle_style__"
|
||||||
},
|
},
|
||||||
"openOptions": {
|
"openOptions": {
|
||||||
"suggested_key": {
|
|
||||||
"default": "Alt+O"
|
|
||||||
},
|
|
||||||
"description": "__MSG_open_options__"
|
"description": "__MSG_open_options__"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"permissions": ["storage"],
|
"permissions": ["storage", "contextMenus", "scripting", "declarativeNetRequest"],
|
||||||
"host_permissions": ["<all_urls>"],
|
"host_permissions": ["<all_urls>"],
|
||||||
"icons": {
|
"icons": {
|
||||||
"16": "images/logo16.png",
|
"16": "images/logo16.png",
|
||||||
|
|||||||
78
public/manifest.thunderbird.json
Normal file
78
public/manifest.thunderbird.json
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
{
|
||||||
|
"manifest_version": 2,
|
||||||
|
"name": "__MSG_app_name__",
|
||||||
|
"description": "__MSG_app_description__",
|
||||||
|
"version": "1.9.2",
|
||||||
|
"default_locale": "en",
|
||||||
|
"author": "Gabe<yugang2002@gmail.com>",
|
||||||
|
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
||||||
|
"browser_specific_settings": {
|
||||||
|
"gecko": {
|
||||||
|
"id": "yugang2002@gmail.com",
|
||||||
|
"strict_min_version": "78.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"background": {
|
||||||
|
"scripts": ["background.js"]
|
||||||
|
},
|
||||||
|
"content_scripts": [
|
||||||
|
{
|
||||||
|
"js": ["content.js"],
|
||||||
|
"matches": ["<all_urls>"],
|
||||||
|
"all_frames": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"commands": {
|
||||||
|
"_execute_browser_action": {
|
||||||
|
"suggested_key": {
|
||||||
|
"default": "Alt+K"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"toggleTranslate": {
|
||||||
|
"suggested_key": {
|
||||||
|
"default": "Alt+Q"
|
||||||
|
},
|
||||||
|
"description": "__MSG_toggle_translate__"
|
||||||
|
},
|
||||||
|
"openTranbox": {
|
||||||
|
"suggested_key": {
|
||||||
|
"default": "Alt+S"
|
||||||
|
},
|
||||||
|
"description": "__MSG_open_tranbox__"
|
||||||
|
},
|
||||||
|
"toggleStyle": {
|
||||||
|
"suggested_key": {
|
||||||
|
"default": "Alt+C"
|
||||||
|
},
|
||||||
|
"description": "__MSG_toggle_style__"
|
||||||
|
},
|
||||||
|
"openOptions": {
|
||||||
|
"description": "__MSG_open_options__"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"permissions": [
|
||||||
|
"<all_urls>",
|
||||||
|
"storage",
|
||||||
|
"menus",
|
||||||
|
"messagesModify",
|
||||||
|
"scripting",
|
||||||
|
"declarativeNetRequest"
|
||||||
|
],
|
||||||
|
"icons": {
|
||||||
|
"16": "images/logo16.png",
|
||||||
|
"32": "images/logo32.png",
|
||||||
|
"48": "images/logo48.png",
|
||||||
|
"128": "images/logo128.png"
|
||||||
|
},
|
||||||
|
"browser_action": {
|
||||||
|
"default_icon": {
|
||||||
|
"128": "images/logo128.png"
|
||||||
|
},
|
||||||
|
"default_title": "__MSG_app_name__",
|
||||||
|
"default_popup": "popup.html"
|
||||||
|
},
|
||||||
|
"options_ui": {
|
||||||
|
"page": "options.html",
|
||||||
|
"open_in_tab": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,230 +1,23 @@
|
|||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import { getBdauth, setBdauth } from "../libs/storage";
|
import { URL_BAIDU_TRANSAPI, DEFAULT_USER_AGENT } from "../config";
|
||||||
import { URL_BAIDU_WEB, URL_BAIDU_TRAN } from "../config";
|
|
||||||
import { fetchApi } from "../libs/fetch";
|
|
||||||
|
|
||||||
/* eslint-disable */
|
|
||||||
function n(t, e) {
|
|
||||||
for (var n = 0; n < e.length - 2; n += 3) {
|
|
||||||
var r = e.charAt(n + 2);
|
|
||||||
(r = "a" <= r ? r.charCodeAt(0) - 87 : Number(r)),
|
|
||||||
(r = "+" === e.charAt(n + 1) ? t >>> r : t << r),
|
|
||||||
(t = "+" === e.charAt(n) ? (t + r) & 4294967295 : t ^ r);
|
|
||||||
}
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
|
|
||||||
function e(t, e) {
|
|
||||||
(null == e || e > t.length) && (e = t.length);
|
|
||||||
for (var n = 0, r = new Array(e); n < e; n++) r[n] = t[n];
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* eslint-disable */
|
|
||||||
function getSign(t, gtk, r = null) {
|
|
||||||
var o,
|
|
||||||
i = t.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g);
|
|
||||||
if (null === i) {
|
|
||||||
var a = t.length;
|
|
||||||
a > 30 &&
|
|
||||||
(t = ""
|
|
||||||
.concat(t.substr(0, 10))
|
|
||||||
.concat(t.substr(Math.floor(a / 2) - 5, 10))
|
|
||||||
.concat(t.substr(-10, 10)));
|
|
||||||
} else {
|
|
||||||
for (
|
|
||||||
var s = t.split(/[\uD800-\uDBFF][\uDC00-\uDFFF]/),
|
|
||||||
c = 0,
|
|
||||||
u = s.length,
|
|
||||||
l = [];
|
|
||||||
c < u;
|
|
||||||
c++
|
|
||||||
)
|
|
||||||
"" !== s[c] &&
|
|
||||||
l.push.apply(
|
|
||||||
l,
|
|
||||||
(function (t) {
|
|
||||||
if (Array.isArray(t)) return e(t);
|
|
||||||
})((o = s[c].split(""))) ||
|
|
||||||
(function (t) {
|
|
||||||
if (
|
|
||||||
("undefined" != typeof Symbol && null != t[Symbol.iterator]) ||
|
|
||||||
null != t["@@iterator"]
|
|
||||||
)
|
|
||||||
return Array.from(t);
|
|
||||||
})(o) ||
|
|
||||||
(function (t, n) {
|
|
||||||
if (t) {
|
|
||||||
if ("string" == typeof t) return e(t, n);
|
|
||||||
var r = Object.prototype.toString.call(t).slice(8, -1);
|
|
||||||
return (
|
|
||||||
"Object" === r && t.constructor && (r = t.constructor.name),
|
|
||||||
"Map" === r || "Set" === r
|
|
||||||
? Array.from(t)
|
|
||||||
: "Arguments" === r ||
|
|
||||||
/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)
|
|
||||||
? e(t, n)
|
|
||||||
: void 0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})(o) ||
|
|
||||||
(function () {
|
|
||||||
throw new TypeError(
|
|
||||||
"Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."
|
|
||||||
);
|
|
||||||
})()
|
|
||||||
),
|
|
||||||
c !== u - 1 && l.push(i[c]);
|
|
||||||
var p = l.length;
|
|
||||||
p > 30 &&
|
|
||||||
(t =
|
|
||||||
l.slice(0, 10).join("") +
|
|
||||||
l.slice(Math.floor(p / 2) - 5, Math.floor(p / 2) + 5).join("") +
|
|
||||||
l.slice(-10).join(""));
|
|
||||||
}
|
|
||||||
for (
|
|
||||||
var d = ""
|
|
||||||
.concat(String.fromCharCode(103))
|
|
||||||
.concat(String.fromCharCode(116))
|
|
||||||
.concat(String.fromCharCode(107)),
|
|
||||||
h = (null !== r ? r : (r = gtk || "") || "").split("."),
|
|
||||||
f = Number(h[0]) || 0,
|
|
||||||
m = Number(h[1]) || 0,
|
|
||||||
g = [],
|
|
||||||
y = 0,
|
|
||||||
v = 0;
|
|
||||||
v < t.length;
|
|
||||||
v++
|
|
||||||
) {
|
|
||||||
var _ = t.charCodeAt(v);
|
|
||||||
_ < 128
|
|
||||||
? (g[y++] = _)
|
|
||||||
: (_ < 2048
|
|
||||||
? (g[y++] = (_ >> 6) | 192)
|
|
||||||
: (55296 == (64512 & _) &&
|
|
||||||
v + 1 < t.length &&
|
|
||||||
56320 == (64512 & t.charCodeAt(v + 1))
|
|
||||||
? ((_ = 65536 + ((1023 & _) << 10) + (1023 & t.charCodeAt(++v))),
|
|
||||||
(g[y++] = (_ >> 18) | 240),
|
|
||||||
(g[y++] = ((_ >> 12) & 63) | 128))
|
|
||||||
: (g[y++] = (_ >> 12) | 224),
|
|
||||||
(g[y++] = ((_ >> 6) & 63) | 128)),
|
|
||||||
(g[y++] = (63 & _) | 128));
|
|
||||||
}
|
|
||||||
for (
|
|
||||||
var b = f,
|
|
||||||
w =
|
|
||||||
""
|
|
||||||
.concat(String.fromCharCode(43))
|
|
||||||
.concat(String.fromCharCode(45))
|
|
||||||
.concat(String.fromCharCode(97)) +
|
|
||||||
""
|
|
||||||
.concat(String.fromCharCode(94))
|
|
||||||
.concat(String.fromCharCode(43))
|
|
||||||
.concat(String.fromCharCode(54)),
|
|
||||||
k =
|
|
||||||
""
|
|
||||||
.concat(String.fromCharCode(43))
|
|
||||||
.concat(String.fromCharCode(45))
|
|
||||||
.concat(String.fromCharCode(51)) +
|
|
||||||
""
|
|
||||||
.concat(String.fromCharCode(94))
|
|
||||||
.concat(String.fromCharCode(43))
|
|
||||||
.concat(String.fromCharCode(98)) +
|
|
||||||
""
|
|
||||||
.concat(String.fromCharCode(43))
|
|
||||||
.concat(String.fromCharCode(45))
|
|
||||||
.concat(String.fromCharCode(102)),
|
|
||||||
x = 0;
|
|
||||||
x < g.length;
|
|
||||||
x++
|
|
||||||
)
|
|
||||||
b = n((b += g[x]), w);
|
|
||||||
return (
|
|
||||||
(b = n(b, k)),
|
|
||||||
(b ^= m) < 0 && (b = 2147483648 + (2147483647 & b)),
|
|
||||||
"".concat((b %= 1e6).toString(), ".").concat(b ^ f)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const getToken = async () => {
|
|
||||||
const res = await fetchApi({
|
|
||||||
input: URL_BAIDU_WEB,
|
|
||||||
init: {
|
|
||||||
headers: {
|
|
||||||
"Content-type": "text/html; charset=utf-8",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error(res.statusText);
|
|
||||||
}
|
|
||||||
|
|
||||||
const text = await res.text();
|
|
||||||
const token = text.match(/token: '(.*)',/)[1];
|
|
||||||
const gtk = text.match(/gtk = "(.*)";/)[1];
|
|
||||||
const exp = Date.now() + 8 * 60 * 60 * 1000;
|
|
||||||
|
|
||||||
if (!token || !gtk) {
|
|
||||||
throw new Error("[baidu] get token error");
|
|
||||||
}
|
|
||||||
|
|
||||||
return { token, gtk, exp };
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 闭包缓存token,减少对storage查询
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
const _bdAuth = () => {
|
|
||||||
let store;
|
|
||||||
|
|
||||||
return async () => {
|
|
||||||
const now = Date.now();
|
|
||||||
|
|
||||||
// 查询内存缓存
|
|
||||||
if (store && store.exp > now) {
|
|
||||||
return store;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查询storage缓存
|
|
||||||
store = await getBdauth();
|
|
||||||
if (store && store.exp > now) {
|
|
||||||
return store;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 缓存没有或失效,查询接口
|
|
||||||
store = await getToken();
|
|
||||||
await setBdauth(store);
|
|
||||||
return store;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const bdAuth = _bdAuth();
|
|
||||||
|
|
||||||
export const genBaidu = async ({ text, from, to }) => {
|
export const genBaidu = async ({ text, from, to }) => {
|
||||||
const { token, gtk } = await bdAuth();
|
|
||||||
const sign = getSign(text, gtk);
|
|
||||||
const data = {
|
const data = {
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
query: text,
|
query: text,
|
||||||
simple_means_flag: 3,
|
source: "txt",
|
||||||
sign,
|
|
||||||
token,
|
|
||||||
domain: "common",
|
|
||||||
ts: Date.now(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const input = `${URL_BAIDU_TRAN}?from=${from}&to=${to}`;
|
|
||||||
const init = {
|
const init = {
|
||||||
headers: {
|
headers: {
|
||||||
|
// Origin: "https://fanyi.baidu.com",
|
||||||
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
|
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||||
|
"User-Agent": DEFAULT_USER_AGENT,
|
||||||
},
|
},
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: queryString.stringify(data),
|
body: queryString.stringify(data),
|
||||||
};
|
};
|
||||||
|
|
||||||
return [input, init];
|
return [URL_BAIDU_TRANSAPI, init];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,25 +1,48 @@
|
|||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import { fetchPolyfill } from "../libs/fetch";
|
import { fetchData } from "../libs/fetch";
|
||||||
import {
|
import {
|
||||||
OPT_TRANS_GOOGLE,
|
OPT_TRANS_GOOGLE,
|
||||||
|
OPT_TRANS_GOOGLE_2,
|
||||||
OPT_TRANS_MICROSOFT,
|
OPT_TRANS_MICROSOFT,
|
||||||
OPT_TRANS_DEEPL,
|
OPT_TRANS_DEEPL,
|
||||||
OPT_TRANS_DEEPLFREE,
|
OPT_TRANS_DEEPLFREE,
|
||||||
OPT_TRANS_DEEPLX,
|
OPT_TRANS_DEEPLX,
|
||||||
|
OPT_TRANS_NIUTRANS,
|
||||||
OPT_TRANS_BAIDU,
|
OPT_TRANS_BAIDU,
|
||||||
OPT_TRANS_TENCENT,
|
OPT_TRANS_TENCENT,
|
||||||
|
OPT_TRANS_VOLCENGINE,
|
||||||
OPT_TRANS_OPENAI,
|
OPT_TRANS_OPENAI,
|
||||||
|
OPT_TRANS_OPENAI_2,
|
||||||
|
OPT_TRANS_OPENAI_3,
|
||||||
|
OPT_TRANS_GEMINI,
|
||||||
|
OPT_TRANS_GEMINI_2,
|
||||||
|
OPT_TRANS_CLAUDE,
|
||||||
OPT_TRANS_CLOUDFLAREAI,
|
OPT_TRANS_CLOUDFLAREAI,
|
||||||
|
OPT_TRANS_OLLAMA,
|
||||||
|
OPT_TRANS_OLLAMA_2,
|
||||||
|
OPT_TRANS_OLLAMA_3,
|
||||||
OPT_TRANS_CUSTOMIZE,
|
OPT_TRANS_CUSTOMIZE,
|
||||||
|
OPT_TRANS_CUSTOMIZE_2,
|
||||||
|
OPT_TRANS_CUSTOMIZE_3,
|
||||||
|
OPT_TRANS_CUSTOMIZE_4,
|
||||||
|
OPT_TRANS_CUSTOMIZE_5,
|
||||||
URL_CACHE_TRAN,
|
URL_CACHE_TRAN,
|
||||||
KV_SALT_SYNC,
|
KV_SALT_SYNC,
|
||||||
|
URL_GOOGLE_TRAN,
|
||||||
|
URL_MICROSOFT_LANGDETECT,
|
||||||
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,
|
||||||
OPT_LANGS_SPECIAL,
|
OPT_LANGS_SPECIAL,
|
||||||
|
OPT_LANGS_MICROSOFT,
|
||||||
} from "../config";
|
} from "../config";
|
||||||
import { sha256 } from "../libs/utils";
|
import { sha256 } from "../libs/utils";
|
||||||
|
import interpreter from "../libs/interpreter";
|
||||||
|
import { msAuth } from "../libs/auth";
|
||||||
|
import { kissLog } from "../libs/log";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 同步数据
|
* 同步数据
|
||||||
@@ -29,7 +52,7 @@ import { sha256 } from "../libs/utils";
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const apiSyncData = async (url, key, data) =>
|
export const apiSyncData = async (url, key, data) =>
|
||||||
fetchPolyfill(url, {
|
fetchData(url, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-type": "application/json",
|
"Content-type": "application/json",
|
||||||
Authorization: `Bearer ${await sha256(key, KV_SALT_SYNC)}`,
|
Authorization: `Bearer ${await sha256(key, KV_SALT_SYNC)}`,
|
||||||
@@ -43,7 +66,53 @@ export const apiSyncData = async (url, key, data) =>
|
|||||||
* @param {*} url
|
* @param {*} url
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const apiFetch = (url) => fetchPolyfill(url);
|
export const apiFetch = (url) => fetchData(url);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Google语言识别
|
||||||
|
* @param {*} text
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const apiGoogleLangdetect = async (text) => {
|
||||||
|
const params = {
|
||||||
|
client: "gtx",
|
||||||
|
dt: "t",
|
||||||
|
dj: 1,
|
||||||
|
ie: "UTF-8",
|
||||||
|
sl: "auto",
|
||||||
|
tl: "zh-CN",
|
||||||
|
q: text,
|
||||||
|
};
|
||||||
|
const input = `${URL_GOOGLE_TRAN}?${queryString.stringify(params)}`;
|
||||||
|
const res = await fetchData(input, {
|
||||||
|
headers: {
|
||||||
|
"Content-type": "application/json",
|
||||||
|
},
|
||||||
|
useCache: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.src;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Microsoft语言识别
|
||||||
|
* @param {*} text
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const apiMicrosoftLangdetect = async (text) => {
|
||||||
|
const [token] = await msAuth();
|
||||||
|
const res = await fetchData(URL_MICROSOFT_LANGDETECT, {
|
||||||
|
headers: {
|
||||||
|
"Content-type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify([{ Text: text }]),
|
||||||
|
useCache: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return OPT_LANGS_MICROSOFT.get(res[0].language) ?? res[0].language;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 百度语言识别
|
* 百度语言识别
|
||||||
@@ -51,7 +120,7 @@ export const apiFetch = (url) => fetchPolyfill(url);
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const apiBaiduLangdetect = async (text) => {
|
export const apiBaiduLangdetect = async (text) => {
|
||||||
const res = await fetchPolyfill(URL_BAIDU_LANGDETECT, {
|
const res = await fetchData(URL_BAIDU_LANGDETECT, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-type": "application/json",
|
"Content-type": "application/json",
|
||||||
},
|
},
|
||||||
@@ -69,6 +138,44 @@ export const apiBaiduLangdetect = async (text) => {
|
|||||||
return "";
|
return "";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 百度翻译建议
|
||||||
|
* @param {*} text
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const apiBaiduSuggest = async (text) => {
|
||||||
|
const res = await fetchData(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 fetchData(url, {
|
||||||
|
useCache: false, // 为避免缓存过快增长,禁用缓存语音数据
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 腾讯语言识别
|
* 腾讯语言识别
|
||||||
* @param {*} text
|
* @param {*} text
|
||||||
@@ -82,7 +189,7 @@ export const apiTencentLangdetect = async (text) => {
|
|||||||
text,
|
text,
|
||||||
});
|
});
|
||||||
|
|
||||||
const res = await fetchPolyfill(URL_TENCENT_TRANSMART, {
|
const res = await fetchData(URL_TENCENT_TRANSMART, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-type": "application/json",
|
"Content-type": "application/json",
|
||||||
},
|
},
|
||||||
@@ -111,20 +218,27 @@ export const apiTranslate = async ({
|
|||||||
let trText = "";
|
let trText = "";
|
||||||
let isSame = false;
|
let isSame = false;
|
||||||
|
|
||||||
|
if (!text) {
|
||||||
|
return [trText, true];
|
||||||
|
}
|
||||||
|
|
||||||
const from =
|
const from =
|
||||||
OPT_LANGS_SPECIAL[translator].get(fromLang) ??
|
OPT_LANGS_SPECIAL[translator].get(fromLang) ??
|
||||||
OPT_LANGS_SPECIAL[translator].get("auto");
|
OPT_LANGS_SPECIAL[translator].get("auto");
|
||||||
const to = OPT_LANGS_SPECIAL[translator].get(toLang);
|
const to = OPT_LANGS_SPECIAL[translator].get(toLang);
|
||||||
if (!text || !to) {
|
if (!to) {
|
||||||
console.log(`[trans] target lang: ${toLang} not support`);
|
kissLog(`target lang: ${toLang} not support`, "translate");
|
||||||
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 = {
|
||||||
@@ -134,7 +248,7 @@ export const apiTranslate = async ({
|
|||||||
to,
|
to,
|
||||||
};
|
};
|
||||||
|
|
||||||
const res = await fetchPolyfill(
|
const res = await fetchData(
|
||||||
`${URL_CACHE_TRAN}?${queryString.stringify(cacheOpts)}`,
|
`${URL_CACHE_TRAN}?${queryString.stringify(cacheOpts)}`,
|
||||||
{
|
{
|
||||||
useCache,
|
useCache,
|
||||||
@@ -149,8 +263,14 @@ export const apiTranslate = async ({
|
|||||||
trText = res.sentences.map((item) => item.trans).join(" ");
|
trText = res.sentences.map((item) => item.trans).join(" ");
|
||||||
isSame = to === res.src;
|
isSame = to === res.src;
|
||||||
break;
|
break;
|
||||||
|
case OPT_TRANS_GOOGLE_2:
|
||||||
|
trText = res?.[0]?.[0] || "";
|
||||||
|
isSame = to === res.src;
|
||||||
|
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:
|
||||||
@@ -165,25 +285,79 @@ export const apiTranslate = async ({
|
|||||||
trText = res.data;
|
trText = res.data;
|
||||||
isSame = to === res.source_lang;
|
isSame = to === res.source_lang;
|
||||||
break;
|
break;
|
||||||
|
case OPT_TRANS_NIUTRANS:
|
||||||
|
const json = JSON.parse(res);
|
||||||
|
if (json.error_msg) {
|
||||||
|
throw new Error(json.error_msg);
|
||||||
|
}
|
||||||
|
trText = json.tgt_text;
|
||||||
|
isSame = to === json.from;
|
||||||
|
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?.[0];
|
||||||
isSame = text === trText;
|
isSame = text === trText;
|
||||||
break;
|
break;
|
||||||
|
case OPT_TRANS_VOLCENGINE:
|
||||||
|
trText = res?.translation || "";
|
||||||
|
isSame = to === res?.detected_language;
|
||||||
|
break;
|
||||||
case OPT_TRANS_OPENAI:
|
case OPT_TRANS_OPENAI:
|
||||||
trText = res?.choices?.[0].message.content;
|
case OPT_TRANS_OPENAI_2:
|
||||||
|
case OPT_TRANS_OPENAI_3:
|
||||||
|
case OPT_TRANS_GEMINI_2:
|
||||||
|
trText = res?.choices?.map((item) => item.message.content).join(" ");
|
||||||
|
isSame = text === trText;
|
||||||
|
break;
|
||||||
|
case OPT_TRANS_GEMINI:
|
||||||
|
trText = res?.candidates
|
||||||
|
?.map((item) => item.content?.parts.map((item) => item.text).join(" "))
|
||||||
|
.join(" ");
|
||||||
|
isSame = text === trText;
|
||||||
|
break;
|
||||||
|
case OPT_TRANS_CLAUDE:
|
||||||
|
trText = res?.content?.map((item) => item.text).join(" ");
|
||||||
isSame = text === trText;
|
isSame = text === trText;
|
||||||
break;
|
break;
|
||||||
case OPT_TRANS_CLOUDFLAREAI:
|
case OPT_TRANS_CLOUDFLAREAI:
|
||||||
trText = res?.result?.translated_text;
|
trText = res?.result?.translated_text;
|
||||||
isSame = text === trText;
|
isSame = text === trText;
|
||||||
break;
|
break;
|
||||||
|
case OPT_TRANS_OLLAMA:
|
||||||
|
case OPT_TRANS_OLLAMA_2:
|
||||||
|
case OPT_TRANS_OLLAMA_3:
|
||||||
|
const { thinkIgnore = "" } = apiSetting;
|
||||||
|
const deepModels = thinkIgnore.split(",").filter((model) => model.trim());
|
||||||
|
if (deepModels.some((model) => res?.model?.startsWith(model))) {
|
||||||
|
trText = res?.response.replace(/<think>[\s\S]*<\/think>/i, "");
|
||||||
|
} else {
|
||||||
|
trText = res?.response;
|
||||||
|
}
|
||||||
|
isSame = text === trText;
|
||||||
|
break;
|
||||||
case OPT_TRANS_CUSTOMIZE:
|
case OPT_TRANS_CUSTOMIZE:
|
||||||
trText = res.text;
|
case OPT_TRANS_CUSTOMIZE_2:
|
||||||
isSame = to === res.from;
|
case OPT_TRANS_CUSTOMIZE_3:
|
||||||
|
case OPT_TRANS_CUSTOMIZE_4:
|
||||||
|
case OPT_TRANS_CUSTOMIZE_5:
|
||||||
|
const { resHook } = apiSetting;
|
||||||
|
if (resHook?.trim()) {
|
||||||
|
interpreter.run(`exports.resHook = ${resHook}`);
|
||||||
|
[trText, isSame] = interpreter.exports.resHook(res, text, from, to);
|
||||||
|
} else {
|
||||||
|
trText = res.text;
|
||||||
|
isSame = to === res.from;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|||||||
612
src/apis/trans.js
Normal file
612
src/apis/trans.js
Normal file
@@ -0,0 +1,612 @@
|
|||||||
|
import queryString from "query-string";
|
||||||
|
import {
|
||||||
|
OPT_TRANS_GOOGLE,
|
||||||
|
OPT_TRANS_GOOGLE_2,
|
||||||
|
OPT_TRANS_MICROSOFT,
|
||||||
|
OPT_TRANS_DEEPL,
|
||||||
|
OPT_TRANS_DEEPLFREE,
|
||||||
|
OPT_TRANS_DEEPLX,
|
||||||
|
OPT_TRANS_NIUTRANS,
|
||||||
|
OPT_TRANS_BAIDU,
|
||||||
|
OPT_TRANS_TENCENT,
|
||||||
|
OPT_TRANS_VOLCENGINE,
|
||||||
|
OPT_TRANS_OPENAI,
|
||||||
|
OPT_TRANS_OPENAI_2,
|
||||||
|
OPT_TRANS_OPENAI_3,
|
||||||
|
OPT_TRANS_GEMINI,
|
||||||
|
OPT_TRANS_GEMINI_2,
|
||||||
|
OPT_TRANS_CLAUDE,
|
||||||
|
OPT_TRANS_CLOUDFLAREAI,
|
||||||
|
OPT_TRANS_OLLAMA,
|
||||||
|
OPT_TRANS_OLLAMA_2,
|
||||||
|
OPT_TRANS_OLLAMA_3,
|
||||||
|
OPT_TRANS_CUSTOMIZE,
|
||||||
|
OPT_TRANS_CUSTOMIZE_2,
|
||||||
|
OPT_TRANS_CUSTOMIZE_3,
|
||||||
|
OPT_TRANS_CUSTOMIZE_4,
|
||||||
|
OPT_TRANS_CUSTOMIZE_5,
|
||||||
|
URL_MICROSOFT_TRAN,
|
||||||
|
URL_TENCENT_TRANSMART,
|
||||||
|
URL_VOLCENGINE_TRAN,
|
||||||
|
INPUT_PLACE_URL,
|
||||||
|
INPUT_PLACE_FROM,
|
||||||
|
INPUT_PLACE_TO,
|
||||||
|
INPUT_PLACE_TEXT,
|
||||||
|
INPUT_PLACE_KEY,
|
||||||
|
INPUT_PLACE_MODEL,
|
||||||
|
} from "../config";
|
||||||
|
import { msAuth } from "../libs/auth";
|
||||||
|
import { genDeeplFree } from "./deepl";
|
||||||
|
import { genBaidu } from "./baidu";
|
||||||
|
import interpreter from "../libs/interpreter";
|
||||||
|
|
||||||
|
const keyMap = new Map();
|
||||||
|
const urlMap = new Map();
|
||||||
|
|
||||||
|
// 轮询key/url
|
||||||
|
const keyPick = (translator, key = "", cacheMap) => {
|
||||||
|
const keys = key
|
||||||
|
.split(/\n|,/)
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
if (keys.length === 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const preIndex = cacheMap.get(translator) ?? -1;
|
||||||
|
const curIndex = (preIndex + 1) % keys.length;
|
||||||
|
cacheMap.set(translator, curIndex);
|
||||||
|
|
||||||
|
return keys[curIndex];
|
||||||
|
};
|
||||||
|
|
||||||
|
const genGoogle = ({ text, from, to, url, key }) => {
|
||||||
|
const params = {
|
||||||
|
client: "gtx",
|
||||||
|
dt: "t",
|
||||||
|
dj: 1,
|
||||||
|
ie: "UTF-8",
|
||||||
|
sl: from,
|
||||||
|
tl: to,
|
||||||
|
q: text,
|
||||||
|
};
|
||||||
|
const input = `${url}?${queryString.stringify(params)}`;
|
||||||
|
const init = {
|
||||||
|
headers: {
|
||||||
|
"Content-type": "application/json",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (key) {
|
||||||
|
init.headers.Authorization = `Bearer ${key}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [input, init];
|
||||||
|
};
|
||||||
|
|
||||||
|
const genGoogle2 = ({ text, from, to, url, key }) => {
|
||||||
|
const body = JSON.stringify([[[text], from, to], "wt_lib"]);
|
||||||
|
const init = {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json+protobuf",
|
||||||
|
"X-Goog-API-Key": key,
|
||||||
|
},
|
||||||
|
body,
|
||||||
|
};
|
||||||
|
|
||||||
|
return [url, init];
|
||||||
|
};
|
||||||
|
|
||||||
|
const genMicrosoft = async ({ text, from, to }) => {
|
||||||
|
const [token] = await msAuth();
|
||||||
|
const params = {
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
"api-version": "3.0",
|
||||||
|
};
|
||||||
|
const input = `${URL_MICROSOFT_TRAN}?${queryString.stringify(params)}`;
|
||||||
|
const init = {
|
||||||
|
headers: {
|
||||||
|
"Content-type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify([{ Text: text }]),
|
||||||
|
};
|
||||||
|
|
||||||
|
return [input, init];
|
||||||
|
};
|
||||||
|
|
||||||
|
const genDeepl = ({ text, from, to, url, key }) => {
|
||||||
|
const data = {
|
||||||
|
text: [text],
|
||||||
|
target_lang: to,
|
||||||
|
source_lang: from,
|
||||||
|
// split_sentences: "0",
|
||||||
|
};
|
||||||
|
const init = {
|
||||||
|
headers: {
|
||||||
|
"Content-type": "application/json",
|
||||||
|
Authorization: `DeepL-Auth-Key ${key}`,
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
};
|
||||||
|
|
||||||
|
return [url, init];
|
||||||
|
};
|
||||||
|
|
||||||
|
const genDeeplX = ({ text, from, to, url, key }) => {
|
||||||
|
const data = {
|
||||||
|
text,
|
||||||
|
target_lang: to,
|
||||||
|
source_lang: from,
|
||||||
|
};
|
||||||
|
|
||||||
|
const init = {
|
||||||
|
headers: {
|
||||||
|
"Content-type": "application/json",
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
};
|
||||||
|
if (key) {
|
||||||
|
init.headers.Authorization = `Bearer ${key}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [url, init];
|
||||||
|
};
|
||||||
|
|
||||||
|
const genNiuTrans = ({ text, from, to, url, key, dictNo, memoryNo }) => {
|
||||||
|
const data = {
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
apikey: key,
|
||||||
|
src_text: text,
|
||||||
|
dictNo,
|
||||||
|
memoryNo,
|
||||||
|
};
|
||||||
|
|
||||||
|
const init = {
|
||||||
|
headers: {
|
||||||
|
"Content-type": "application/json",
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
};
|
||||||
|
|
||||||
|
return [url, init];
|
||||||
|
};
|
||||||
|
|
||||||
|
const genTencent = ({ text, from, to }) => {
|
||||||
|
const data = {
|
||||||
|
header: {
|
||||||
|
fn: "auto_translation",
|
||||||
|
client_key:
|
||||||
|
"browser-chrome-110.0.0-Mac OS-df4bd4c5-a65d-44b2-a40f-42f34f3535f2-1677486696487",
|
||||||
|
},
|
||||||
|
type: "plain",
|
||||||
|
model_category: "normal",
|
||||||
|
source: {
|
||||||
|
text_list: [text],
|
||||||
|
lang: from,
|
||||||
|
},
|
||||||
|
target: {
|
||||||
|
lang: to,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const init = {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"user-agent":
|
||||||
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36",
|
||||||
|
referer: "https://transmart.qq.com/zh-CN/index",
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
};
|
||||||
|
|
||||||
|
return [URL_TENCENT_TRANSMART, init];
|
||||||
|
};
|
||||||
|
|
||||||
|
const genVolcengine = ({ text, from, to }) => {
|
||||||
|
const data = {
|
||||||
|
source_language: from,
|
||||||
|
target_language: to,
|
||||||
|
text: text,
|
||||||
|
};
|
||||||
|
|
||||||
|
const init = {
|
||||||
|
headers: {
|
||||||
|
"Content-type": "application/json",
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
};
|
||||||
|
|
||||||
|
return [URL_VOLCENGINE_TRAN, init];
|
||||||
|
};
|
||||||
|
|
||||||
|
const genOpenAI = ({
|
||||||
|
text,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
url,
|
||||||
|
key,
|
||||||
|
systemPrompt,
|
||||||
|
userPrompt,
|
||||||
|
model,
|
||||||
|
temperature,
|
||||||
|
maxTokens,
|
||||||
|
}) => {
|
||||||
|
// 兼容历史上作为systemPrompt的prompt,如果prompt中不包含带翻译文本,则添加文本到prompt末尾
|
||||||
|
// if (!prompt.includes(INPUT_PLACE_TEXT)) {
|
||||||
|
// prompt += `\nSource Text: ${INPUT_PLACE_TEXT}`;
|
||||||
|
// }
|
||||||
|
systemPrompt = systemPrompt
|
||||||
|
.replaceAll(INPUT_PLACE_FROM, from)
|
||||||
|
.replaceAll(INPUT_PLACE_TO, to)
|
||||||
|
.replaceAll(INPUT_PLACE_TEXT, text);
|
||||||
|
userPrompt = userPrompt
|
||||||
|
.replaceAll(INPUT_PLACE_FROM, from)
|
||||||
|
.replaceAll(INPUT_PLACE_TO, to)
|
||||||
|
.replaceAll(INPUT_PLACE_TEXT, text);
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
model,
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "system",
|
||||||
|
content: systemPrompt,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: userPrompt,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
temperature,
|
||||||
|
max_completion_tokens: maxTokens,
|
||||||
|
};
|
||||||
|
|
||||||
|
const init = {
|
||||||
|
headers: {
|
||||||
|
"Content-type": "application/json",
|
||||||
|
Authorization: `Bearer ${key}`, // OpenAI
|
||||||
|
"api-key": key, // Azure OpenAI
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
};
|
||||||
|
|
||||||
|
return [url, init];
|
||||||
|
};
|
||||||
|
|
||||||
|
const genGemini = ({
|
||||||
|
text,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
url,
|
||||||
|
key,
|
||||||
|
systemPrompt,
|
||||||
|
userPrompt,
|
||||||
|
model,
|
||||||
|
temperature,
|
||||||
|
maxTokens,
|
||||||
|
}) => {
|
||||||
|
url = url
|
||||||
|
.replaceAll(INPUT_PLACE_MODEL, model)
|
||||||
|
.replaceAll(INPUT_PLACE_KEY, key);
|
||||||
|
systemPrompt = systemPrompt
|
||||||
|
.replaceAll(INPUT_PLACE_FROM, from)
|
||||||
|
.replaceAll(INPUT_PLACE_TO, to)
|
||||||
|
.replaceAll(INPUT_PLACE_TEXT, text);
|
||||||
|
userPrompt = userPrompt
|
||||||
|
.replaceAll(INPUT_PLACE_FROM, from)
|
||||||
|
.replaceAll(INPUT_PLACE_TO, to)
|
||||||
|
.replaceAll(INPUT_PLACE_TEXT, text);
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
system_instruction: {
|
||||||
|
parts: {
|
||||||
|
text: systemPrompt,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
contents: {
|
||||||
|
role: "user",
|
||||||
|
parts: {
|
||||||
|
text: userPrompt,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
generationConfig: {
|
||||||
|
maxOutputTokens: maxTokens,
|
||||||
|
temperature,
|
||||||
|
// topP: 0.8,
|
||||||
|
// topK: 10,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const init = {
|
||||||
|
headers: {
|
||||||
|
"Content-type": "application/json",
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
};
|
||||||
|
|
||||||
|
return [url, init];
|
||||||
|
};
|
||||||
|
|
||||||
|
const genGemini2 = ({
|
||||||
|
text,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
url,
|
||||||
|
key,
|
||||||
|
systemPrompt,
|
||||||
|
userPrompt,
|
||||||
|
model,
|
||||||
|
temperature,
|
||||||
|
maxTokens,
|
||||||
|
}) => {
|
||||||
|
systemPrompt = systemPrompt
|
||||||
|
.replaceAll(INPUT_PLACE_FROM, from)
|
||||||
|
.replaceAll(INPUT_PLACE_TO, to)
|
||||||
|
.replaceAll(INPUT_PLACE_TEXT, text);
|
||||||
|
userPrompt = userPrompt
|
||||||
|
.replaceAll(INPUT_PLACE_FROM, from)
|
||||||
|
.replaceAll(INPUT_PLACE_TO, to)
|
||||||
|
.replaceAll(INPUT_PLACE_TEXT, text);
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
model,
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "system",
|
||||||
|
content: systemPrompt,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: userPrompt,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
temperature,
|
||||||
|
max_tokens: maxTokens,
|
||||||
|
};
|
||||||
|
|
||||||
|
const init = {
|
||||||
|
headers: {
|
||||||
|
"Content-type": "application/json",
|
||||||
|
Authorization: `Bearer ${key}`,
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
};
|
||||||
|
|
||||||
|
return [url, init];
|
||||||
|
};
|
||||||
|
|
||||||
|
const genClaude = ({
|
||||||
|
text,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
url,
|
||||||
|
key,
|
||||||
|
systemPrompt,
|
||||||
|
userPrompt,
|
||||||
|
model,
|
||||||
|
temperature,
|
||||||
|
maxTokens,
|
||||||
|
}) => {
|
||||||
|
systemPrompt = systemPrompt
|
||||||
|
.replaceAll(INPUT_PLACE_FROM, from)
|
||||||
|
.replaceAll(INPUT_PLACE_TO, to)
|
||||||
|
.replaceAll(INPUT_PLACE_TEXT, text);
|
||||||
|
userPrompt = userPrompt
|
||||||
|
.replaceAll(INPUT_PLACE_FROM, from)
|
||||||
|
.replaceAll(INPUT_PLACE_TO, to)
|
||||||
|
.replaceAll(INPUT_PLACE_TEXT, text);
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
model,
|
||||||
|
system: systemPrompt,
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: userPrompt,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
temperature,
|
||||||
|
max_tokens: maxTokens,
|
||||||
|
};
|
||||||
|
|
||||||
|
const init = {
|
||||||
|
headers: {
|
||||||
|
"Content-type": "application/json",
|
||||||
|
"anthropic-version": "2023-06-01",
|
||||||
|
"x-api-key": key,
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
};
|
||||||
|
|
||||||
|
return [url, init];
|
||||||
|
};
|
||||||
|
|
||||||
|
const genOllama = ({
|
||||||
|
text,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
think,
|
||||||
|
url,
|
||||||
|
key,
|
||||||
|
systemPrompt,
|
||||||
|
userPrompt,
|
||||||
|
model,
|
||||||
|
}) => {
|
||||||
|
systemPrompt = systemPrompt
|
||||||
|
.replaceAll(INPUT_PLACE_FROM, from)
|
||||||
|
.replaceAll(INPUT_PLACE_TO, to)
|
||||||
|
.replaceAll(INPUT_PLACE_TEXT, text);
|
||||||
|
userPrompt = userPrompt
|
||||||
|
.replaceAll(INPUT_PLACE_FROM, from)
|
||||||
|
.replaceAll(INPUT_PLACE_TO, to)
|
||||||
|
.replaceAll(INPUT_PLACE_TEXT, text);
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
model,
|
||||||
|
system: systemPrompt,
|
||||||
|
prompt: userPrompt,
|
||||||
|
think: think,
|
||||||
|
stream: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const init = {
|
||||||
|
headers: {
|
||||||
|
"Content-type": "application/json",
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
};
|
||||||
|
if (key) {
|
||||||
|
init.headers.Authorization = `Bearer ${key}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [url, init];
|
||||||
|
};
|
||||||
|
|
||||||
|
const genCloudflareAI = ({ text, from, to, url, key }) => {
|
||||||
|
const data = {
|
||||||
|
text,
|
||||||
|
source_lang: from,
|
||||||
|
target_lang: to,
|
||||||
|
};
|
||||||
|
|
||||||
|
const init = {
|
||||||
|
headers: {
|
||||||
|
"Content-type": "application/json",
|
||||||
|
Authorization: `Bearer ${key}`,
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
};
|
||||||
|
|
||||||
|
return [url, init];
|
||||||
|
};
|
||||||
|
|
||||||
|
const genCustom = ({ text, from, to, url, key, reqHook }) => {
|
||||||
|
url = url
|
||||||
|
.replaceAll(INPUT_PLACE_URL, url)
|
||||||
|
.replaceAll(INPUT_PLACE_FROM, from)
|
||||||
|
.replaceAll(INPUT_PLACE_TO, to)
|
||||||
|
.replaceAll(INPUT_PLACE_TEXT, text)
|
||||||
|
.replaceAll(INPUT_PLACE_KEY, key);
|
||||||
|
let init = {};
|
||||||
|
|
||||||
|
if (reqHook?.trim()) {
|
||||||
|
interpreter.run(`exports.reqHook = ${reqHook}`);
|
||||||
|
[url, init] = interpreter.exports.reqHook(text, from, to, url, key);
|
||||||
|
return [url, init];
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
text,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
};
|
||||||
|
init = {
|
||||||
|
headers: {
|
||||||
|
"Content-type": "application/json",
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
};
|
||||||
|
if (key) {
|
||||||
|
init.headers.Authorization = `Bearer ${key}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [url, init];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造翻译接口请求参数
|
||||||
|
* @param {*}
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const genTransReq = ({ translator, text, from, to }, apiSetting) => {
|
||||||
|
const args = { text, from, to, ...apiSetting };
|
||||||
|
|
||||||
|
switch (translator) {
|
||||||
|
case OPT_TRANS_DEEPL:
|
||||||
|
case OPT_TRANS_OPENAI:
|
||||||
|
case OPT_TRANS_OPENAI_2:
|
||||||
|
case OPT_TRANS_OPENAI_3:
|
||||||
|
case OPT_TRANS_GEMINI:
|
||||||
|
case OPT_TRANS_GEMINI_2:
|
||||||
|
case OPT_TRANS_CLAUDE:
|
||||||
|
case OPT_TRANS_CLOUDFLAREAI:
|
||||||
|
case OPT_TRANS_OLLAMA:
|
||||||
|
case OPT_TRANS_OLLAMA_2:
|
||||||
|
case OPT_TRANS_OLLAMA_3:
|
||||||
|
case OPT_TRANS_NIUTRANS:
|
||||||
|
case OPT_TRANS_CUSTOMIZE:
|
||||||
|
case OPT_TRANS_CUSTOMIZE_2:
|
||||||
|
case OPT_TRANS_CUSTOMIZE_3:
|
||||||
|
case OPT_TRANS_CUSTOMIZE_4:
|
||||||
|
case OPT_TRANS_CUSTOMIZE_5:
|
||||||
|
args.key = keyPick(translator, args.key, keyMap);
|
||||||
|
break;
|
||||||
|
case OPT_TRANS_DEEPLX:
|
||||||
|
args.url = keyPick(translator, args.url, urlMap);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (translator) {
|
||||||
|
case OPT_TRANS_GOOGLE:
|
||||||
|
return genGoogle(args);
|
||||||
|
case OPT_TRANS_GOOGLE_2:
|
||||||
|
return genGoogle2(args);
|
||||||
|
case OPT_TRANS_MICROSOFT:
|
||||||
|
return genMicrosoft(args);
|
||||||
|
case OPT_TRANS_DEEPL:
|
||||||
|
return genDeepl(args);
|
||||||
|
case OPT_TRANS_DEEPLFREE:
|
||||||
|
return genDeeplFree(args);
|
||||||
|
case OPT_TRANS_DEEPLX:
|
||||||
|
return genDeeplX(args);
|
||||||
|
case OPT_TRANS_NIUTRANS:
|
||||||
|
return genNiuTrans(args);
|
||||||
|
case OPT_TRANS_BAIDU:
|
||||||
|
return genBaidu(args);
|
||||||
|
case OPT_TRANS_TENCENT:
|
||||||
|
return genTencent(args);
|
||||||
|
case OPT_TRANS_VOLCENGINE:
|
||||||
|
return genVolcengine(args);
|
||||||
|
case OPT_TRANS_OPENAI:
|
||||||
|
case OPT_TRANS_OPENAI_2:
|
||||||
|
case OPT_TRANS_OPENAI_3:
|
||||||
|
return genOpenAI(args);
|
||||||
|
case OPT_TRANS_GEMINI:
|
||||||
|
return genGemini(args);
|
||||||
|
case OPT_TRANS_GEMINI_2:
|
||||||
|
return genGemini2(args);
|
||||||
|
case OPT_TRANS_CLAUDE:
|
||||||
|
return genClaude(args);
|
||||||
|
case OPT_TRANS_CLOUDFLAREAI:
|
||||||
|
return genCloudflareAI(args);
|
||||||
|
case OPT_TRANS_OLLAMA:
|
||||||
|
case OPT_TRANS_OLLAMA_2:
|
||||||
|
case OPT_TRANS_OLLAMA_3:
|
||||||
|
return genOllama(args);
|
||||||
|
case OPT_TRANS_CUSTOMIZE:
|
||||||
|
case OPT_TRANS_CUSTOMIZE_2:
|
||||||
|
case OPT_TRANS_CUSTOMIZE_3:
|
||||||
|
case OPT_TRANS_CUSTOMIZE_4:
|
||||||
|
case OPT_TRANS_CUSTOMIZE_5:
|
||||||
|
return genCustom(args);
|
||||||
|
default:
|
||||||
|
throw new Error(`[trans] translator: ${translator} not support`);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,89 +1,225 @@
|
|||||||
import browser from "webextension-polyfill";
|
import browser from "webextension-polyfill";
|
||||||
import {
|
import {
|
||||||
MSG_FETCH,
|
MSG_FETCH,
|
||||||
MSG_FETCH_LIMIT,
|
MSG_GET_HTTPCACHE,
|
||||||
MSG_FETCH_CLEAR,
|
|
||||||
MSG_TRANS_TOGGLE,
|
MSG_TRANS_TOGGLE,
|
||||||
MSG_OPEN_OPTIONS,
|
MSG_OPEN_OPTIONS,
|
||||||
MSG_SAVE_RULE,
|
MSG_SAVE_RULE,
|
||||||
MSG_TRANS_TOGGLE_STYLE,
|
MSG_TRANS_TOGGLE_STYLE,
|
||||||
|
MSG_OPEN_TRANBOX,
|
||||||
|
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,
|
||||||
|
CMD_OPEN_TRANBOX,
|
||||||
|
CLIENT_THUNDERBIRD,
|
||||||
} from "./config";
|
} from "./config";
|
||||||
import { getSettingWithDefault, tryInitDefaultData } from "./libs/storage";
|
import { getSettingWithDefault, tryInitDefaultData } from "./libs/storage";
|
||||||
import { trySyncSettingAndRules } from "./libs/sync";
|
import { trySyncSettingAndRules } from "./libs/sync";
|
||||||
import { fetchData, fetchPool } from "./libs/fetch";
|
import { fetchHandle, getHttpCache } from "./libs/fetch";
|
||||||
import { sendTabMsg } from "./libs/msg";
|
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`,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加右键菜单
|
||||||
|
*/
|
||||||
|
async function addContextMenus(contextMenuType = 1) {
|
||||||
|
// 添加前先删除,避免重复ID的错误
|
||||||
|
try {
|
||||||
|
await browser.contextMenus.removeAll();
|
||||||
|
} catch (err) {
|
||||||
|
kissLog(err, "remove contextMenus");
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (contextMenuType) {
|
||||||
|
case 1:
|
||||||
|
browser.contextMenus.create({
|
||||||
|
id: CMD_TOGGLE_TRANSLATE,
|
||||||
|
title: browser.i18n.getMessage("app_name"),
|
||||||
|
contexts: ["page", "selection"],
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
browser.contextMenus.create({
|
||||||
|
id: CMD_TOGGLE_TRANSLATE,
|
||||||
|
title: browser.i18n.getMessage("toggle_translate"),
|
||||||
|
contexts: ["page", "selection"],
|
||||||
|
});
|
||||||
|
browser.contextMenus.create({
|
||||||
|
id: CMD_TOGGLE_STYLE,
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
async function updateCspRules(csplist = DEFAULT_CSPLIST.join(",\n")) {
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册邮件显示脚本
|
||||||
|
*/
|
||||||
|
async function registerMsgDisplayScript() {
|
||||||
|
await messenger.messageDisplayScripts.register({
|
||||||
|
js: [{ file: "/content.js" }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 插件安装
|
* 插件安装
|
||||||
*/
|
*/
|
||||||
browser.runtime.onInstalled.addListener(() => {
|
browser.runtime.onInstalled.addListener(() => {
|
||||||
tryInitDefaultData();
|
tryInitDefaultData();
|
||||||
|
|
||||||
|
//在thunderbird中注册脚本
|
||||||
|
if (process.env.REACT_APP_CLIENT === CLIENT_THUNDERBIRD) {
|
||||||
|
registerMsgDisplayScript();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 右键菜单
|
||||||
|
addContextMenus();
|
||||||
|
|
||||||
|
// 禁用CSP
|
||||||
|
updateCspRules();
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 浏览器启动
|
* 浏览器启动
|
||||||
*/
|
*/
|
||||||
browser.runtime.onStartup.addListener(async () => {
|
browser.runtime.onStartup.addListener(async () => {
|
||||||
console.log("browser onStartup");
|
|
||||||
|
|
||||||
// 同步数据
|
// 同步数据
|
||||||
await trySyncSettingAndRules();
|
await trySyncSettingAndRules();
|
||||||
|
|
||||||
|
const { clearCache, contextMenuType, subrulesList, csplist } =
|
||||||
|
await getSettingWithDefault();
|
||||||
|
|
||||||
// 清除缓存
|
// 清除缓存
|
||||||
const setting = await getSettingWithDefault();
|
if (clearCache) {
|
||||||
if (setting.clearCache) {
|
|
||||||
tryClearCaches();
|
tryClearCaches();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//在thunderbird中注册脚本
|
||||||
|
if (process.env.REACT_APP_CLIENT === CLIENT_THUNDERBIRD) {
|
||||||
|
registerMsgDisplayScript();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 右键菜单
|
||||||
|
// firefox重启后菜单会消失,故重复添加
|
||||||
|
addContextMenus(contextMenuType);
|
||||||
|
|
||||||
|
// 禁用CSP
|
||||||
|
updateCspRules(csplist);
|
||||||
|
|
||||||
// 同步订阅规则
|
// 同步订阅规则
|
||||||
trySyncAllSubRules(setting);
|
trySyncAllSubRules({ subrulesList });
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 监听消息
|
* 监听消息
|
||||||
*/
|
*/
|
||||||
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:
|
return await fetchHandle(args);
|
||||||
const { input, opts } = args;
|
case MSG_GET_HTTPCACHE:
|
||||||
fetchData(input, opts)
|
const { input, init } = args;
|
||||||
.then((data) => {
|
return await getHttpCache(input, init);
|
||||||
sendResponse({ data });
|
case MSG_OPEN_OPTIONS:
|
||||||
})
|
return await browser.runtime.openOptionsPage();
|
||||||
.catch((error) => {
|
case MSG_SAVE_RULE:
|
||||||
sendResponse({ error: error.message });
|
return await saveRule(args);
|
||||||
});
|
case MSG_INJECT_JS:
|
||||||
break;
|
return await browser.scripting.executeScript({
|
||||||
case MSG_FETCH_LIMIT:
|
target: { tabId: await getCurTabId(), allFrames: true },
|
||||||
const { interval, limit } = args;
|
func: injectInlineJs,
|
||||||
fetchPool.update(interval, limit);
|
args: [args],
|
||||||
sendResponse({ data: "ok" });
|
world: "MAIN",
|
||||||
break;
|
});
|
||||||
case MSG_FETCH_CLEAR:
|
case MSG_INJECT_CSS:
|
||||||
fetchPool.clear();
|
return await browser.scripting.executeScript({
|
||||||
sendResponse({ data: "ok" });
|
target: { tabId: await getCurTabId(), allFrames: true },
|
||||||
break;
|
func: injectInternalCss,
|
||||||
case MSG_OPEN_OPTIONS:
|
args: [args],
|
||||||
browser.runtime.openOptionsPage();
|
world: "MAIN",
|
||||||
break;
|
});
|
||||||
case MSG_SAVE_RULE:
|
case MSG_UPDATE_CSP:
|
||||||
saveRule(args);
|
return await updateCspRules(args);
|
||||||
break;
|
case MSG_CONTEXT_MENUS:
|
||||||
default:
|
return await addContextMenus(args);
|
||||||
sendResponse({ error: `message action is unavailable: ${action}` });
|
case MSG_COMMAND_SHORTCUTS:
|
||||||
}
|
return await browser.commands.getAll();
|
||||||
return true;
|
default:
|
||||||
|
throw new Error(`message action is unavailable: ${action}`);
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 监听快捷键
|
* 监听快捷键
|
||||||
@@ -94,6 +230,9 @@ browser.commands.onCommand.addListener((command) => {
|
|||||||
case CMD_TOGGLE_TRANSLATE:
|
case CMD_TOGGLE_TRANSLATE:
|
||||||
sendTabMsg(MSG_TRANS_TOGGLE);
|
sendTabMsg(MSG_TRANS_TOGGLE);
|
||||||
break;
|
break;
|
||||||
|
case CMD_OPEN_TRANBOX:
|
||||||
|
sendTabMsg(MSG_OPEN_TRANBOX);
|
||||||
|
break;
|
||||||
case CMD_TOGGLE_STYLE:
|
case CMD_TOGGLE_STYLE:
|
||||||
sendTabMsg(MSG_TRANS_TOGGLE_STYLE);
|
sendTabMsg(MSG_TRANS_TOGGLE_STYLE);
|
||||||
break;
|
break;
|
||||||
@@ -103,3 +242,24 @@ browser.commands.onCommand.addListener((command) => {
|
|||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监听右键菜单
|
||||||
|
*/
|
||||||
|
browser.contextMenus.onClicked.addListener(({ menuItemId }) => {
|
||||||
|
switch (menuItemId) {
|
||||||
|
case CMD_TOGGLE_TRANSLATE:
|
||||||
|
sendTabMsg(MSG_TRANS_TOGGLE);
|
||||||
|
break;
|
||||||
|
case CMD_TOGGLE_STYLE:
|
||||||
|
sendTabMsg(MSG_TRANS_TOGGLE_STYLE);
|
||||||
|
break;
|
||||||
|
case CMD_OPEN_TRANBOX:
|
||||||
|
sendTabMsg(MSG_OPEN_TRANBOX);
|
||||||
|
break;
|
||||||
|
case CMD_OPEN_OPTIONS:
|
||||||
|
browser.runtime.openOptionsPage();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
230
src/common.js
230
src/common.js
@@ -8,26 +8,79 @@ import {
|
|||||||
MSG_TRANS_TOGGLE_STYLE,
|
MSG_TRANS_TOGGLE_STYLE,
|
||||||
MSG_TRANS_GETRULE,
|
MSG_TRANS_GETRULE,
|
||||||
MSG_TRANS_PUTRULE,
|
MSG_TRANS_PUTRULE,
|
||||||
|
MSG_OPEN_TRANBOX,
|
||||||
APP_LCNAME,
|
APP_LCNAME,
|
||||||
DEFAULT_TRANBOX_SETTING,
|
DEFAULT_TRANBOX_SETTING,
|
||||||
} from "./config";
|
} from "./config";
|
||||||
import { getRulesWithDefault, getFabWithDefault } from "./libs/storage";
|
import { getFabWithDefault, getSettingWithDefault } from "./libs/storage";
|
||||||
import { Translator } from "./libs/translator";
|
import { Translator } from "./libs/translator";
|
||||||
import { sendIframeMsg, sendParentMsg } from "./libs/iframe";
|
import { isIframe, sendIframeMsg } from "./libs/iframe";
|
||||||
import { matchRule } from "./libs/rules";
|
|
||||||
import Slection from "./views/Selection";
|
import Slection from "./views/Selection";
|
||||||
|
import { touchTapListener } from "./libs/touch";
|
||||||
|
import { debounce, genEventName } from "./libs/utils";
|
||||||
|
import { handlePing, injectScript } from "./libs/gm";
|
||||||
|
import { browser } from "./libs/browser";
|
||||||
|
import { matchRule } from "./libs/rules";
|
||||||
|
import { trySyncAllSubRules } from "./libs/subRules";
|
||||||
|
import { isInBlacklist } from "./libs/blacklist";
|
||||||
|
import inputTranslate from "./libs/inputTranslate";
|
||||||
|
|
||||||
export async function runTranslator(setting) {
|
/**
|
||||||
const href = document.location.href;
|
* 油猴脚本设置页面
|
||||||
const rules = await getRulesWithDefault();
|
*/
|
||||||
const rule = await matchRule(rules, href, setting);
|
function runSettingPage() {
|
||||||
const translator = new Translator(rule, setting);
|
if (GM?.info?.script?.grant?.includes("unsafeWindow")) {
|
||||||
|
unsafeWindow.GM = GM;
|
||||||
return { translator, rule };
|
unsafeWindow.APP_INFO = {
|
||||||
|
name: process.env.REACT_APP_NAME,
|
||||||
|
version: process.env.REACT_APP_VERSION,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const ping = genEventName();
|
||||||
|
window.addEventListener(ping, handlePing);
|
||||||
|
// window.eval(`(${injectScript})("${ping}")`); // eslint-disable-line
|
||||||
|
const script = document.createElement("script");
|
||||||
|
script.textContent = `(${injectScript})("${ping}")`;
|
||||||
|
document.head.append(script);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function runIframe(setting) {
|
/**
|
||||||
let translator;
|
* 插件监听后端事件
|
||||||
|
* @param {*} translator
|
||||||
|
*/
|
||||||
|
function runtimeListener(translator) {
|
||||||
|
browser?.runtime.onMessage.addListener(async ({ action, args }) => {
|
||||||
|
switch (action) {
|
||||||
|
case MSG_TRANS_TOGGLE:
|
||||||
|
translator.toggle();
|
||||||
|
sendIframeMsg(MSG_TRANS_TOGGLE);
|
||||||
|
break;
|
||||||
|
case MSG_TRANS_TOGGLE_STYLE:
|
||||||
|
translator.toggleStyle();
|
||||||
|
sendIframeMsg(MSG_TRANS_TOGGLE_STYLE);
|
||||||
|
break;
|
||||||
|
case MSG_TRANS_GETRULE:
|
||||||
|
break;
|
||||||
|
case MSG_TRANS_PUTRULE:
|
||||||
|
translator.updateRule(args);
|
||||||
|
sendIframeMsg(MSG_TRANS_PUTRULE, args);
|
||||||
|
break;
|
||||||
|
case MSG_OPEN_TRANBOX:
|
||||||
|
window.dispatchEvent(new CustomEvent(MSG_OPEN_TRANBOX));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return { error: `message action is unavailable: ${action}` };
|
||||||
|
}
|
||||||
|
return { rule: translator.rule, setting: translator.setting };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* iframe 页面执行
|
||||||
|
* @param {*} translator
|
||||||
|
*/
|
||||||
|
function runIframe(translator) {
|
||||||
window.addEventListener("message", (e) => {
|
window.addEventListener("message", (e) => {
|
||||||
const { action, args } = e.data || {};
|
const { action, args } = e.data || {};
|
||||||
switch (action) {
|
switch (action) {
|
||||||
@@ -38,26 +91,25 @@ export 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function showFab(translator) {
|
/**
|
||||||
|
* 悬浮按钮
|
||||||
|
* @param {*} translator
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async function showFab(translator) {
|
||||||
const fab = await getFabWithDefault();
|
const fab = await getFabWithDefault();
|
||||||
if (fab.isHide) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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");
|
||||||
@@ -78,20 +130,37 @@ export async function showFab(translator) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showTransbox({
|
/**
|
||||||
tranboxSetting = DEFAULT_TRANBOX_SETTING,
|
* 划词翻译
|
||||||
transApis,
|
* @param {*} param0
|
||||||
}) {
|
* @returns
|
||||||
if (!tranboxSetting?.transOpen) {
|
*/
|
||||||
|
function showTransbox(
|
||||||
|
{
|
||||||
|
contextMenuType,
|
||||||
|
tranboxSetting = DEFAULT_TRANBOX_SETTING,
|
||||||
|
transApis,
|
||||||
|
darkMode,
|
||||||
|
uiLang,
|
||||||
|
langDetector,
|
||||||
|
},
|
||||||
|
{ transSelected }
|
||||||
|
) {
|
||||||
|
if (transSelected === "false") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
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");
|
||||||
const shadowRootElement = document.createElement("div");
|
const shadowRootElement = document.createElement("div");
|
||||||
|
shadowRootElement.classList.add(`KT-transbox`);
|
||||||
|
shadowRootElement.classList.add(`KT-transbox_${darkMode ? "dark" : "light"}`);
|
||||||
shadowContainer.appendChild(emotionRoot);
|
shadowContainer.appendChild(emotionRoot);
|
||||||
shadowContainer.appendChild(shadowRootElement);
|
shadowContainer.appendChild(shadowRootElement);
|
||||||
const cache = createCache({
|
const cache = createCache({
|
||||||
@@ -102,28 +171,101 @@ export function showTransbox({
|
|||||||
ReactDOM.createRoot(shadowRootElement).render(
|
ReactDOM.createRoot(shadowRootElement).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<CacheProvider value={cache}>
|
<CacheProvider value={cache}>
|
||||||
<Slection tranboxSetting={tranboxSetting} transApis={transApis} />
|
<Slection
|
||||||
|
contextMenuType={contextMenuType}
|
||||||
|
tranboxSetting={tranboxSetting}
|
||||||
|
transApis={transApis}
|
||||||
|
uiLang={uiLang}
|
||||||
|
langDetector={langDetector}
|
||||||
|
/>
|
||||||
</CacheProvider>
|
</CacheProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function windowListener(rule) {
|
/**
|
||||||
window.addEventListener("message", (e) => {
|
* 显示错误信息到页面顶部
|
||||||
const { action } = e.data || {};
|
* @param {*} message
|
||||||
switch (action) {
|
*/
|
||||||
case MSG_TRANS_GETRULE:
|
function showErr(message) {
|
||||||
sendIframeMsg(MSG_TRANS_PUTRULE, rule);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function showErr(err) {
|
|
||||||
console.error("[KISS-Translator]", err);
|
|
||||||
const $err = document.createElement("div");
|
const $err = document.createElement("div");
|
||||||
$err.innerText = `KISS-Translator: ${err.message}`;
|
$err.innerText = `KISS-Translator: ${message}`;
|
||||||
$err.style.cssText = "background:red; color:#fff;";
|
$err.style.cssText = "background:red; color:#fff;";
|
||||||
document.body.prepend($err);
|
document.body.prepend($err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监听触屏操作
|
||||||
|
* @param {*} translator
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function touchOperation(translator) {
|
||||||
|
const { touchTranslate = 2 } = translator.setting;
|
||||||
|
if (touchTranslate === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTap = debounce(() => {
|
||||||
|
translator.toggle();
|
||||||
|
sendIframeMsg(MSG_TRANS_TOGGLE);
|
||||||
|
});
|
||||||
|
touchTapListener(handleTap, touchTranslate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 入口函数
|
||||||
|
*/
|
||||||
|
export async function run(isUserscript = false) {
|
||||||
|
try {
|
||||||
|
const href = document.location.href;
|
||||||
|
|
||||||
|
// 设置页面
|
||||||
|
if (
|
||||||
|
isUserscript &&
|
||||||
|
(href.includes(process.env.REACT_APP_OPTIONSPAGE_DEV) ||
|
||||||
|
href.includes(process.env.REACT_APP_OPTIONSPAGE))
|
||||||
|
) {
|
||||||
|
runSettingPage();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取设置信息
|
||||||
|
const setting = await getSettingWithDefault();
|
||||||
|
|
||||||
|
// 黑名单
|
||||||
|
if (isInBlacklist(href, setting)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 翻译网页
|
||||||
|
const rule = await matchRule(href, setting);
|
||||||
|
const translator = new Translator(rule, setting);
|
||||||
|
|
||||||
|
// 适配iframe
|
||||||
|
if (isIframe) {
|
||||||
|
runIframe(translator);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听消息
|
||||||
|
!isUserscript && runtimeListener(translator);
|
||||||
|
|
||||||
|
// 输入框翻译
|
||||||
|
inputTranslate(setting);
|
||||||
|
|
||||||
|
// 划词翻译
|
||||||
|
showTransbox(setting, rule);
|
||||||
|
|
||||||
|
// 浮球按钮
|
||||||
|
await showFab(translator);
|
||||||
|
|
||||||
|
// 触屏操作
|
||||||
|
touchOperation(translator);
|
||||||
|
|
||||||
|
// 同步订阅规则
|
||||||
|
isUserscript && (await trySyncAllSubRules(setting));
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[KISS-Translator]", err);
|
||||||
|
showErr(err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -42,58 +42,95 @@ const customApiLangs = `["en", "English - English"],
|
|||||||
["vi", "Vietnamese - Tiếng Việt"],
|
["vi", "Vietnamese - Tiếng Việt"],
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const customApiHelpZH = `/// 自定义翻译源接口说明
|
const customApiHelpZH = `// 请求数据默认格式
|
||||||
|
|
||||||
// 请求(Request)数据将按下面规范发送
|
|
||||||
{
|
{
|
||||||
url: {{YOUR_URL}},
|
"url": "{{url}}",
|
||||||
method: "POST",
|
"method": "POST",
|
||||||
headers: {
|
"headers": {
|
||||||
"Content-type": "application/json",
|
"Content-type": "application/json",
|
||||||
"Authorization": "Bearer {{YOUR_KEY}}",
|
"Authorization": "Bearer {{key}}"
|
||||||
|
},
|
||||||
|
"body": {
|
||||||
|
"text": "{{text}}", // 待翻译文字
|
||||||
|
"from": "{{from}}", // 文字的语言(可能为空)
|
||||||
|
"to": "{{to}}", // 目标语言
|
||||||
},
|
},
|
||||||
body: {
|
|
||||||
text: "", // 需要翻译的文字
|
|
||||||
from: "", // 源语言,可能为空,表示需要接口自动识别语言
|
|
||||||
to: "", // 目标语言
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 返回(Response)数据需符合下面的JSON规范
|
|
||||||
|
// 返回数据默认格式
|
||||||
{
|
{
|
||||||
text: "", // 翻译后的文字
|
text: "", // 翻译后的文字
|
||||||
from: "", // 识别的源语言
|
from: "", // 识别的源语言
|
||||||
to: "", // 目标语言(可选)
|
to: "", // 目标语言(可选)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Hook 范例
|
||||||
|
// URL
|
||||||
|
https://translate.googleapis.com/translate_a/single?client=gtx&dj=1&dt=t&ie=UTF-8&q={{text}}&sl=en&tl=zh-CN
|
||||||
|
|
||||||
|
// Request Hook
|
||||||
|
(text, from, to, url, key) => [url, {
|
||||||
|
headers: {
|
||||||
|
"Content-type": "application/json",
|
||||||
|
},
|
||||||
|
method: "GET",
|
||||||
|
body: null,
|
||||||
|
}]
|
||||||
|
|
||||||
|
// Response Hook
|
||||||
|
// 其中返回数组第一个值表示译文字符串,第二个值为布尔值,表示原文语言与目标语言是否相同
|
||||||
|
(res, text, from, to) => [res.sentences.map((item) => item.trans).join(" "), to === res.src]
|
||||||
|
|
||||||
|
|
||||||
// 支持的语言代码如下
|
// 支持的语言代码如下
|
||||||
${customApiLangs}
|
${customApiLangs}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const customApiHelpEN = `/// Custom translation source interface description
|
const customApiHelpEN = `// Default request
|
||||||
|
|
||||||
// Request data will be sent according to the following specifications
|
|
||||||
{
|
{
|
||||||
url: {{YOUR_URL}},
|
"url": "{{url}}",
|
||||||
method: "POST",
|
"method": "POST",
|
||||||
headers: {
|
"headers": {
|
||||||
"Content-type": "application/json",
|
"Content-type": "application/json",
|
||||||
"Authorization": "Bearer {{YOUR_KEY}}",
|
"Authorization": "Bearer {{key}}"
|
||||||
|
},
|
||||||
|
"body": {
|
||||||
|
"text": "{{text}}", // Text to be translated
|
||||||
|
"from": "{{from}}", // The language of the text (may be empty)
|
||||||
|
"to": "{{to}}", // Target language
|
||||||
},
|
},
|
||||||
body: {
|
|
||||||
text: "", // text to be translated
|
|
||||||
from: "", // Source language, may be empty
|
|
||||||
to: "", // Target language
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The returned data must conform to the following JSON specification
|
|
||||||
|
// Default response
|
||||||
{
|
{
|
||||||
text: "", // translated text
|
text: "", // translated text
|
||||||
from: "", // Recognized source language
|
from: "", // Recognized source language
|
||||||
to: "", // Target language (optional)
|
to: "", // Target language (optional)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Hook Example
|
||||||
|
// URL
|
||||||
|
https://translate.googleapis.com/translate_a/single?client=gtx&dj=1&dt=t&ie=UTF-8&q={{text}}&sl=en&tl=zh-CN
|
||||||
|
|
||||||
|
// Request Hook
|
||||||
|
(text, from, to, url, key) => [url, {
|
||||||
|
headers: {
|
||||||
|
"Content-type": "application/json",
|
||||||
|
},
|
||||||
|
method: "GET",
|
||||||
|
body: null,
|
||||||
|
}]
|
||||||
|
|
||||||
|
// Response Hook
|
||||||
|
// In the returned array, the first value is the translated string, while the second value is a boolean
|
||||||
|
// that indicates whether the source language is the same as the target language.
|
||||||
|
(res, text, from, to) => [res.sentences.map((item) => item.trans).join(" "), to === res.src]
|
||||||
|
|
||||||
|
|
||||||
// The supported language codes are as follows
|
// The supported language codes are as follows
|
||||||
${customApiLangs}
|
${customApiLangs}
|
||||||
`;
|
`;
|
||||||
@@ -112,8 +149,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 +197,44 @@ 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)`,
|
||||||
|
},
|
||||||
|
if_think: {
|
||||||
|
zh: `启用或禁用模型的深度思考能力`,
|
||||||
|
en: `Enable or disable the model’s thinking behavior `,
|
||||||
|
},
|
||||||
|
think: {
|
||||||
|
zh: `启用深度思考`,
|
||||||
|
en: `enable thinking`,
|
||||||
|
},
|
||||||
|
nothink: {
|
||||||
|
zh: `禁用深度思考`,
|
||||||
|
en: `disable thinking`,
|
||||||
|
},
|
||||||
|
think_ignore: {
|
||||||
|
zh: `忽略以下模型的<think>输出,逗号(,)分割,当模型支持思考但ollama不支持时需要填写本参数`,
|
||||||
|
en: `Ignore the <think> block for the following models, comma (,) separated`,
|
||||||
},
|
},
|
||||||
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)`,
|
||||||
|
},
|
||||||
|
http_timeout: {
|
||||||
|
zh: `请求超时时间 (5000-30000ms)`,
|
||||||
|
en: `Request Timeout Time (5000-30000ms)`,
|
||||||
},
|
},
|
||||||
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 +244,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: {
|
||||||
@@ -215,13 +280,21 @@ export const I18N = {
|
|||||||
zh: `目标语言`,
|
zh: `目标语言`,
|
||||||
en: `Target Language`,
|
en: `Target Language`,
|
||||||
},
|
},
|
||||||
|
to_lang2: {
|
||||||
|
zh: `第二目标语言`,
|
||||||
|
en: `Target Language 2`,
|
||||||
|
},
|
||||||
|
to_lang2_helper: {
|
||||||
|
zh: `设定后,与目标语言产生互译效果,但依赖远程语言识别。`,
|
||||||
|
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: `样式颜色`,
|
||||||
@@ -295,13 +368,25 @@ 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: `涉及隐私数据的同步请谨慎选择第三方同步服务,建议自行搭建 kiss-worker 或 WebDAV 服务。`,
|
||||||
|
en: `When synchronizing data that involves privacy, please be cautious about choosing third-party sync services. It is recommended to set up your own sync service using kiss-worker or WebDAV.`,
|
||||||
|
},
|
||||||
|
sync_warn_2: {
|
||||||
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.`,
|
||||||
},
|
},
|
||||||
about_sync_api: {
|
about_sync_api: {
|
||||||
zh: `查看关于数据同步接口部署`,
|
zh: `自建kiss-wroker数据同步服务`,
|
||||||
en: `View About Data Synchronization Interface Deployment`,
|
en: `Self-hosting a Kiss-worker data sync service`,
|
||||||
|
},
|
||||||
|
about_api: {
|
||||||
|
zh: `暂未列出的接口,理论上都可以通过自定义接口的形式支持。`,
|
||||||
|
en: `Interfaces that have not yet been launched can theoretically be supported through custom interfaces.`,
|
||||||
},
|
},
|
||||||
about_api_proxy: {
|
about_api_proxy: {
|
||||||
zh: `查看自建一个翻译接口代理`,
|
zh: `查看自建一个翻译接口代理`,
|
||||||
@@ -335,6 +420,10 @@ export const I18N = {
|
|||||||
zh: `高亮`,
|
zh: `高亮`,
|
||||||
en: `Highlight`,
|
en: `Highlight`,
|
||||||
},
|
},
|
||||||
|
blockquote: {
|
||||||
|
zh: `引用`,
|
||||||
|
en: `Blockquote`,
|
||||||
|
},
|
||||||
diy_style: {
|
diy_style: {
|
||||||
zh: `自定义样式`,
|
zh: `自定义样式`,
|
||||||
en: `Custom Style`,
|
en: `Custom Style`,
|
||||||
@@ -344,20 +433,20 @@ export const I18N = {
|
|||||||
en: `Follow the syntax of "CSS"`,
|
en: `Follow the syntax of "CSS"`,
|
||||||
},
|
},
|
||||||
setting: {
|
setting: {
|
||||||
zh: `设置 (Alt+O)`,
|
zh: `设置`,
|
||||||
en: `Setting (Alt+O)`,
|
en: `Setting`,
|
||||||
},
|
},
|
||||||
pattern: {
|
pattern: {
|
||||||
zh: `匹配网址`,
|
zh: `匹配网址`,
|
||||||
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 can be 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: `开启翻译`,
|
||||||
@@ -375,6 +464,62 @@ export const I18N = {
|
|||||||
zh: `选择器`,
|
zh: `选择器`,
|
||||||
en: `Selector`,
|
en: `Selector`,
|
||||||
},
|
},
|
||||||
|
keep_selector: {
|
||||||
|
zh: `保留元素选择器`,
|
||||||
|
en: `Keep unchanged selector`,
|
||||||
|
},
|
||||||
|
keep_selector_helper: {
|
||||||
|
zh: `1、遵循CSS选择器语法。`,
|
||||||
|
en: `1. Follow CSS selector syntax.`,
|
||||||
|
},
|
||||||
|
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: {
|
||||||
|
zh: `根选择器`,
|
||||||
|
en: `Root Selector`,
|
||||||
|
},
|
||||||
|
fixer_function: {
|
||||||
|
zh: `修复函数`,
|
||||||
|
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`,
|
||||||
@@ -383,6 +528,10 @@ export const I18N = {
|
|||||||
zh: `导出`,
|
zh: `导出`,
|
||||||
en: `Export`,
|
en: `Export`,
|
||||||
},
|
},
|
||||||
|
export_translation: {
|
||||||
|
zh: `导出释义`,
|
||||||
|
en: `Export Translation`,
|
||||||
|
},
|
||||||
error_cant_be_blank: {
|
error_cant_be_blank: {
|
||||||
zh: `不能为空`,
|
zh: `不能为空`,
|
||||||
en: `Can not be blank`,
|
en: `Can not be blank`,
|
||||||
@@ -539,6 +688,18 @@ export const I18N = {
|
|||||||
zh: `隐藏悬浮按钮`,
|
zh: `隐藏悬浮按钮`,
|
||||||
en: `Hide Fab Button`,
|
en: `Hide Fab Button`,
|
||||||
},
|
},
|
||||||
|
hide_tran_button: {
|
||||||
|
zh: `隐藏翻译按钮`,
|
||||||
|
en: `Hide Translate Button`,
|
||||||
|
},
|
||||||
|
hide_click_away: {
|
||||||
|
zh: `点击外部关闭弹窗`,
|
||||||
|
en: `Click outside to close the pop-up window`,
|
||||||
|
},
|
||||||
|
use_simple_style: {
|
||||||
|
zh: `使用简洁界面`,
|
||||||
|
en: `Use a simple interface`,
|
||||||
|
},
|
||||||
show: {
|
show: {
|
||||||
zh: `显示`,
|
zh: `显示`,
|
||||||
en: `Show`,
|
en: `Show`,
|
||||||
@@ -620,16 +781,24 @@ export const I18N = {
|
|||||||
en: `Use Selection Translate`,
|
en: `Use Selection Translate`,
|
||||||
},
|
},
|
||||||
trigger_tranbox_shortcut: {
|
trigger_tranbox_shortcut: {
|
||||||
zh: `显示翻译框快捷键`,
|
zh: `显示翻译框/翻译选中文字快捷键`,
|
||||||
en: `Toggle Translate Box Shortcut`,
|
en: `Open Translate Popup/Translate Selected Shortcut`,
|
||||||
},
|
},
|
||||||
tranbtn_offset_x: {
|
tranbtn_offset_x: {
|
||||||
zh: `翻译按钮偏移X(0-100)`,
|
zh: `翻译按钮偏移X(±200)`,
|
||||||
en: `Translate Button Offset X (0-100)`,
|
en: `Translate Button Offset X (±200)`,
|
||||||
},
|
},
|
||||||
tranbtn_offset_y: {
|
tranbtn_offset_y: {
|
||||||
zh: `翻译按钮偏移Y(0-100)`,
|
zh: `翻译按钮偏移Y(±200)`,
|
||||||
en: `Translate Button Offset Y (0-100)`,
|
en: `Translate Button Offset Y (±200)`,
|
||||||
|
},
|
||||||
|
tranbox_offset_x: {
|
||||||
|
zh: `翻译框偏移X(±200)`,
|
||||||
|
en: `Translate Box Offset X (±200)`,
|
||||||
|
},
|
||||||
|
tranbox_offset_y: {
|
||||||
|
zh: `翻译框偏移Y(±200)`,
|
||||||
|
en: `Translate Box Offset Y (±200)`,
|
||||||
},
|
},
|
||||||
translated_text: {
|
translated_text: {
|
||||||
zh: `译文`,
|
zh: `译文`,
|
||||||
@@ -643,4 +812,184 @@ export const I18N = {
|
|||||||
zh: `收藏词汇`,
|
zh: `收藏词汇`,
|
||||||
en: `Favorite Words`,
|
en: `Favorite Words`,
|
||||||
},
|
},
|
||||||
|
touch_setting: {
|
||||||
|
zh: `触屏设置`,
|
||||||
|
en: `Touch Setting`,
|
||||||
|
},
|
||||||
|
touch_translate_shortcut: {
|
||||||
|
zh: `触屏翻译快捷方式`,
|
||||||
|
en: `Touch Translate Shortcut`,
|
||||||
|
},
|
||||||
|
touch_tap_0: {
|
||||||
|
zh: `禁用`,
|
||||||
|
en: `Disable`,
|
||||||
|
},
|
||||||
|
touch_tap_2: {
|
||||||
|
zh: `双指轻触`,
|
||||||
|
en: `Two finger tap`,
|
||||||
|
},
|
||||||
|
touch_tap_3: {
|
||||||
|
zh: `三指轻触`,
|
||||||
|
en: `Three finger tap`,
|
||||||
|
},
|
||||||
|
touch_tap_4: {
|
||||||
|
zh: `四指轻触`,
|
||||||
|
en: `Four finger tap`,
|
||||||
|
},
|
||||||
|
translate_blacklist: {
|
||||||
|
zh: `禁用翻译名单`,
|
||||||
|
en: `Translate Blacklist`,
|
||||||
|
},
|
||||||
|
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: `不翻译的语言`,
|
||||||
|
en: `Disable Languages`,
|
||||||
|
},
|
||||||
|
skip_langs_helper: {
|
||||||
|
zh: `此功能依赖准确的语言检测,建议启用远程语言检测。`,
|
||||||
|
en: `This feature relies on accurate language detection. It is recommended to enable remote language detection.`,
|
||||||
|
},
|
||||||
|
context_menus: {
|
||||||
|
zh: `右键菜单`,
|
||||||
|
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: {
|
||||||
|
zh: `支持用换行或英文逗号“,”分隔,轮询调用。`,
|
||||||
|
en: `Supports 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`,
|
||||||
|
},
|
||||||
|
less: {
|
||||||
|
zh: `更少`,
|
||||||
|
en: `Less`,
|
||||||
|
},
|
||||||
|
fixer_selector: {
|
||||||
|
zh: `网页修复选择器`,
|
||||||
|
en: `Fixer Selector`,
|
||||||
|
},
|
||||||
|
reg_niutrans: {
|
||||||
|
zh: `获取小牛翻译密钥【简约翻译专属新用户注册赠送300万字符】`,
|
||||||
|
en: `Get NiuTrans APIKey [KISS Translator Exclusive New User Registration Free 3 Million Characters]`,
|
||||||
|
},
|
||||||
|
trigger_mode: {
|
||||||
|
zh: `触发方式`,
|
||||||
|
en: `Trigger Mode`,
|
||||||
|
},
|
||||||
|
trigger_click: {
|
||||||
|
zh: `点击触发`,
|
||||||
|
en: `Click Trigger`,
|
||||||
|
},
|
||||||
|
trigger_hover: {
|
||||||
|
zh: `鼠标悬停触发`,
|
||||||
|
en: `Hover Trigger`,
|
||||||
|
},
|
||||||
|
trigger_select: {
|
||||||
|
zh: `选中触发`,
|
||||||
|
en: `Select Trigger`,
|
||||||
|
},
|
||||||
|
extend_styles: {
|
||||||
|
zh: `附加样式`,
|
||||||
|
en: `Extend Styles`,
|
||||||
|
},
|
||||||
|
custom_option: {
|
||||||
|
zh: `自定义选项`,
|
||||||
|
en: `Custom Option`,
|
||||||
|
},
|
||||||
|
translate_selected_text: {
|
||||||
|
zh: `翻译选中文字`,
|
||||||
|
en: `Translate Selected Text`,
|
||||||
|
},
|
||||||
|
toggle_style: {
|
||||||
|
zh: `切换样式`,
|
||||||
|
en: `Toggle Style`,
|
||||||
|
},
|
||||||
|
open_menu: {
|
||||||
|
zh: `打开弹窗菜单`,
|
||||||
|
en: `Open Popup Menu`,
|
||||||
|
},
|
||||||
|
open_setting: {
|
||||||
|
zh: `打开设置`,
|
||||||
|
en: `Open Setting`,
|
||||||
|
},
|
||||||
|
follow_selection: {
|
||||||
|
zh: `翻译框跟随选中文本`,
|
||||||
|
en: `Transbox Follow Selection`,
|
||||||
|
},
|
||||||
|
translate_start_hook: {
|
||||||
|
zh: `翻译开始钩子函数`,
|
||||||
|
en: `Translate Start Hook`,
|
||||||
|
},
|
||||||
|
translate_start_hook_helper: {
|
||||||
|
zh: `翻译开始时运行,入参为: 翻译节点,原文文本。`,
|
||||||
|
en: `Run when translation starts, the input parameters are: translation node, original text.`,
|
||||||
|
},
|
||||||
|
translate_end_hook: {
|
||||||
|
zh: `翻译完成钩子函数`,
|
||||||
|
en: `Translate End Hook`,
|
||||||
|
},
|
||||||
|
translate_end_hook_helper: {
|
||||||
|
zh: `翻译完成时运行,入参为: 翻译节点,原文文本,译文文本,保留元素。`,
|
||||||
|
en: `Run when the translation is completed, the input parameters are: translation node, original text, translation text, retained elements.`,
|
||||||
|
},
|
||||||
|
translate_remove_hook: {
|
||||||
|
zh: `翻译移除钩子函数`,
|
||||||
|
en: `Translate Removed Hook`,
|
||||||
|
},
|
||||||
|
translate_remove_hook_helper: {
|
||||||
|
zh: `翻译移除时运行,入参为: 翻译节点。`,
|
||||||
|
en: `Run when translation is removed, the input parameters are: translation node.`,
|
||||||
|
},
|
||||||
|
english_dict: {
|
||||||
|
zh: `英文词典`,
|
||||||
|
en: `English Dictionary`,
|
||||||
|
},
|
||||||
|
api_name: {
|
||||||
|
zh: `接口名称`,
|
||||||
|
en: `API Name`,
|
||||||
|
},
|
||||||
|
is_disabled: {
|
||||||
|
zh: `是否禁用`,
|
||||||
|
en: `Is Disabled`,
|
||||||
|
},
|
||||||
|
translate_selected: {
|
||||||
|
zh: `是否启用划词翻译`,
|
||||||
|
en: `If translate selected`,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
DEFAULT_SELECTOR,
|
DEFAULT_SELECTOR,
|
||||||
|
DEFAULT_KEEP_SELECTOR,
|
||||||
GLOBAL_KEY,
|
GLOBAL_KEY,
|
||||||
REMAIN_KEY,
|
REMAIN_KEY,
|
||||||
SHADOW_KEY,
|
SHADOW_KEY,
|
||||||
@@ -27,18 +28,24 @@ 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";
|
||||||
export const CMD_OPEN_OPTIONS = "openOptions";
|
export const CMD_OPEN_OPTIONS = "openOptions";
|
||||||
|
export const CMD_OPEN_TRANBOX = "openTranbox";
|
||||||
|
|
||||||
export const CLIENT_WEB = "web";
|
export const CLIENT_WEB = "web";
|
||||||
export const CLIENT_CHROME = "chrome";
|
export const CLIENT_CHROME = "chrome";
|
||||||
export const CLIENT_EDGE = "edge";
|
export const CLIENT_EDGE = "edge";
|
||||||
export const CLIENT_FIREFOX = "firefox";
|
export const CLIENT_FIREFOX = "firefox";
|
||||||
export const CLIENT_USERSCRIPT = "userscript";
|
export const CLIENT_USERSCRIPT = "userscript";
|
||||||
export const CLIENT_EXTS = [CLIENT_CHROME, CLIENT_EDGE, CLIENT_FIREFOX];
|
export const CLIENT_THUNDERBIRD = "thunderbird";
|
||||||
|
export const CLIENT_EXTS = [
|
||||||
|
CLIENT_CHROME,
|
||||||
|
CLIENT_EDGE,
|
||||||
|
CLIENT_FIREFOX,
|
||||||
|
CLIENT_THUNDERBIRD,
|
||||||
|
];
|
||||||
|
|
||||||
export const KV_RULES_KEY = "kiss-rules.json";
|
export const KV_RULES_KEY = "kiss-rules.json";
|
||||||
export const KV_WORDS_KEY = "kiss-words.json";
|
export const KV_WORDS_KEY = "kiss-words.json";
|
||||||
@@ -50,15 +57,20 @@ export const KV_SALT_SHARE = "KISS-Translator-SHARE";
|
|||||||
export const CACHE_NAME = `${APP_NAME}_cache`;
|
export const CACHE_NAME = `${APP_NAME}_cache`;
|
||||||
|
|
||||||
export const MSG_FETCH = "fetch";
|
export const MSG_FETCH = "fetch";
|
||||||
export const MSG_FETCH_LIMIT = "fetch_limit";
|
export const MSG_GET_HTTPCACHE = "get_httpcache";
|
||||||
export const MSG_FETCH_CLEAR = "fetch_clear";
|
|
||||||
export const MSG_OPEN_OPTIONS = "open_options";
|
export const MSG_OPEN_OPTIONS = "open_options";
|
||||||
export const MSG_SAVE_RULE = "save_rule";
|
export const MSG_SAVE_RULE = "save_rule";
|
||||||
export const MSG_TRANS_TOGGLE = "trans_toggle";
|
export const MSG_TRANS_TOGGLE = "trans_toggle";
|
||||||
export const MSG_TRANS_TOGGLE_STYLE = "trans_toggle_style";
|
export const MSG_TRANS_TOGGLE_STYLE = "trans_toggle_style";
|
||||||
|
export const MSG_OPEN_TRANBOX = "open_tranbox";
|
||||||
export const MSG_TRANS_GETRULE = "trans_getrule";
|
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_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";
|
||||||
@@ -72,36 +84,96 @@ export const URL_RAW_PREFIX =
|
|||||||
"https://raw.githubusercontent.com/fishjar/kiss-translator/master";
|
"https://raw.githubusercontent.com/fishjar/kiss-translator/master";
|
||||||
|
|
||||||
export const URL_CACHE_TRAN = `https://${APP_LCNAME}/translate`;
|
export const URL_CACHE_TRAN = `https://${APP_LCNAME}/translate`;
|
||||||
|
|
||||||
|
// api.cognitive.microsofttranslator.com
|
||||||
export const URL_MICROSOFT_TRAN =
|
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_MICROSOFT_LANGDETECT =
|
||||||
|
"https://api-edge.cognitive.microsofttranslator.com/detect?api-version=3.0";
|
||||||
|
|
||||||
|
export const URL_GOOGLE_TRAN =
|
||||||
|
"https://translate.googleapis.com/translate_a/single";
|
||||||
|
export const URL_GOOGLE_TRAN2 =
|
||||||
|
"https://translate-pa.googleapis.com/v1/translateHtml";
|
||||||
|
export const DEFAULT_GOOGLE_API_KEY = "AIzaSyATBXajvzQLTDHEQbcpq0Ihe0vWDHmO520";
|
||||||
|
|
||||||
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";
|
||||||
|
export const URL_VOLCENGINE_TRAN =
|
||||||
|
"https://translate.volcengine.com/crx/translate/v1";
|
||||||
|
export const URL_NIUTRANS_REG =
|
||||||
|
"https://niutrans.com/login?active=3&userSource=kiss-translator";
|
||||||
|
|
||||||
|
export const DEFAULT_USER_AGENT =
|
||||||
|
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36";
|
||||||
|
|
||||||
|
export const OPT_DICT_BAIDU = "Baidu";
|
||||||
|
|
||||||
export const OPT_TRANS_GOOGLE = "Google";
|
export const OPT_TRANS_GOOGLE = "Google";
|
||||||
|
export const OPT_TRANS_GOOGLE_2 = "Google2";
|
||||||
export const OPT_TRANS_MICROSOFT = "Microsoft";
|
export const OPT_TRANS_MICROSOFT = "Microsoft";
|
||||||
export const OPT_TRANS_DEEPL = "DeepL";
|
export const OPT_TRANS_DEEPL = "DeepL";
|
||||||
export const OPT_TRANS_DEEPLX = "DeepLX";
|
export const OPT_TRANS_DEEPLX = "DeepLX";
|
||||||
export const OPT_TRANS_DEEPLFREE = "DeepLFree";
|
export const OPT_TRANS_DEEPLFREE = "DeepLFree";
|
||||||
|
export const OPT_TRANS_NIUTRANS = "NiuTrans";
|
||||||
export const OPT_TRANS_BAIDU = "Baidu";
|
export const OPT_TRANS_BAIDU = "Baidu";
|
||||||
export const OPT_TRANS_TENCENT = "Tencent";
|
export const OPT_TRANS_TENCENT = "Tencent";
|
||||||
|
export const OPT_TRANS_VOLCENGINE = "Volcengine";
|
||||||
export const OPT_TRANS_OPENAI = "OpenAI";
|
export const OPT_TRANS_OPENAI = "OpenAI";
|
||||||
|
export const OPT_TRANS_OPENAI_2 = "OpenAI2";
|
||||||
|
export const OPT_TRANS_OPENAI_3 = "OpenAI3";
|
||||||
|
export const OPT_TRANS_GEMINI = "Gemini";
|
||||||
|
export const OPT_TRANS_GEMINI_2 = "Gemini2";
|
||||||
|
export const OPT_TRANS_CLAUDE = "Claude";
|
||||||
export const OPT_TRANS_CLOUDFLAREAI = "CloudflareAI";
|
export const OPT_TRANS_CLOUDFLAREAI = "CloudflareAI";
|
||||||
|
export const OPT_TRANS_OLLAMA = "Ollama";
|
||||||
|
export const OPT_TRANS_OLLAMA_2 = "Ollama2";
|
||||||
|
export const OPT_TRANS_OLLAMA_3 = "Ollama3";
|
||||||
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_GOOGLE_2,
|
||||||
OPT_TRANS_MICROSOFT,
|
OPT_TRANS_MICROSOFT,
|
||||||
|
OPT_TRANS_BAIDU,
|
||||||
|
OPT_TRANS_TENCENT,
|
||||||
|
OPT_TRANS_VOLCENGINE,
|
||||||
OPT_TRANS_DEEPL,
|
OPT_TRANS_DEEPL,
|
||||||
OPT_TRANS_DEEPLFREE,
|
OPT_TRANS_DEEPLFREE,
|
||||||
OPT_TRANS_DEEPLX,
|
OPT_TRANS_DEEPLX,
|
||||||
|
OPT_TRANS_NIUTRANS,
|
||||||
|
OPT_TRANS_OPENAI,
|
||||||
|
OPT_TRANS_OPENAI_2,
|
||||||
|
OPT_TRANS_OPENAI_3,
|
||||||
|
OPT_TRANS_GEMINI,
|
||||||
|
OPT_TRANS_GEMINI_2,
|
||||||
|
OPT_TRANS_CLAUDE,
|
||||||
|
OPT_TRANS_CLOUDFLAREAI,
|
||||||
|
OPT_TRANS_OLLAMA,
|
||||||
|
OPT_TRANS_OLLAMA_2,
|
||||||
|
OPT_TRANS_OLLAMA_3,
|
||||||
|
OPT_TRANS_CUSTOMIZE,
|
||||||
|
OPT_TRANS_CUSTOMIZE_2,
|
||||||
|
OPT_TRANS_CUSTOMIZE_3,
|
||||||
|
OPT_TRANS_CUSTOMIZE_4,
|
||||||
|
OPT_TRANS_CUSTOMIZE_5,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const OPT_LANGDETECTOR_ALL = [
|
||||||
|
OPT_TRANS_GOOGLE,
|
||||||
|
OPT_TRANS_MICROSOFT,
|
||||||
OPT_TRANS_BAIDU,
|
OPT_TRANS_BAIDU,
|
||||||
OPT_TRANS_TENCENT,
|
OPT_TRANS_TENCENT,
|
||||||
OPT_TRANS_OPENAI,
|
|
||||||
OPT_TRANS_CLOUDFLAREAI,
|
|
||||||
OPT_TRANS_CUSTOMIZE,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export const OPT_LANGS_TO = [
|
export const OPT_LANGS_TO = [
|
||||||
@@ -146,6 +218,7 @@ export const OPT_LANGS_TO = [
|
|||||||
export const OPT_LANGS_FROM = [["auto", "Auto-detect"], ...OPT_LANGS_TO];
|
export const OPT_LANGS_FROM = [["auto", "Auto-detect"], ...OPT_LANGS_TO];
|
||||||
export const OPT_LANGS_SPECIAL = {
|
export const OPT_LANGS_SPECIAL = {
|
||||||
[OPT_TRANS_GOOGLE]: new Map(OPT_LANGS_FROM.map(([key]) => [key, key])),
|
[OPT_TRANS_GOOGLE]: new Map(OPT_LANGS_FROM.map(([key]) => [key, key])),
|
||||||
|
[OPT_TRANS_GOOGLE_2]: new Map(OPT_LANGS_FROM.map(([key]) => [key, key])),
|
||||||
[OPT_TRANS_MICROSOFT]: new Map([
|
[OPT_TRANS_MICROSOFT]: new Map([
|
||||||
...OPT_LANGS_FROM.map(([key]) => [key, key]),
|
...OPT_LANGS_FROM.map(([key]) => [key, key]),
|
||||||
["auto", ""],
|
["auto", ""],
|
||||||
@@ -166,10 +239,22 @@ export const OPT_LANGS_SPECIAL = {
|
|||||||
]),
|
]),
|
||||||
[OPT_TRANS_DEEPLX]: new Map([
|
[OPT_TRANS_DEEPLX]: new Map([
|
||||||
...OPT_LANGS_FROM.map(([key]) => [key, key.toUpperCase()]),
|
...OPT_LANGS_FROM.map(([key]) => [key, key.toUpperCase()]),
|
||||||
["auto", ""],
|
["auto", "auto"],
|
||||||
["zh-CN", "ZH"],
|
["zh-CN", "ZH"],
|
||||||
["zh-TW", "ZH"],
|
["zh-TW", "ZH"],
|
||||||
]),
|
]),
|
||||||
|
[OPT_TRANS_NIUTRANS]: new Map([
|
||||||
|
...OPT_LANGS_FROM.map(([key]) => [key, key]),
|
||||||
|
["auto", "auto"],
|
||||||
|
["zh-CN", "zh"],
|
||||||
|
["zh-TW", "cht"],
|
||||||
|
]),
|
||||||
|
[OPT_TRANS_VOLCENGINE]: new Map([
|
||||||
|
...OPT_LANGS_FROM.map(([key]) => [key, key]),
|
||||||
|
["auto", "auto"],
|
||||||
|
["zh-CN", "zh"],
|
||||||
|
["zh-TW", "zh-Hant"],
|
||||||
|
]),
|
||||||
[OPT_TRANS_BAIDU]: new Map([
|
[OPT_TRANS_BAIDU]: new Map([
|
||||||
...OPT_LANGS_FROM.map(([key]) => [key, key]),
|
...OPT_LANGS_FROM.map(([key]) => [key, key]),
|
||||||
["zh-CN", "zh"],
|
["zh-CN", "zh"],
|
||||||
@@ -222,6 +307,30 @@ export const OPT_LANGS_SPECIAL = {
|
|||||||
[OPT_TRANS_OPENAI]: new Map(
|
[OPT_TRANS_OPENAI]: new Map(
|
||||||
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
||||||
),
|
),
|
||||||
|
[OPT_TRANS_OPENAI_2]: new Map(
|
||||||
|
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
||||||
|
),
|
||||||
|
[OPT_TRANS_OPENAI_3]: new Map(
|
||||||
|
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
||||||
|
),
|
||||||
|
[OPT_TRANS_GEMINI]: new Map(
|
||||||
|
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
||||||
|
),
|
||||||
|
[OPT_TRANS_GEMINI_2]: new Map(
|
||||||
|
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
||||||
|
),
|
||||||
|
[OPT_TRANS_CLAUDE]: new Map(
|
||||||
|
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
||||||
|
),
|
||||||
|
[OPT_TRANS_OLLAMA]: new Map(
|
||||||
|
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
||||||
|
),
|
||||||
|
[OPT_TRANS_OLLAMA_2]: new Map(
|
||||||
|
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
||||||
|
),
|
||||||
|
[OPT_TRANS_OLLAMA_3]: new Map(
|
||||||
|
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
||||||
|
),
|
||||||
[OPT_TRANS_CLOUDFLAREAI]: new Map([
|
[OPT_TRANS_CLOUDFLAREAI]: new Map([
|
||||||
["auto", ""],
|
["auto", ""],
|
||||||
["zh-CN", "chinese"],
|
["zh-CN", "chinese"],
|
||||||
@@ -240,8 +349,30 @@ export const OPT_LANGS_SPECIAL = {
|
|||||||
...OPT_LANGS_FROM.map(([key]) => [key, key]),
|
...OPT_LANGS_FROM.map(([key]) => [key, key]),
|
||||||
["auto", ""],
|
["auto", ""],
|
||||||
]),
|
]),
|
||||||
|
[OPT_TRANS_CUSTOMIZE_2]: new Map([
|
||||||
|
...OPT_LANGS_FROM.map(([key]) => [key, key]),
|
||||||
|
["auto", ""],
|
||||||
|
]),
|
||||||
|
[OPT_TRANS_CUSTOMIZE_3]: new Map([
|
||||||
|
...OPT_LANGS_FROM.map(([key]) => [key, key]),
|
||||||
|
["auto", ""],
|
||||||
|
]),
|
||||||
|
[OPT_TRANS_CUSTOMIZE_4]: new Map([
|
||||||
|
...OPT_LANGS_FROM.map(([key]) => [key, key]),
|
||||||
|
["auto", ""],
|
||||||
|
]),
|
||||||
|
[OPT_TRANS_CUSTOMIZE_5]: new Map([
|
||||||
|
...OPT_LANGS_FROM.map(([key]) => [key, key]),
|
||||||
|
["auto", ""],
|
||||||
|
]),
|
||||||
};
|
};
|
||||||
export const OPT_LANGS_LIST = OPT_LANGS_TO.map(([lang]) => lang);
|
export const OPT_LANGS_LIST = OPT_LANGS_TO.map(([lang]) => lang);
|
||||||
|
export const OPT_LANGS_MICROSOFT = new Map(
|
||||||
|
Array.from(OPT_LANGS_SPECIAL[OPT_TRANS_MICROSOFT].entries()).map(([k, v]) => [
|
||||||
|
v,
|
||||||
|
k,
|
||||||
|
])
|
||||||
|
);
|
||||||
export const OPT_LANGS_BAIDU = new Map(
|
export const OPT_LANGS_BAIDU = new Map(
|
||||||
Array.from(OPT_LANGS_SPECIAL[OPT_TRANS_BAIDU].entries()).map(([k, v]) => [
|
Array.from(OPT_LANGS_SPECIAL[OPT_TRANS_BAIDU].entries()).map(([k, v]) => [
|
||||||
v,
|
v,
|
||||||
@@ -263,6 +394,7 @@ export const OPT_STYLE_DASHLINE = "dash_line"; // 虚线
|
|||||||
export const OPT_STYLE_WAVYLINE = "wavy_line"; // 波浪线
|
export const OPT_STYLE_WAVYLINE = "wavy_line"; // 波浪线
|
||||||
export const OPT_STYLE_FUZZY = "fuzzy"; // 模糊
|
export const OPT_STYLE_FUZZY = "fuzzy"; // 模糊
|
||||||
export const OPT_STYLE_HIGHLIGHT = "highlight"; // 高亮
|
export const OPT_STYLE_HIGHLIGHT = "highlight"; // 高亮
|
||||||
|
export const OPT_STYLE_BLOCKQUOTE = "blockquote"; // 引用
|
||||||
export const OPT_STYLE_DIY = "diy_style"; // 自定义样式
|
export const OPT_STYLE_DIY = "diy_style"; // 自定义样式
|
||||||
export const OPT_STYLE_ALL = [
|
export const OPT_STYLE_ALL = [
|
||||||
OPT_STYLE_NONE,
|
OPT_STYLE_NONE,
|
||||||
@@ -272,6 +404,7 @@ export const OPT_STYLE_ALL = [
|
|||||||
OPT_STYLE_WAVYLINE,
|
OPT_STYLE_WAVYLINE,
|
||||||
OPT_STYLE_FUZZY,
|
OPT_STYLE_FUZZY,
|
||||||
OPT_STYLE_HIGHLIGHT,
|
OPT_STYLE_HIGHLIGHT,
|
||||||
|
OPT_STYLE_BLOCKQUOTE,
|
||||||
OPT_STYLE_DIY,
|
OPT_STYLE_DIY,
|
||||||
];
|
];
|
||||||
export const OPT_STYLE_USE_COLOR = [
|
export const OPT_STYLE_USE_COLOR = [
|
||||||
@@ -280,40 +413,69 @@ export const OPT_STYLE_USE_COLOR = [
|
|||||||
OPT_STYLE_DASHLINE,
|
OPT_STYLE_DASHLINE,
|
||||||
OPT_STYLE_WAVYLINE,
|
OPT_STYLE_WAVYLINE,
|
||||||
OPT_STYLE_HIGHLIGHT,
|
OPT_STYLE_HIGHLIGHT,
|
||||||
|
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; // 默认最大任务数量
|
||||||
export const DEFAULT_FETCH_INTERVAL = 100; // 默认任务间隔时间
|
export const DEFAULT_FETCH_INTERVAL = 100; // 默认任务间隔时间
|
||||||
|
|
||||||
export const PROMPT_PLACE_FROM = "{{from}}"; // 占位符
|
export const INPUT_PLACE_URL = "{{url}}"; // 占位符
|
||||||
export const PROMPT_PLACE_TO = "{{to}}"; // 占位符
|
export const INPUT_PLACE_FROM = "{{from}}"; // 占位符
|
||||||
|
export const INPUT_PLACE_TO = "{{to}}"; // 占位符
|
||||||
|
export const INPUT_PLACE_TEXT = "{{text}}"; // 占位符
|
||||||
|
export const INPUT_PLACE_KEY = "{{key}}"; // 占位符
|
||||||
|
export const INPUT_PLACE_MODEL = "{{model}}"; // 占位符
|
||||||
|
|
||||||
export const DEFAULT_COLOR = "#209CEE"; // 默认高亮背景色/线条颜色
|
export const DEFAULT_COLOR = "#209CEE"; // 默认高亮背景色/线条颜色
|
||||||
|
|
||||||
|
export const DEFAULT_TRANS_TAG = "font";
|
||||||
|
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, // 选择器
|
||||||
translator: OPT_TRANS_MICROSOFT,
|
keepSelector: DEFAULT_KEEP_SELECTOR, // 保留元素选择器
|
||||||
fromLang: "auto",
|
terms: "", // 专业术语
|
||||||
toLang: "zh-CN",
|
translator: OPT_TRANS_MICROSOFT, // 翻译服务
|
||||||
textStyle: OPT_STYLE_DASHLINE,
|
fromLang: "auto", // 源语言
|
||||||
transOpen: "false",
|
toLang: "zh-CN", // 目标语言
|
||||||
bgColor: "",
|
textStyle: OPT_STYLE_DASHLINE, // 译文样式
|
||||||
textDiyStyle: "",
|
transOpen: "false", // 开启翻译
|
||||||
|
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", // 是否同时翻译页面标题
|
||||||
|
transSelected: "true", // 是否启用划词翻译
|
||||||
|
detectRemote: "false", // 是否使用远程语言检测
|
||||||
|
skipLangs: [], // 不翻译的语言
|
||||||
|
fixerSelector: "", // 修复函数选择器
|
||||||
|
fixerFunc: "-", // 修复函数
|
||||||
|
transStartHook: "", // 钩子函数
|
||||||
|
transEndHook: "", // 钩子函数
|
||||||
|
transRemoveHook: "", // 钩子函数
|
||||||
};
|
};
|
||||||
|
|
||||||
// 输入框翻译
|
// 输入框翻译
|
||||||
@@ -331,15 +493,37 @@ export const DEFAULT_INPUT_RULE = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 划词翻译
|
// 划词翻译
|
||||||
export const DEFAULT_TRANBOX_SHORTCUT = ["AltLeft", "KeyB"];
|
export const PHONIC_MAP = {
|
||||||
|
en_phonic: ["英", "uk"],
|
||||||
|
us_phonic: ["美", "en"],
|
||||||
|
};
|
||||||
|
export const OPT_TRANBOX_TRIGGER_CLICK = "click";
|
||||||
|
export const OPT_TRANBOX_TRIGGER_HOVER = "hover";
|
||||||
|
export const OPT_TRANBOX_TRIGGER_SELECT = "select";
|
||||||
|
export const OPT_TRANBOX_TRIGGER_ALL = [
|
||||||
|
OPT_TRANBOX_TRIGGER_CLICK,
|
||||||
|
OPT_TRANBOX_TRIGGER_HOVER,
|
||||||
|
OPT_TRANBOX_TRIGGER_SELECT,
|
||||||
|
];
|
||||||
|
export const DEFAULT_TRANBOX_SHORTCUT = ["AltLeft", "KeyS"];
|
||||||
export const DEFAULT_TRANBOX_SETTING = {
|
export const DEFAULT_TRANBOX_SETTING = {
|
||||||
transOpen: true,
|
// transOpen: true, // 是否启用划词翻译(作废,移至rule)
|
||||||
translator: OPT_TRANS_MICROSOFT,
|
translator: OPT_TRANS_MICROSOFT,
|
||||||
fromLang: "auto",
|
fromLang: "auto",
|
||||||
toLang: "zh-CN",
|
toLang: "zh-CN",
|
||||||
|
toLang2: "en",
|
||||||
tranboxShortcut: DEFAULT_TRANBOX_SHORTCUT,
|
tranboxShortcut: DEFAULT_TRANBOX_SHORTCUT,
|
||||||
btnOffsetX: 10,
|
btnOffsetX: 10,
|
||||||
btnOffsetY: 10,
|
btnOffsetY: 10,
|
||||||
|
boxOffsetX: 0,
|
||||||
|
boxOffsetY: 10,
|
||||||
|
hideTranBtn: false, // 是否隐藏翻译按钮
|
||||||
|
hideClickAway: false, // 是否点击外部关闭弹窗
|
||||||
|
simpleStyle: false, // 是否简洁界面
|
||||||
|
followSelection: false, // 翻译框是否跟随选中文本
|
||||||
|
triggerMode: OPT_TRANBOX_TRIGGER_CLICK, // 触发翻译方式
|
||||||
|
extStyles: "", // 附加样式
|
||||||
|
enDict: OPT_DICT_BAIDU, // 英文词典
|
||||||
};
|
};
|
||||||
|
|
||||||
// 订阅列表
|
// 订阅列表
|
||||||
@@ -358,34 +542,194 @@ export const DEFAULT_SUBRULES_LIST = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const DEFAULT_HTTP_TIMEOUT = 5000; // 调用超时时间
|
||||||
|
|
||||||
// 翻译接口
|
// 翻译接口
|
||||||
|
const defaultCustomApi = {
|
||||||
|
url: "",
|
||||||
|
key: "",
|
||||||
|
customOption: "", // (作废)
|
||||||
|
reqHook: "", // request 钩子函数
|
||||||
|
resHook: "", // response 钩子函数
|
||||||
|
fetchLimit: DEFAULT_FETCH_LIMIT,
|
||||||
|
fetchInterval: DEFAULT_FETCH_INTERVAL,
|
||||||
|
apiName: "",
|
||||||
|
isDisabled: false,
|
||||||
|
httpTimeout: DEFAULT_HTTP_TIMEOUT,
|
||||||
|
};
|
||||||
|
const defaultOpenaiApi = {
|
||||||
|
url: "https://api.openai.com/v1/chat/completions",
|
||||||
|
key: "",
|
||||||
|
model: "gpt-4",
|
||||||
|
systemPrompt: `You are a professional, authentic machine translation engine.`,
|
||||||
|
userPrompt: `Translate the following source text from ${INPUT_PLACE_FROM} to ${INPUT_PLACE_TO}. Output translation directly without any additional text.\n\nSource Text: ${INPUT_PLACE_TEXT}\n\nTranslated Text:`,
|
||||||
|
temperature: 0,
|
||||||
|
maxTokens: 256,
|
||||||
|
fetchLimit: 1,
|
||||||
|
fetchInterval: 500,
|
||||||
|
apiName: "",
|
||||||
|
isDisabled: false,
|
||||||
|
httpTimeout: DEFAULT_HTTP_TIMEOUT * 2,
|
||||||
|
};
|
||||||
|
const defaultOllamaApi = {
|
||||||
|
url: "http://localhost:11434/api/generate",
|
||||||
|
key: "",
|
||||||
|
model: "llama3.1",
|
||||||
|
systemPrompt: `You are a professional, authentic machine translation engine.`,
|
||||||
|
userPrompt: `Translate the following source text from ${INPUT_PLACE_FROM} to ${INPUT_PLACE_TO}. Output translation directly without any additional text.\n\nSource Text: ${INPUT_PLACE_TEXT}\n\nTranslated Text:`,
|
||||||
|
think: false,
|
||||||
|
thinkIgnore: `qwen3,deepseek-r1`,
|
||||||
|
fetchLimit: 1,
|
||||||
|
fetchInterval: 500,
|
||||||
|
apiName: "",
|
||||||
|
isDisabled: false,
|
||||||
|
httpTimeout: DEFAULT_HTTP_TIMEOUT * 2,
|
||||||
|
};
|
||||||
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: URL_GOOGLE_TRAN,
|
||||||
key: "",
|
key: "",
|
||||||
|
fetchLimit: DEFAULT_FETCH_LIMIT, // 最大任务数量
|
||||||
|
fetchInterval: DEFAULT_FETCH_INTERVAL, // 任务间隔时间
|
||||||
|
apiName: OPT_TRANS_GOOGLE, // 接口自定义名称
|
||||||
|
isDisabled: false, // 是否禁用
|
||||||
|
httpTimeout: DEFAULT_HTTP_TIMEOUT, // 超时时间
|
||||||
|
},
|
||||||
|
[OPT_TRANS_GOOGLE_2]: {
|
||||||
|
url: URL_GOOGLE_TRAN2,
|
||||||
|
key: DEFAULT_GOOGLE_API_KEY,
|
||||||
|
fetchLimit: DEFAULT_FETCH_LIMIT,
|
||||||
|
fetchInterval: DEFAULT_FETCH_INTERVAL,
|
||||||
|
apiName: OPT_TRANS_GOOGLE_2,
|
||||||
|
isDisabled: false,
|
||||||
|
httpTimeout: DEFAULT_HTTP_TIMEOUT,
|
||||||
|
},
|
||||||
|
[OPT_TRANS_MICROSOFT]: {
|
||||||
|
fetchLimit: DEFAULT_FETCH_LIMIT,
|
||||||
|
fetchInterval: DEFAULT_FETCH_INTERVAL,
|
||||||
|
apiName: OPT_TRANS_MICROSOFT,
|
||||||
|
isDisabled: false,
|
||||||
|
httpTimeout: DEFAULT_HTTP_TIMEOUT,
|
||||||
|
},
|
||||||
|
[OPT_TRANS_BAIDU]: {
|
||||||
|
fetchLimit: DEFAULT_FETCH_LIMIT,
|
||||||
|
fetchInterval: DEFAULT_FETCH_INTERVAL,
|
||||||
|
apiName: OPT_TRANS_BAIDU,
|
||||||
|
isDisabled: false,
|
||||||
|
httpTimeout: DEFAULT_HTTP_TIMEOUT,
|
||||||
|
},
|
||||||
|
[OPT_TRANS_TENCENT]: {
|
||||||
|
fetchLimit: DEFAULT_FETCH_LIMIT,
|
||||||
|
fetchInterval: DEFAULT_FETCH_INTERVAL,
|
||||||
|
apiName: OPT_TRANS_TENCENT,
|
||||||
|
isDisabled: false,
|
||||||
|
httpTimeout: DEFAULT_HTTP_TIMEOUT,
|
||||||
|
},
|
||||||
|
[OPT_TRANS_VOLCENGINE]: {
|
||||||
|
fetchLimit: DEFAULT_FETCH_LIMIT,
|
||||||
|
fetchInterval: DEFAULT_FETCH_INTERVAL,
|
||||||
|
apiName: OPT_TRANS_VOLCENGINE,
|
||||||
|
isDisabled: false,
|
||||||
|
httpTimeout: DEFAULT_HTTP_TIMEOUT,
|
||||||
},
|
},
|
||||||
[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,
|
||||||
|
apiName: OPT_TRANS_DEEPL,
|
||||||
|
isDisabled: false,
|
||||||
|
httpTimeout: DEFAULT_HTTP_TIMEOUT,
|
||||||
|
},
|
||||||
|
[OPT_TRANS_DEEPLFREE]: {
|
||||||
|
fetchLimit: 1,
|
||||||
|
fetchInterval: 500,
|
||||||
|
apiName: OPT_TRANS_DEEPLFREE,
|
||||||
|
isDisabled: false,
|
||||||
|
httpTimeout: DEFAULT_HTTP_TIMEOUT,
|
||||||
},
|
},
|
||||||
[OPT_TRANS_DEEPLX]: {
|
[OPT_TRANS_DEEPLX]: {
|
||||||
url: "http://localhost:1188/translate",
|
url: "http://localhost:1188/translate",
|
||||||
key: "",
|
key: "",
|
||||||
|
fetchLimit: 1,
|
||||||
|
fetchInterval: 500,
|
||||||
|
apiName: OPT_TRANS_DEEPLX,
|
||||||
|
isDisabled: false,
|
||||||
|
httpTimeout: DEFAULT_HTTP_TIMEOUT,
|
||||||
},
|
},
|
||||||
[OPT_TRANS_OPENAI]: {
|
[OPT_TRANS_NIUTRANS]: {
|
||||||
url: "https://api.openai.com/v1/chat/completions",
|
url: "https://api.niutrans.com/NiuTransServer/translation",
|
||||||
key: "",
|
key: "",
|
||||||
model: "gpt-4",
|
dictNo: "",
|
||||||
prompt: `You will be provided with a sentence in ${PROMPT_PLACE_FROM}, and your task is to translate it into ${PROMPT_PLACE_TO}.`,
|
memoryNo: "",
|
||||||
|
fetchLimit: DEFAULT_FETCH_LIMIT,
|
||||||
|
fetchInterval: DEFAULT_FETCH_INTERVAL,
|
||||||
|
apiName: OPT_TRANS_NIUTRANS,
|
||||||
|
isDisabled: false,
|
||||||
|
httpTimeout: DEFAULT_HTTP_TIMEOUT,
|
||||||
|
},
|
||||||
|
[OPT_TRANS_OPENAI]: defaultOpenaiApi,
|
||||||
|
[OPT_TRANS_OPENAI_2]: defaultOpenaiApi,
|
||||||
|
[OPT_TRANS_OPENAI_3]: defaultOpenaiApi,
|
||||||
|
[OPT_TRANS_GEMINI]: {
|
||||||
|
url: `https://generativelanguage.googleapis.com/v1/models/${INPUT_PLACE_MODEL}:generateContent?key=${INPUT_PLACE_KEY}`,
|
||||||
|
key: "",
|
||||||
|
model: "gemini-2.5-flash",
|
||||||
|
systemPrompt: `You are a professional, authentic machine translation engine.`,
|
||||||
|
userPrompt: `Translate the following source text from ${INPUT_PLACE_FROM} to ${INPUT_PLACE_TO}. Output translation directly without any additional text.\n\nSource Text: ${INPUT_PLACE_TEXT}\n\nTranslated Text:`,
|
||||||
|
temperature: 0,
|
||||||
|
maxTokens: 2048,
|
||||||
|
fetchLimit: 1,
|
||||||
|
fetchInterval: 500,
|
||||||
|
apiName: OPT_TRANS_GEMINI,
|
||||||
|
isDisabled: false,
|
||||||
|
httpTimeout: DEFAULT_HTTP_TIMEOUT * 2,
|
||||||
|
},
|
||||||
|
[OPT_TRANS_GEMINI_2]: {
|
||||||
|
url: `https://generativelanguage.googleapis.com/v1beta/openai/chat/completions`,
|
||||||
|
key: "",
|
||||||
|
model: "gemini-2.0-flash",
|
||||||
|
systemPrompt: `You are a professional, authentic machine translation engine.`,
|
||||||
|
userPrompt: `Translate the following source text from ${INPUT_PLACE_FROM} to ${INPUT_PLACE_TO}. Output translation directly without any additional text.\n\nSource Text: ${INPUT_PLACE_TEXT}\n\nTranslated Text:`,
|
||||||
|
temperature: 0,
|
||||||
|
maxTokens: 2048,
|
||||||
|
fetchLimit: 1,
|
||||||
|
fetchInterval: 500,
|
||||||
|
apiName: OPT_TRANS_GEMINI_2,
|
||||||
|
isDisabled: false,
|
||||||
|
httpTimeout: DEFAULT_HTTP_TIMEOUT * 2,
|
||||||
|
},
|
||||||
|
[OPT_TRANS_CLAUDE]: {
|
||||||
|
url: "https://api.anthropic.com/v1/messages",
|
||||||
|
key: "",
|
||||||
|
model: "claude-3-haiku-20240307",
|
||||||
|
systemPrompt: `You are a professional, authentic machine translation engine.`,
|
||||||
|
userPrompt: `Translate the following source text from ${INPUT_PLACE_FROM} to ${INPUT_PLACE_TO}. Output translation directly without any additional text.\n\nSource Text: ${INPUT_PLACE_TEXT}\n\nTranslated Text:`,
|
||||||
|
temperature: 0,
|
||||||
|
maxTokens: 1024,
|
||||||
|
fetchLimit: 1,
|
||||||
|
fetchInterval: 500,
|
||||||
|
apiName: OPT_TRANS_CLAUDE,
|
||||||
|
isDisabled: false,
|
||||||
|
httpTimeout: DEFAULT_HTTP_TIMEOUT * 2,
|
||||||
},
|
},
|
||||||
[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: "",
|
|
||||||
},
|
|
||||||
[OPT_TRANS_CUSTOMIZE]: {
|
|
||||||
url: "",
|
|
||||||
key: "",
|
key: "",
|
||||||
|
fetchLimit: 1,
|
||||||
|
fetchInterval: 500,
|
||||||
|
apiName: OPT_TRANS_CLOUDFLAREAI,
|
||||||
|
isDisabled: false,
|
||||||
|
httpTimeout: DEFAULT_HTTP_TIMEOUT * 2,
|
||||||
},
|
},
|
||||||
|
[OPT_TRANS_OLLAMA]: defaultOllamaApi,
|
||||||
|
[OPT_TRANS_OLLAMA_2]: defaultOllamaApi,
|
||||||
|
[OPT_TRANS_OLLAMA_3]: defaultOllamaApi,
|
||||||
|
[OPT_TRANS_CUSTOMIZE]: defaultCustomApi,
|
||||||
|
[OPT_TRANS_CUSTOMIZE_2]: defaultCustomApi,
|
||||||
|
[OPT_TRANS_CUSTOMIZE_3]: defaultCustomApi,
|
||||||
|
[OPT_TRANS_CUSTOMIZE_4]: defaultCustomApi,
|
||||||
|
[OPT_TRANS_CUSTOMIZE_5]: defaultCustomApi,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 默认快捷键
|
// 默认快捷键
|
||||||
@@ -397,32 +741,52 @@ export const DEFAULT_SHORTCUTS = {
|
|||||||
[OPT_SHORTCUT_TRANSLATE]: ["AltLeft", "KeyQ"],
|
[OPT_SHORTCUT_TRANSLATE]: ["AltLeft", "KeyQ"],
|
||||||
[OPT_SHORTCUT_STYLE]: ["AltLeft", "KeyC"],
|
[OPT_SHORTCUT_STYLE]: ["AltLeft", "KeyC"],
|
||||||
[OPT_SHORTCUT_POPUP]: ["AltLeft", "KeyK"],
|
[OPT_SHORTCUT_POPUP]: ["AltLeft", "KeyK"],
|
||||||
[OPT_SHORTCUT_SETTING]: ["AltLeft", "KeyN"],
|
[OPT_SHORTCUT_SETTING]: ["AltLeft", "KeyO"],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TRANS_MIN_LENGTH = 5; // 最短翻译长度
|
export const TRANS_MIN_LENGTH = 5; // 最短翻译长度
|
||||||
export const TRANS_MAX_LENGTH = 5000; // 最长翻译长度
|
export const TRANS_MAX_LENGTH = 5000; // 最长翻译长度
|
||||||
export const TRANS_NEWLINE_LENGTH = 20; // 换行字符数
|
export const TRANS_NEWLINE_LENGTH = 20; // 换行字符数
|
||||||
|
export const DEFAULT_BLACKLIST = [
|
||||||
|
"https://fishjar.github.io/kiss-translator/options.html",
|
||||||
|
"https://translate.google.com",
|
||||||
|
"https://www.deepl.com/translator",
|
||||||
|
"oapi.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,
|
||||||
|
httpTimeout: DEFAULT_HTTP_TIMEOUT,
|
||||||
clearCache: false, // 是否在浏览器下次启动时清除缓存
|
clearCache: false, // 是否在浏览器下次启动时清除缓存
|
||||||
injectRules: true, // 是否注入订阅规则
|
injectRules: true, // 是否注入订阅规则
|
||||||
injectWebfix: true, // 是否注入修复补丁
|
// injectWebfix: true, // 是否注入修复补丁(作废)
|
||||||
detectRemote: false, // 是否使用远程语言检测
|
// detectRemote: false, // 是否使用远程语言检测(移至rule,作废)
|
||||||
|
// 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, // 触屏翻译
|
||||||
|
blacklist: DEFAULT_BLACKLIST.join(",\n"), // 禁用翻译名单
|
||||||
|
csplist: DEFAULT_CSPLIST.join(",\n"), // 禁用CSP名单
|
||||||
|
// disableLangs: [], // 不翻译的语言(移至rule,作废)
|
||||||
|
transInterval: 500, // 翻译间隔时间
|
||||||
|
langDetector: OPT_TRANS_MICROSOFT, // 远程语言识别服务
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_RULES = [GLOBLA_RULE];
|
export const DEFAULT_RULES = [GLOBLA_RULE];
|
||||||
|
|||||||
@@ -1,22 +1,39 @@
|
|||||||
const els = `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_SELECTOR = `:is(${els})`;
|
|
||||||
|
|
||||||
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: "", // 选择器
|
||||||
translator: GLOBAL_KEY,
|
keepSelector: "", // 保留元素选择器
|
||||||
fromLang: GLOBAL_KEY,
|
terms: "", // 专业术语
|
||||||
toLang: GLOBAL_KEY,
|
translator: GLOBAL_KEY, // 翻译服务
|
||||||
textStyle: GLOBAL_KEY,
|
fromLang: GLOBAL_KEY, // 源语言
|
||||||
transOpen: GLOBAL_KEY,
|
toLang: GLOBAL_KEY, // 目标语言
|
||||||
bgColor: "",
|
textStyle: GLOBAL_KEY, // 译文样式
|
||||||
textDiyStyle: "",
|
transOpen: GLOBAL_KEY, // 开启翻译
|
||||||
|
bgColor: "", // 译文颜色
|
||||||
|
textDiyStyle: "", // 自定义译文样式
|
||||||
|
selectStyle: "", // 选择器节点样式
|
||||||
|
parentStyle: "", // 选择器父节点样式
|
||||||
|
injectJs: "", // 注入JS
|
||||||
|
injectCss: "", // 注入CSS
|
||||||
|
transOnly: GLOBAL_KEY, // 是否仅显示译文
|
||||||
|
transTiming: GLOBAL_KEY, // 翻译时机/鼠标悬停翻译
|
||||||
|
transTag: GLOBAL_KEY, // 译文元素标签
|
||||||
|
transTitle: GLOBAL_KEY, // 是否同时翻译页面标题
|
||||||
|
transSelected: GLOBAL_KEY, // 是否启用划词翻译
|
||||||
|
detectRemote: GLOBAL_KEY, // 是否使用远程语言检测
|
||||||
|
skipLangs: [], // 不翻译的语言
|
||||||
|
fixerSelector: "", // 修复函数选择器
|
||||||
|
fixerFunc: GLOBAL_KEY, // 修复函数
|
||||||
|
transStartHook: "", // 钩子函数
|
||||||
|
transEndHook: "", // 钩子函数
|
||||||
|
transRemoveHook: "", // 钩子函数
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_DIY_STYLE = `color: #666;
|
const DEFAULT_DIY_STYLE = `color: #666;
|
||||||
@@ -42,145 +59,264 @@ export const DEFAULT_OW_RULE = {
|
|||||||
textDiyStyle: DEFAULT_DIY_STYLE,
|
textDiyStyle: DEFAULT_DIY_STYLE,
|
||||||
};
|
};
|
||||||
|
|
||||||
const RULES = [
|
const RULES_MAP = {
|
||||||
{
|
"www.google.com/search": {
|
||||||
pattern: `www.google.com/search`,
|
|
||||||
selector: `h3, .IsZvec, .VwiC3b`,
|
selector: `h3, .IsZvec, .VwiC3b`,
|
||||||
},
|
},
|
||||||
{
|
"news.google.com": {
|
||||||
pattern: `news.google.com`,
|
selector: `[data-n-tid], ${DEFAULT_SELECTOR}`,
|
||||||
selector: `h4`,
|
|
||||||
},
|
},
|
||||||
{
|
"www.foxnews.com": {
|
||||||
pattern: `www.foxnews.com`,
|
|
||||||
selector: `h1, h2, .title, .sidebar [data-type="Title"], .article-content ${DEFAULT_SELECTOR}; [data-spotim-module="conversation"]>div >>> [data-spot-im-class="message-text"] p, [data-spot-im-class="message-text"]`,
|
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"]`,
|
||||||
},
|
},
|
||||||
{
|
"bearblog.dev, www.theverge.com, www.tampermonkey.net/documentation.php": {
|
||||||
pattern: `bearblog.dev, www.theverge.com, www.tampermonkey.net/documentation.php`,
|
selector: `${DEFAULT_SELECTOR}`,
|
||||||
selector: DEFAULT_SELECTOR,
|
|
||||||
},
|
},
|
||||||
{
|
"themessenger.com": {
|
||||||
pattern: `themessenger.com`,
|
|
||||||
selector: `.leading-tight, .leading-tighter, .my-2 p, .font-body p, article ${DEFAULT_SELECTOR}`,
|
selector: `.leading-tight, .leading-tighter, .my-2 p, .font-body p, article ${DEFAULT_SELECTOR}`,
|
||||||
},
|
},
|
||||||
{
|
"www.telegraph.co.uk, go.dev/doc/": {
|
||||||
pattern: `www.telegraph.co.uk`,
|
|
||||||
selector: `article ${DEFAULT_SELECTOR}`,
|
selector: `article ${DEFAULT_SELECTOR}`,
|
||||||
},
|
},
|
||||||
{
|
"www.theguardian.com": {
|
||||||
pattern: `www.theguardian.com`,
|
|
||||||
selector: `.show-underline, .dcr-hup5wm div, .dcr-7vl6y8 div, .dcr-12evv1c, figcaption, article ${DEFAULT_SELECTOR}, [data-cy="mostviewed-footer"] h4`,
|
selector: `.show-underline, .dcr-hup5wm div, .dcr-7vl6y8 div, .dcr-12evv1c, figcaption, article ${DEFAULT_SELECTOR}, [data-cy="mostviewed-footer"] h4`,
|
||||||
},
|
},
|
||||||
{
|
"www.semafor.com": {
|
||||||
pattern: `www.semafor.com`,
|
|
||||||
selector: `${DEFAULT_SELECTOR}, .styles_intro__IYj__, [class*="styles_description"]`,
|
selector: `${DEFAULT_SELECTOR}, .styles_intro__IYj__, [class*="styles_description"]`,
|
||||||
},
|
},
|
||||||
{
|
"www.noemamag.com": {
|
||||||
pattern: `www.noemamag.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`,
|
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`,
|
||||||
},
|
},
|
||||||
{
|
"restofworld.org": {
|
||||||
pattern: `restofworld.org`,
|
|
||||||
selector: `${DEFAULT_SELECTOR}, .recirc-story__headline, .recirc-story__dek`,
|
selector: `${DEFAULT_SELECTOR}, .recirc-story__headline, .recirc-story__dek`,
|
||||||
},
|
},
|
||||||
{
|
"www.axios.com": {
|
||||||
pattern: `www.axios.com`,
|
|
||||||
selector: `.h7, ${DEFAULT_SELECTOR}`,
|
selector: `.h7, ${DEFAULT_SELECTOR}`,
|
||||||
},
|
},
|
||||||
{
|
"www.newyorker.com": {
|
||||||
pattern: `www.newyorker.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`,
|
||||||
selector: `.summary-item__hed, .summary-item__dek, .summary-collection-grid__dek, .dqtvfu, .rubric__link, .caption, article ${DEFAULT_SELECTOR}, .HEhan ${DEFAULT_SELECTOR}, .ContributorBioBio-fBolsO`,
|
|
||||||
},
|
},
|
||||||
{
|
"time.com": {
|
||||||
pattern: `https://time.com/`,
|
|
||||||
selector: `h1, h3, .summary, .video-title, #article-body ${DEFAULT_SELECTOR}, .image-wrap-container .credit.body-caption, .media-heading`,
|
selector: `h1, h3, .summary, .video-title, #article-body ${DEFAULT_SELECTOR}, .image-wrap-container .credit.body-caption, .media-heading`,
|
||||||
},
|
},
|
||||||
{
|
"www.dw.com": {
|
||||||
pattern: `www.dw.com`,
|
|
||||||
selector: `.ts-teaser-title a, .news-title a, .title a, .teaser-description a, .hbudab h3, .hbudab p, figcaption ,article ${DEFAULT_SELECTOR}`,
|
selector: `.ts-teaser-title a, .news-title a, .title a, .teaser-description a, .hbudab h3, .hbudab p, figcaption ,article ${DEFAULT_SELECTOR}`,
|
||||||
},
|
},
|
||||||
{
|
"www.bbc.com": {
|
||||||
pattern: `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`,
|
||||||
selector: `h1, h2, .media__link, .media__summary, article ${DEFAULT_SELECTOR}, .ssrcss-y7krbn-Stack, .ssrcss-1mrs5ns-PromoLink, .ssrcss-18cjaf3-Headline, .gs-c-promo-heading__title, .gs-c-promo-summary, .media__content h3, .article__intro`,
|
|
||||||
},
|
},
|
||||||
{
|
"www.chinadaily.com.cn": {
|
||||||
pattern: `www.chinadaily.com.cn`,
|
|
||||||
selector: `h1, .tMain [shape="rect"], .cMain [shape="rect"], .photo_art [shape="rect"], .mai_r [shape="rect"], .lisBox li, #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.facebook.com": {
|
||||||
pattern: `www.facebook.com`,
|
|
||||||
selector: `[role="main"] [dir="auto"]`,
|
selector: `[role="main"] [dir="auto"]`,
|
||||||
},
|
},
|
||||||
{
|
"www.reddit.com, new.reddit.com, sh.reddit.com": {
|
||||||
pattern: `www.reddit.com`,
|
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)`,
|
||||||
selector: `[slot="title"], [slot="text-body"] ${DEFAULT_SELECTOR}, #-post-rtjson-content p`,
|
|
||||||
},
|
},
|
||||||
{
|
"www.quora.com": {
|
||||||
pattern: `www.quora.com`,
|
|
||||||
selector: `.qu-wordBreak--break-word`,
|
selector: `.qu-wordBreak--break-word`,
|
||||||
},
|
},
|
||||||
{
|
"edition.cnn.com": {
|
||||||
pattern: `edition.cnn.com`,
|
|
||||||
selector: `.container__title, .container__headline, .headline__text, .image__caption, [data-type="Title"], .article__content ${DEFAULT_SELECTOR}`,
|
selector: `.container__title, .container__headline, .headline__text, .image__caption, [data-type="Title"], .article__content ${DEFAULT_SELECTOR}`,
|
||||||
},
|
},
|
||||||
{
|
"www.reuters.com": {
|
||||||
pattern: `www.reuters.com`,
|
|
||||||
selector: `#main-content [data-testid="Heading"], #main-content [data-testid="Body"], .article-body__content__17Yit ${DEFAULT_SELECTOR}`,
|
selector: `#main-content [data-testid="Heading"], #main-content [data-testid="Body"], .article-body__content__17Yit ${DEFAULT_SELECTOR}`,
|
||||||
},
|
},
|
||||||
{
|
"www.bloomberg.com": {
|
||||||
pattern: `www.bloomberg.com`,
|
|
||||||
selector: `[data-component="headline"], [data-component="related-item-headline"], [data-component="title"], article ${DEFAULT_SELECTOR}`,
|
selector: `[data-component="headline"], [data-component="related-item-headline"], [data-component="title"], article ${DEFAULT_SELECTOR}`,
|
||||||
},
|
},
|
||||||
{
|
"deno.land, docs.github.com": {
|
||||||
pattern: `deno.land, docs.github.com`,
|
|
||||||
selector: `main ${DEFAULT_SELECTOR}`,
|
selector: `main ${DEFAULT_SELECTOR}`,
|
||||||
|
keepSelector: DEFAULT_KEEP_SELECTOR,
|
||||||
},
|
},
|
||||||
{
|
"doc.rust-lang.org": {
|
||||||
pattern: `doc.rust-lang.org`,
|
selector: `.content ${DEFAULT_SELECTOR}`,
|
||||||
selector: `#content ${DEFAULT_SELECTOR}`,
|
keepSelector: DEFAULT_KEEP_SELECTOR,
|
||||||
},
|
},
|
||||||
{
|
"www.indiehackers.com": {
|
||||||
pattern: `www.indiehackers.com`,
|
|
||||||
selector: `h1, h3, .content ${DEFAULT_SELECTOR}, .feed-item__title-link`,
|
selector: `h1, h3, .content ${DEFAULT_SELECTOR}, .feed-item__title-link`,
|
||||||
},
|
},
|
||||||
{
|
"platform.openai.com/docs": {
|
||||||
pattern: `platform.openai.com/docs`,
|
|
||||||
selector: `.docs-body ${DEFAULT_SELECTOR}`,
|
selector: `.docs-body ${DEFAULT_SELECTOR}`,
|
||||||
|
keepSelector: DEFAULT_KEEP_SELECTOR,
|
||||||
},
|
},
|
||||||
{
|
"en.wikipedia.org": {
|
||||||
pattern: `en.wikipedia.org`,
|
|
||||||
selector: `h1, .mw-parser-output ${DEFAULT_SELECTOR}`,
|
selector: `h1, .mw-parser-output ${DEFAULT_SELECTOR}`,
|
||||||
|
keepSelector: `.mwe-math-element`,
|
||||||
},
|
},
|
||||||
{
|
"stackoverflow.com, serverfault.com, superuser.com, stackexchange.com, askubuntu.com, stackapps.com, mathoverflow.net":
|
||||||
pattern: `stackoverflow.com`,
|
{
|
||||||
selector: `h1, .s-prose p, .comment-body .comment-copy`,
|
selector: `.s-prose ${DEFAULT_SELECTOR}, .comment-copy, .question-hyperlink, .s-post-summary--content-title, .s-post-summary--content-excerpt`,
|
||||||
|
keepSelector: `${DEFAULT_KEEP_SELECTOR}, .math-container`,
|
||||||
|
},
|
||||||
|
"www.npmjs.com/package, developer.chrome.com/docs, medium.com, react.dev, create-react-app.dev, pytorch.org":
|
||||||
|
{
|
||||||
|
selector: `article ${DEFAULT_SELECTOR}`,
|
||||||
|
},
|
||||||
|
"news.ycombinator.com": {
|
||||||
|
selector: `.title, p`,
|
||||||
|
fixerSelector: `.toptext, .commtext`,
|
||||||
|
fixerFunc: FIXER_BR,
|
||||||
},
|
},
|
||||||
{
|
"github.com": {
|
||||||
pattern: `www.npmjs.com/package/, developer.chrome.com/docs, medium.com, developers.cloudflare.com, react.dev, create-react-app.dev, pytorch.org/`,
|
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,
|
||||||
|
},
|
||||||
|
"twitter.com": {
|
||||||
|
selector: `[data-testid="tweetText"], [data-testid="birdwatch-pivot"]>div.css-1rynq56`,
|
||||||
|
keepSelector: `img, a, .r-18u37iz, .css-175oi2r`,
|
||||||
|
},
|
||||||
|
"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`,
|
||||||
|
selectStyle: `-webkit-line-clamp: unset; max-height: none; height: auto;`,
|
||||||
|
parentStyle: `-webkit-line-clamp: unset; max-height: none; height: auto;`,
|
||||||
|
keepSelector: `img, #content-text>a`,
|
||||||
|
},
|
||||||
|
"www.youtube.com": {
|
||||||
|
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;`,
|
||||||
|
parentStyle: `-webkit-line-clamp: unset; max-height: none; height: auto;`,
|
||||||
|
keepSelector: `img, #content-text>a`,
|
||||||
|
},
|
||||||
|
"bard.google.com": {
|
||||||
|
selector: `.query-content ${DEFAULT_SELECTOR}, message-content ${DEFAULT_SELECTOR}`,
|
||||||
|
},
|
||||||
|
"www.bing.com, copilot.microsoft.com": {
|
||||||
|
selector: `.b_algoSlug, .rwrl_padref; .cib-serp-main >>> .ac-textBlock ${DEFAULT_SELECTOR}, .text-message-content div`,
|
||||||
|
},
|
||||||
|
"www.phoronix.com": {
|
||||||
|
selector: `article ${DEFAULT_SELECTOR}`,
|
||||||
|
fixerSelector: `.content`,
|
||||||
|
fixerFunc: FIXER_BR,
|
||||||
|
},
|
||||||
|
"wx2.qq.com": {
|
||||||
|
selector: `.js_message_plain`,
|
||||||
|
},
|
||||||
|
"app.slack.com/client/": {
|
||||||
|
selector: `.p-rich_text_section, .c-message_attachment__text, .p-rich_text_list li`,
|
||||||
|
},
|
||||||
|
"discord.com/channels/": {
|
||||||
|
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']`,
|
||||||
|
},
|
||||||
|
"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}`,
|
selector: `article ${DEFAULT_SELECTOR}`,
|
||||||
},
|
},
|
||||||
{
|
"notebooks.githubusercontent.com/view/ipynb": {
|
||||||
pattern: `news.ycombinator.com`,
|
selector: `#notebook-container ${DEFAULT_SELECTOR}`,
|
||||||
selector: `.title, .commtext`,
|
keepSelector: DEFAULT_KEEP_SELECTOR,
|
||||||
},
|
},
|
||||||
{
|
"developers.cloudflare.com": {
|
||||||
pattern: `https://github.com/`,
|
selector: `article ${DEFAULT_SELECTOR}, .WorkerStarter--description`,
|
||||||
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`,
|
keepSelector: `a[rel='noopener'], code`,
|
||||||
},
|
},
|
||||||
{
|
"ubuntuforums.org": {
|
||||||
pattern: `twitter.com`,
|
fixerSelector: `.postcontent`,
|
||||||
selector: `[data-testid='tweetText']`,
|
fixerFunc: FIXER_BR,
|
||||||
},
|
},
|
||||||
{
|
"play.google.com/store/apps/details": {
|
||||||
pattern: `youtube.com`,
|
fixerSelector: `[data-g-id="description"]`,
|
||||||
selector: `h1, #video-title, #content-text, #title, yt-attributed-string>span>span`,
|
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 = RULES.sort((a, b) =>
|
export const BUILTIN_RULES = Object.entries(RULES_MAP)
|
||||||
a.pattern.localeCompare(b.pattern)
|
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||||
).map((item) => ({
|
.map(([pattern, rule]) => ({
|
||||||
...DEFAULT_RULE,
|
...DEFAULT_RULE,
|
||||||
...item,
|
...rule,
|
||||||
transOpen: "true",
|
pattern,
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,76 +1,3 @@
|
|||||||
import { browser } from "./libs/browser";
|
import { run } from "./common";
|
||||||
import {
|
|
||||||
MSG_TRANS_TOGGLE,
|
|
||||||
MSG_TRANS_TOGGLE_STYLE,
|
|
||||||
MSG_TRANS_GETRULE,
|
|
||||||
MSG_TRANS_PUTRULE,
|
|
||||||
} from "./config";
|
|
||||||
import { getSettingWithDefault } from "./libs/storage";
|
|
||||||
import { isIframe, sendIframeMsg } from "./libs/iframe";
|
|
||||||
import { runWebfix } from "./libs/webfix";
|
|
||||||
import {
|
|
||||||
runIframe,
|
|
||||||
runTranslator,
|
|
||||||
showFab,
|
|
||||||
showTransbox,
|
|
||||||
windowListener,
|
|
||||||
showErr,
|
|
||||||
} from "./common";
|
|
||||||
|
|
||||||
function runtimeListener(translator) {
|
run();
|
||||||
browser?.runtime.onMessage.addListener(async ({ action, args }) => {
|
|
||||||
switch (action) {
|
|
||||||
case MSG_TRANS_TOGGLE:
|
|
||||||
translator.toggle();
|
|
||||||
sendIframeMsg(MSG_TRANS_TOGGLE);
|
|
||||||
break;
|
|
||||||
case MSG_TRANS_TOGGLE_STYLE:
|
|
||||||
translator.toggleStyle();
|
|
||||||
sendIframeMsg(MSG_TRANS_TOGGLE_STYLE);
|
|
||||||
break;
|
|
||||||
case MSG_TRANS_GETRULE:
|
|
||||||
break;
|
|
||||||
case MSG_TRANS_PUTRULE:
|
|
||||||
translator.updateRule(args);
|
|
||||||
sendIframeMsg(MSG_TRANS_PUTRULE, args);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return { error: `message action is unavailable: ${action}` };
|
|
||||||
}
|
|
||||||
return { data: translator.rule };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 入口函数
|
|
||||||
*/
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
// 读取设置信息
|
|
||||||
const setting = await getSettingWithDefault();
|
|
||||||
|
|
||||||
// 适配iframe
|
|
||||||
if (isIframe) {
|
|
||||||
runIframe(setting);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 不规范网页修复
|
|
||||||
await runWebfix(setting);
|
|
||||||
|
|
||||||
// 翻译网页
|
|
||||||
const { translator, rule } = await runTranslator(setting);
|
|
||||||
|
|
||||||
// 监听消息
|
|
||||||
windowListener(rule);
|
|
||||||
runtimeListener(translator);
|
|
||||||
|
|
||||||
// 划词翻译
|
|
||||||
showTransbox(setting);
|
|
||||||
|
|
||||||
// 浮球按钮
|
|
||||||
await showFab(translator);
|
|
||||||
} catch (err) {
|
|
||||||
showErr(err);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ export function useApi(translator) {
|
|||||||
|
|
||||||
const updateApi = useCallback(
|
const updateApi = useCallback(
|
||||||
async (obj) => {
|
async (obj) => {
|
||||||
const api = transApis[translator] || {};
|
const api = {
|
||||||
|
...DEFAULT_TRANS_APIS[translator],
|
||||||
|
...(transApis[translator] || {}),
|
||||||
|
};
|
||||||
Object.assign(transApis, { [translator]: { ...api, ...obj } });
|
Object.assign(transApis, { [translator]: { ...api, ...obj } });
|
||||||
await updateSetting({ transApis });
|
await updateSetting({ transApis });
|
||||||
},
|
},
|
||||||
@@ -20,5 +23,12 @@ export function useApi(translator) {
|
|||||||
await updateSetting({ transApis });
|
await updateSetting({ transApis });
|
||||||
}, [translator, transApis, updateSetting]);
|
}, [translator, transApis, updateSetting]);
|
||||||
|
|
||||||
return { api: transApis[translator] || {}, updateApi, resetApi };
|
return {
|
||||||
|
api: {
|
||||||
|
...DEFAULT_TRANS_APIS[translator],
|
||||||
|
...(transApis[translator] || {}),
|
||||||
|
},
|
||||||
|
updateApi,
|
||||||
|
resetApi,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
61
src/hooks/Audio.js
Normal file
61
src/hooks/Audio.js
Normal 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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,14 @@ import { useSetting } from "./Setting";
|
|||||||
import { I18N, URL_RAW_PREFIX } from "../config";
|
import { I18N, URL_RAW_PREFIX } from "../config";
|
||||||
import { useFetch } from "./Fetch";
|
import { useFetch } from "./Fetch";
|
||||||
|
|
||||||
|
export const getI18n = (uiLang, key, defaultText = "") => {
|
||||||
|
return I18N?.[key]?.[uiLang] ?? defaultText;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useLangMap = (uiLang) => {
|
||||||
|
return (key, defaultText = "") => getI18n(uiLang, key, defaultText);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 多语言 hook
|
* 多语言 hook
|
||||||
* @returns
|
* @returns
|
||||||
@@ -10,7 +18,7 @@ export const useI18n = () => {
|
|||||||
const {
|
const {
|
||||||
setting: { uiLang },
|
setting: { uiLang },
|
||||||
} = useSetting();
|
} = useSetting();
|
||||||
return (key, defaultText = "") => I18N?.[key]?.[uiLang] ?? defaultText;
|
return useLangMap(uiLang);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useI18nMd = (key) => {
|
export const useI18nMd = (key) => {
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
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 {*} defaultVal 需为调用hook外的常量
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
export function useStorage(key, defaultVal) {
|
export function useStorage(key, defaultVal) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [data, setData] = useState(null);
|
const [data, setData] = useState(null);
|
||||||
@@ -34,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);
|
||||||
}
|
}
|
||||||
@@ -52,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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { ThemeProvider, createTheme } from "@mui/material/styles";
|
import { ThemeProvider, createTheme } from "@mui/material/styles";
|
||||||
import CssBaseline from "@mui/material/CssBaseline";
|
import { CssBaseline, GlobalStyles } from "@mui/material";
|
||||||
import { useDarkMode } from "./ColorMode";
|
import { useDarkMode } from "./ColorMode";
|
||||||
import { THEME_DARK, THEME_LIGHT } from "../config";
|
import { THEME_DARK, THEME_LIGHT } from "../config";
|
||||||
|
|
||||||
@@ -9,13 +9,27 @@ import { THEME_DARK, THEME_LIGHT } from "../config";
|
|||||||
* @param {*} param0
|
* @param {*} param0
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export default function Theme({ children, options }) {
|
export default function Theme({ children, options, styles }) {
|
||||||
const { darkMode } = useDarkMode();
|
const { darkMode } = useDarkMode();
|
||||||
const theme = useMemo(() => {
|
const theme = useMemo(() => {
|
||||||
|
let htmlFontSize = 16;
|
||||||
|
try {
|
||||||
|
const s = window.getComputedStyle(document.body.parentNode).fontSize;
|
||||||
|
const fontSize = parseInt(s.replace("px", ""));
|
||||||
|
if (fontSize > 0 && fontSize < 1000) {
|
||||||
|
htmlFontSize = fontSize;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
return createTheme({
|
return createTheme({
|
||||||
palette: {
|
palette: {
|
||||||
mode: darkMode ? THEME_DARK : THEME_LIGHT,
|
mode: darkMode ? THEME_DARK : THEME_LIGHT,
|
||||||
},
|
},
|
||||||
|
typography: {
|
||||||
|
htmlFontSize,
|
||||||
|
},
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
}, [darkMode, options]);
|
}, [darkMode, options]);
|
||||||
@@ -24,6 +38,7 @@ export default function Theme({ children, options }) {
|
|||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
|
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
|
<GlobalStyles styles={styles} />
|
||||||
{children}
|
{children}
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,15 +17,28 @@ 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()) {
|
||||||
if (deLang && toLang.includes(deLang)) {
|
setText(q);
|
||||||
|
setSamelang(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let deLang = "";
|
||||||
|
if (fromLang === "auto") {
|
||||||
|
deLang = await tryDetectLang(
|
||||||
|
q,
|
||||||
|
detectRemote === "true",
|
||||||
|
setting.langDetector
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (deLang && (toLang.includes(deLang) || skipLangs.includes(deLang))) {
|
||||||
setSamelang(true);
|
setSamelang(true);
|
||||||
} else {
|
} else {
|
||||||
const [trText, isSame] = await apiTranslate({
|
const [trText, isSame] = await apiTranslate({
|
||||||
@@ -32,19 +46,21 @@ export function useTranslate(q, rule, setting) {
|
|||||||
text: q,
|
text: q,
|
||||||
fromLang,
|
fromLang,
|
||||||
toLang,
|
toLang,
|
||||||
apiSetting:
|
apiSetting: {
|
||||||
setting.transApis?.[translator] || DEFAULT_TRANS_APIS[translator],
|
...DEFAULT_TRANS_APIS[translator],
|
||||||
|
...(setting.transApis[translator] || {}),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
setText(trText);
|
setText(trText);
|
||||||
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 };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,19 +36,10 @@ function App() {
|
|||||||
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}>
|
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}>
|
||||||
Install/Update Userscript for Tampermonkey/Violentmonkey
|
Install/Update Userscript for Tampermonkey/Violentmonkey
|
||||||
</Link>
|
</Link>
|
||||||
{/* <Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL2}>
|
|
||||||
Install/Update Userscript for Tampermonkey/Violentmonkey 2
|
|
||||||
</Link> */}
|
|
||||||
<Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL}>
|
<Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL}>
|
||||||
Install/Update Userscript for iOS Safari
|
Install/Update Userscript for iOS Safari
|
||||||
</Link>
|
</Link>
|
||||||
{/* <Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL2}>
|
|
||||||
Install/Update Userscript for iOS Safari 2
|
|
||||||
</Link> */}
|
|
||||||
<Link href={process.env.REACT_APP_OPTIONSPAGE}>Open Options Page</Link>
|
<Link href={process.env.REACT_APP_OPTIONSPAGE}>Open Options Page</Link>
|
||||||
{/* <Link href={process.env.REACT_APP_OPTIONSPAGE2}>
|
|
||||||
Open Options Page 2
|
|
||||||
</Link> */}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
|
|||||||
@@ -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 { fetchHandle } 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;
|
||||||
};
|
};
|
||||||
@@ -34,7 +35,7 @@ const _msAuth = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 缓存没有或失效,查询接口
|
// 缓存没有或失效,查询接口
|
||||||
token = await fetchData(URL_MICROSOFT_AUTH);
|
token = await fetchHandle({ input: URL_MICROSOFT_AUTH });
|
||||||
exp = parseMSToken(token);
|
exp = parseMSToken(token);
|
||||||
await setMsauth({ token, exp });
|
await setMsauth({ token, exp });
|
||||||
return [token, exp];
|
return [token, exp];
|
||||||
|
|||||||
10
src/libs/blacklist.js
Normal file
10
src/libs/blacklist.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { isMatch } from "./utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否在黑名单中
|
||||||
|
* @param {*} href
|
||||||
|
* @param {*} param1
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const isInBlacklist = (href, { blacklist }) =>
|
||||||
|
blacklist.split(/\n|,/).some((url) => isMatch(href, url.trim()));
|
||||||
@@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,38 @@
|
|||||||
import { isExt, isGm } from "./client";
|
import { isExt, isGm } from "./client";
|
||||||
import { sendBgMsg } from "./msg";
|
import { sendBgMsg } from "./msg";
|
||||||
import { taskPool } from "./pool";
|
import { taskPool } from "./pool";
|
||||||
|
import { getSettingWithDefault } from "./storage";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
MSG_FETCH,
|
MSG_FETCH,
|
||||||
MSG_FETCH_LIMIT,
|
MSG_GET_HTTPCACHE,
|
||||||
MSG_FETCH_CLEAR,
|
|
||||||
CACHE_NAME,
|
CACHE_NAME,
|
||||||
DEFAULT_FETCH_INTERVAL,
|
DEFAULT_FETCH_INTERVAL,
|
||||||
DEFAULT_FETCH_LIMIT,
|
DEFAULT_FETCH_LIMIT,
|
||||||
|
DEFAULT_HTTP_TIMEOUT,
|
||||||
} from "../config";
|
} from "../config";
|
||||||
import { isBg } from "./browser";
|
import { isBg } from "./browser";
|
||||||
import { newCacheReq, newTransReq } from "./req";
|
import { genTransReq } from "../apis/trans";
|
||||||
|
import { kissLog } from "./log";
|
||||||
|
import { blobToBase64 } from "./utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造缓存 request
|
||||||
|
* @param {*} input
|
||||||
|
* @param {*} init
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const newCacheReq = async (input, init) => {
|
||||||
|
let request = new Request(input, init);
|
||||||
|
if (request.method !== "GET") {
|
||||||
|
const body = await request.text();
|
||||||
|
const cacheUrl = new URL(request.url);
|
||||||
|
cacheUrl.pathname += body;
|
||||||
|
request = new Request(cacheUrl.toString(), { method: "GET" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return request;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 油猴脚本的请求封装
|
* 油猴脚本的请求封装
|
||||||
@@ -18,7 +40,10 @@ import { newCacheReq, newTransReq } from "./req";
|
|||||||
* @param {*} init
|
* @param {*} init
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const fetchGM = async (input, { method = "GET", headers, body } = {}) =>
|
export const fetchGM = async (
|
||||||
|
input,
|
||||||
|
{ method = "GET", headers, body, timeout } = {}
|
||||||
|
) =>
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
GM.xmlHttpRequest({
|
GM.xmlHttpRequest({
|
||||||
method,
|
method,
|
||||||
@@ -26,7 +51,8 @@ export const fetchGM = async (input, { method = "GET", headers, body } = {}) =>
|
|||||||
headers,
|
headers,
|
||||||
data: body,
|
data: body,
|
||||||
// withCredentials: true,
|
// withCredentials: true,
|
||||||
onload: ({ response, responseHeaders, status, statusText, ...opts }) => {
|
timeout,
|
||||||
|
onload: ({ response, responseHeaders, status, statusText }) => {
|
||||||
const headers = {};
|
const headers = {};
|
||||||
responseHeaders.split("\n").forEach((line) => {
|
responseHeaders.split("\n").forEach((line) => {
|
||||||
const [name, value] = line.split(":").map((item) => item.trim());
|
const [name, value] = line.split(":").map((item) => item.trim());
|
||||||
@@ -50,126 +76,230 @@ export const fetchGM = async (input, { method = "GET", headers, body } = {}) =>
|
|||||||
* @param {*} param0
|
* @param {*} param0
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const fetchApi = async ({ input, init, transOpts, apiSetting }) => {
|
export const fetchPatcher = async (input, init, transOpts, apiSetting) => {
|
||||||
if (transOpts?.translator) {
|
if (transOpts?.translator) {
|
||||||
[input, init] = await newTransReq(transOpts, apiSetting);
|
[input, init] = await genTransReq(transOpts, apiSetting);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!input) {
|
||||||
|
throw new Error("url is empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
let timeout = apiSetting?.httpTimeout || DEFAULT_HTTP_TIMEOUT;
|
||||||
|
if (!apiSetting) {
|
||||||
|
try {
|
||||||
|
timeout = (await getSettingWithDefault()).httpTimeout;
|
||||||
|
} catch (err) {
|
||||||
|
//
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isGm) {
|
if (isGm) {
|
||||||
let info;
|
// let info;
|
||||||
if (window.KISS_GM) {
|
// if (window.KISS_GM) {
|
||||||
info = await window.KISS_GM.getInfo();
|
// info = await window.KISS_GM.getInfo();
|
||||||
} else {
|
// } else {
|
||||||
info = GM.info;
|
// info = GM.info;
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Tampermonkey --> .connects
|
// Tampermonkey --> .connects
|
||||||
// Violentmonkey --> .connect
|
// Violentmonkey --> .connect
|
||||||
const connects = info?.script?.connects || info?.script?.connect || [];
|
// const connects = info?.script?.connects || info?.script?.connect || [];
|
||||||
const url = new URL(input);
|
// const url = new URL(input);
|
||||||
const isSafe = connects.find((item) => url.hostname.endsWith(item));
|
// const isSafe = connects.find((item) => url.hostname.endsWith(item));
|
||||||
|
|
||||||
if (isSafe) {
|
// if (isSafe) {
|
||||||
const { body, headers, status, statusText } = window.KISS_GM
|
// // todo: 自定义接口 init 可能包含了 signal
|
||||||
? await window.KISS_GM.fetch(input, init)
|
// Object.assign(init, { timeout });
|
||||||
: await fetchGM(input, init);
|
|
||||||
|
|
||||||
return new Response(body, {
|
// const { body, headers, status, statusText } = window.KISS_GM
|
||||||
headers: new Headers(headers),
|
// ? await window.KISS_GM.fetch(input, init)
|
||||||
status,
|
// : await fetchGM(input, init);
|
||||||
statusText,
|
|
||||||
});
|
// return new Response(body, {
|
||||||
}
|
// headers: new Headers(headers),
|
||||||
|
// status,
|
||||||
|
// statusText,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// todo: 自定义接口 init 可能包含了 signal
|
||||||
|
Object.assign(init, { timeout });
|
||||||
|
|
||||||
|
const { body, headers, status, statusText } = window.KISS_GM
|
||||||
|
? await window.KISS_GM.fetch(input, init)
|
||||||
|
: await fetchGM(input, init);
|
||||||
|
|
||||||
|
return new Response(body, {
|
||||||
|
headers: new Headers(headers),
|
||||||
|
status,
|
||||||
|
statusText,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AbortSignal?.timeout && !init.signal) {
|
||||||
|
Object.assign(init, { signal: AbortSignal.timeout(timeout) });
|
||||||
}
|
}
|
||||||
|
|
||||||
return fetch(input, init);
|
return fetch(input, init);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 response
|
||||||
|
* @param {*} res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const parseResponse = async (res) => {
|
||||||
|
if (!res) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentType = res.headers.get("Content-Type");
|
||||||
|
if (contentType?.includes("json")) {
|
||||||
|
return await res.json();
|
||||||
|
} else if (contentType?.includes("audio")) {
|
||||||
|
const blob = await res.blob();
|
||||||
|
return await blobToBase64(blob);
|
||||||
|
}
|
||||||
|
return await res.text();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询 caches
|
||||||
|
* @param {*} input
|
||||||
|
* @param {*} param1
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getHttpCache = async (input, { method, headers, body }) => {
|
||||||
|
try {
|
||||||
|
const req = await newCacheReq(input, { method, headers, body });
|
||||||
|
const cache = await caches.open(CACHE_NAME);
|
||||||
|
const res = await cache.match(req);
|
||||||
|
return parseResponse(res);
|
||||||
|
} catch (err) {
|
||||||
|
kissLog(err, "get cache");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插入 caches
|
||||||
|
* @param {*} input
|
||||||
|
* @param {*} param1
|
||||||
|
* @param {*} res
|
||||||
|
*/
|
||||||
|
export const putHttpCache = async (input, { method, headers, body }, res) => {
|
||||||
|
try {
|
||||||
|
const req = await newCacheReq(input, { method, headers, body });
|
||||||
|
const cache = await caches.open(CACHE_NAME);
|
||||||
|
await cache.put(req, res);
|
||||||
|
} catch (err) {
|
||||||
|
kissLog(err, "put cache");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理请求
|
||||||
|
* @param {*} param0
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const fetchHandle = async ({
|
||||||
|
input,
|
||||||
|
useCache,
|
||||||
|
transOpts,
|
||||||
|
apiSetting,
|
||||||
|
...init
|
||||||
|
}) => {
|
||||||
|
// 发送请求
|
||||||
|
const res = await fetchPatcher(input, init, transOpts, apiSetting);
|
||||||
|
if (!res) {
|
||||||
|
throw new Error("Unknow error");
|
||||||
|
} else if (!res.ok) {
|
||||||
|
const msg = {
|
||||||
|
url: res.url,
|
||||||
|
status: res.status,
|
||||||
|
};
|
||||||
|
if (res.headers.get("Content-Type")?.includes("json")) {
|
||||||
|
msg.response = await res.json();
|
||||||
|
}
|
||||||
|
throw new Error(JSON.stringify(msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 插入缓存
|
||||||
|
if (useCache) {
|
||||||
|
await putHttpCache(input, init, res.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseResponse(res);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* fetch 兼容性封装
|
||||||
|
* @param {*} args
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const fetchPolyfill = (args) => {
|
||||||
|
// 插件
|
||||||
|
if (isExt && !isBg()) {
|
||||||
|
return sendBgMsg(MSG_FETCH, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 油猴/网页/BackgroundPage
|
||||||
|
return fetchHandle(args);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getHttpCache 兼容性封装
|
||||||
|
* @param {*} input
|
||||||
|
* @param {*} init
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getHttpCachePolyfill = (input, init) => {
|
||||||
|
// 插件
|
||||||
|
if (isExt && !isBg()) {
|
||||||
|
return sendBgMsg(MSG_GET_HTTPCACHE, { input, init });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 油猴/网页/BackgroundPage
|
||||||
|
return getHttpCache(input, init);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 请求池实例
|
* 请求池实例
|
||||||
*/
|
*/
|
||||||
export const fetchPool = taskPool(
|
export const fetchPool = taskPool(
|
||||||
fetchApi,
|
fetchPolyfill,
|
||||||
null,
|
null,
|
||||||
DEFAULT_FETCH_INTERVAL,
|
DEFAULT_FETCH_INTERVAL,
|
||||||
DEFAULT_FETCH_LIMIT
|
DEFAULT_FETCH_LIMIT
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 请求数据统一接口
|
* 数据请求
|
||||||
* @param {*} input
|
* @param {*} input
|
||||||
* @param {*} opts
|
* @param {*} param1
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const fetchData = async (
|
export const fetchData = async (input, { useCache, usePool, ...args } = {}) => {
|
||||||
input,
|
|
||||||
{ useCache, usePool, transOpts, apiSetting, ...init } = {}
|
|
||||||
) => {
|
|
||||||
const cacheReq = await newCacheReq(input, init);
|
|
||||||
let res;
|
|
||||||
|
|
||||||
// 查询缓存
|
|
||||||
if (useCache) {
|
|
||||||
try {
|
|
||||||
const cache = await caches.open(CACHE_NAME);
|
|
||||||
res = await cache.match(cacheReq);
|
|
||||||
} catch (err) {
|
|
||||||
console.log("[cache match]", err.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!res) {
|
|
||||||
// 发送请求
|
|
||||||
if (usePool) {
|
|
||||||
res = await fetchPool.push({ input, init, transOpts, apiSetting });
|
|
||||||
} else {
|
|
||||||
res = await fetchApi({ input, init, transOpts, apiSetting });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!res?.ok) {
|
|
||||||
throw new Error(`response: ${res.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 插入缓存
|
|
||||||
if (useCache) {
|
|
||||||
try {
|
|
||||||
const cache = await caches.open(CACHE_NAME);
|
|
||||||
await cache.put(cacheReq, res.clone());
|
|
||||||
} catch (err) {
|
|
||||||
console.log("[cache put]", err.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const contentType = res.headers.get("Content-Type");
|
|
||||||
if (contentType?.includes("json")) {
|
|
||||||
return await res.json();
|
|
||||||
}
|
|
||||||
return await res.text();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* fetch 兼容性封装
|
|
||||||
* @param {*} input
|
|
||||||
* @param {*} opts
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const fetchPolyfill = async (input, opts) => {
|
|
||||||
if (!input?.trim()) {
|
if (!input?.trim()) {
|
||||||
throw new Error("URL is empty");
|
throw new Error("URL is empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 插件
|
// 查询缓存
|
||||||
if (isExt && !isBg()) {
|
if (useCache) {
|
||||||
const res = await sendBgMsg(MSG_FETCH, { input, opts });
|
const cache = await getHttpCachePolyfill(input, args);
|
||||||
if (res.error) {
|
if (cache) {
|
||||||
throw new Error(res.error);
|
return cache;
|
||||||
}
|
}
|
||||||
return res.data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 油猴/网页/BackgroundPage
|
// 通过任务池发送请求
|
||||||
return await fetchData(input, opts);
|
if (usePool) {
|
||||||
|
return fetchPool.push({ input, useCache, ...args });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接请求
|
||||||
|
return fetchPolyfill({ input, useCache, ...args });
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -177,27 +307,13 @@ export const fetchPolyfill = async (input, opts) => {
|
|||||||
* @param {*} interval
|
* @param {*} interval
|
||||||
* @param {*} limit
|
* @param {*} limit
|
||||||
*/
|
*/
|
||||||
export const updateFetchPool = async (interval, limit) => {
|
export const updateFetchPool = (interval, limit) => {
|
||||||
if (isExt) {
|
fetchPool.update(interval, limit);
|
||||||
const res = await sendBgMsg(MSG_FETCH_LIMIT, { interval, limit });
|
|
||||||
if (res.error) {
|
|
||||||
throw new Error(res.error);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fetchPool.update(interval, limit);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清空任务池
|
* 清空任务池
|
||||||
*/
|
*/
|
||||||
export const clearFetchPool = async () => {
|
export const clearFetchPool = () => {
|
||||||
if (isExt) {
|
fetchPool.clear();
|
||||||
const res = await sendBgMsg(MSG_FETCH_CLEAR);
|
|
||||||
if (res.error) {
|
|
||||||
throw new Error(res.error);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fetchPool.clear();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,25 @@
|
|||||||
import { CACHE_NAME } from "../config";
|
import {
|
||||||
|
CACHE_NAME,
|
||||||
|
OPT_TRANS_GOOGLE,
|
||||||
|
OPT_TRANS_MICROSOFT,
|
||||||
|
OPT_TRANS_BAIDU,
|
||||||
|
OPT_TRANS_TENCENT,
|
||||||
|
} from "../config";
|
||||||
import { browser } from "./browser";
|
import { browser } from "./browser";
|
||||||
import { apiBaiduLangdetect } from "../apis";
|
import {
|
||||||
|
apiGoogleLangdetect,
|
||||||
|
apiMicrosoftLangdetect,
|
||||||
|
apiBaiduLangdetect,
|
||||||
|
apiTencentLangdetect,
|
||||||
|
} from "../apis";
|
||||||
|
import { kissLog } from "./log";
|
||||||
|
|
||||||
|
const langdetectMap = {
|
||||||
|
[OPT_TRANS_GOOGLE]: apiGoogleLangdetect,
|
||||||
|
[OPT_TRANS_MICROSOFT]: apiMicrosoftLangdetect,
|
||||||
|
[OPT_TRANS_BAIDU]: apiBaiduLangdetect,
|
||||||
|
[OPT_TRANS_TENCENT]: apiTencentLangdetect,
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清除缓存数据
|
* 清除缓存数据
|
||||||
@@ -9,7 +28,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");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -18,14 +37,18 @@ export const tryClearCaches = async () => {
|
|||||||
* @param {*} q
|
* @param {*} q
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const tryDetectLang = async (q, useRemote = false) => {
|
export const tryDetectLang = async (
|
||||||
|
q,
|
||||||
|
useRemote = false,
|
||||||
|
langDetector = OPT_TRANS_MICROSOFT
|
||||||
|
) => {
|
||||||
let lang = "";
|
let lang = "";
|
||||||
|
|
||||||
if (useRemote) {
|
if (useRemote) {
|
||||||
try {
|
try {
|
||||||
lang = await apiBaiduLangdetect(q);
|
lang = await langdetectMap[langDetector](q);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("[detect lang remote]", err.message);
|
kissLog(err, "detect lang remote");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,7 +57,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
35
src/libs/injector.js
Normal 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);
|
||||||
|
};
|
||||||
199
src/libs/inputTranslate.js
Normal file
199
src/libs/inputTranslate.js
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
import {
|
||||||
|
DEFAULT_INPUT_RULE,
|
||||||
|
DEFAULT_TRANS_APIS,
|
||||||
|
DEFAULT_INPUT_SHORTCUT,
|
||||||
|
OPT_LANGS_LIST,
|
||||||
|
} from "../config";
|
||||||
|
import { genEventName, removeEndchar, matchInputStr, sleep } from "./utils";
|
||||||
|
import { stepShortcutRegister } from "./shortcut";
|
||||||
|
import { apiTranslate } from "../apis";
|
||||||
|
import { loadingSvg } from "./svg";
|
||||||
|
import { kissLog } from "./log";
|
||||||
|
|
||||||
|
function isInputNode(node) {
|
||||||
|
return node.nodeName === "INPUT" || node.nodeName === "TEXTAREA";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEditAbleNode(node) {
|
||||||
|
return node.hasAttribute("contenteditable");
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectContent(node) {
|
||||||
|
node.focus();
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNodeContents(node);
|
||||||
|
|
||||||
|
const selection = window.getSelection();
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(range);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pasteContentEvent(node, text) {
|
||||||
|
node.focus();
|
||||||
|
const data = new DataTransfer();
|
||||||
|
data.setData("text/plain", text);
|
||||||
|
|
||||||
|
const event = new ClipboardEvent("paste", { clipboardData: data });
|
||||||
|
document.dispatchEvent(event);
|
||||||
|
data.clearData();
|
||||||
|
}
|
||||||
|
|
||||||
|
function pasteContentCommand(node, text) {
|
||||||
|
node.focus();
|
||||||
|
document.execCommand("insertText", false, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
function collapseToEnd(node) {
|
||||||
|
node.focus();
|
||||||
|
const selection = window.getSelection();
|
||||||
|
selection.collapseToEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNodeText(node) {
|
||||||
|
if (isInputNode(node)) {
|
||||||
|
return node.value;
|
||||||
|
}
|
||||||
|
return node.innerText || node.textContent || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function addLoading(node, loadingId) {
|
||||||
|
const div = document.createElement("div");
|
||||||
|
div.id = loadingId;
|
||||||
|
div.innerHTML = loadingSvg;
|
||||||
|
div.style.cssText = `
|
||||||
|
width: ${node.offsetWidth}px;
|
||||||
|
height: ${node.offsetHeight}px;
|
||||||
|
line-height: ${node.offsetHeight}px;
|
||||||
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
|
left: ${node.offsetLeft}px;
|
||||||
|
top: ${node.offsetTop}px;
|
||||||
|
z-index: 2147483647;
|
||||||
|
`;
|
||||||
|
node.offsetParent?.appendChild(div);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeLoading(node, loadingId) {
|
||||||
|
const div = node.offsetParent.querySelector(`#${loadingId}`);
|
||||||
|
if (div) {
|
||||||
|
div.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 输入框翻译
|
||||||
|
*/
|
||||||
|
export default function inputTranslate({
|
||||||
|
inputRule: {
|
||||||
|
transOpen,
|
||||||
|
triggerShortcut,
|
||||||
|
translator,
|
||||||
|
fromLang,
|
||||||
|
toLang,
|
||||||
|
triggerCount,
|
||||||
|
triggerTime,
|
||||||
|
transSign,
|
||||||
|
} = DEFAULT_INPUT_RULE,
|
||||||
|
transApis,
|
||||||
|
}) {
|
||||||
|
if (!transOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiSetting = transApis?.[translator] || DEFAULT_TRANS_APIS[translator];
|
||||||
|
if (triggerShortcut.length === 0) {
|
||||||
|
triggerShortcut = DEFAULT_INPUT_SHORTCUT;
|
||||||
|
triggerCount = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
stepShortcutRegister(
|
||||||
|
triggerShortcut,
|
||||||
|
async () => {
|
||||||
|
let node = document.activeElement;
|
||||||
|
|
||||||
|
if (!node) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (node.shadowRoot) {
|
||||||
|
node = node.shadowRoot.activeElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isInputNode(node) && !isEditAbleNode(node)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let initText = getNodeText(node);
|
||||||
|
if (triggerShortcut.length === 1 && triggerShortcut[0].length === 1) {
|
||||||
|
// todo: remove multiple char
|
||||||
|
initText = removeEndchar(initText, triggerShortcut[0], triggerCount);
|
||||||
|
}
|
||||||
|
if (!initText.trim()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = initText;
|
||||||
|
if (transSign) {
|
||||||
|
const res = matchInputStr(text, transSign);
|
||||||
|
if (res) {
|
||||||
|
let lang = res[1];
|
||||||
|
if (lang === "zh" || lang === "cn") {
|
||||||
|
lang = "zh-CN";
|
||||||
|
} else if (lang === "tw" || lang === "hk") {
|
||||||
|
lang = "zh-TW";
|
||||||
|
}
|
||||||
|
if (lang && OPT_LANGS_LIST.includes(lang)) {
|
||||||
|
toLang = lang;
|
||||||
|
}
|
||||||
|
text = res[2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log("input -->", text);
|
||||||
|
|
||||||
|
const loadingId = "kiss-" + genEventName();
|
||||||
|
try {
|
||||||
|
addLoading(node, loadingId);
|
||||||
|
|
||||||
|
const [trText, isSame] = await apiTranslate({
|
||||||
|
translator,
|
||||||
|
text,
|
||||||
|
fromLang,
|
||||||
|
toLang,
|
||||||
|
apiSetting,
|
||||||
|
});
|
||||||
|
if (!trText || isSame) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isInputNode(node)) {
|
||||||
|
node.value = trText;
|
||||||
|
node.dispatchEvent(
|
||||||
|
new Event("input", { bubbles: true, cancelable: true })
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectContent(node);
|
||||||
|
await sleep(200);
|
||||||
|
|
||||||
|
pasteContentEvent(node, trText);
|
||||||
|
await sleep(200);
|
||||||
|
|
||||||
|
// todo: use includes?
|
||||||
|
if (getNodeText(node).startsWith(initText)) {
|
||||||
|
pasteContentCommand(node, trText);
|
||||||
|
await sleep(100);
|
||||||
|
} else {
|
||||||
|
collapseToEnd(node);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
kissLog(err, "translate input");
|
||||||
|
} finally {
|
||||||
|
removeLoading(node, loadingId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
triggerCount,
|
||||||
|
triggerTime
|
||||||
|
);
|
||||||
|
}
|
||||||
16
src/libs/interpreter.js
Normal file
16
src/libs/interpreter.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import Sval from "sval";
|
||||||
|
|
||||||
|
const interpreter = new Sval({
|
||||||
|
// ECMA Version of the code
|
||||||
|
// 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15
|
||||||
|
// or 2015 | 2016 | 2017 | 2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024
|
||||||
|
// or "latest"
|
||||||
|
ecmaVer: "latest",
|
||||||
|
// Code source type
|
||||||
|
// "script" or "module"
|
||||||
|
sourceType: "script",
|
||||||
|
// Whether the code runs in a sandbox
|
||||||
|
sandBox: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default interpreter;
|
||||||
12
src/libs/log.js
Normal file
12
src/libs/log.js
Normal 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}`);
|
||||||
|
};
|
||||||
@@ -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];
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
251
src/libs/req.js
251
src/libs/req.js
@@ -1,251 +0,0 @@
|
|||||||
import queryString from "query-string";
|
|
||||||
import {
|
|
||||||
OPT_TRANS_GOOGLE,
|
|
||||||
OPT_TRANS_MICROSOFT,
|
|
||||||
OPT_TRANS_DEEPL,
|
|
||||||
OPT_TRANS_DEEPLFREE,
|
|
||||||
OPT_TRANS_DEEPLX,
|
|
||||||
OPT_TRANS_BAIDU,
|
|
||||||
OPT_TRANS_TENCENT,
|
|
||||||
OPT_TRANS_OPENAI,
|
|
||||||
OPT_TRANS_CLOUDFLAREAI,
|
|
||||||
OPT_TRANS_CUSTOMIZE,
|
|
||||||
URL_MICROSOFT_TRAN,
|
|
||||||
URL_TENCENT_TRANSMART,
|
|
||||||
PROMPT_PLACE_FROM,
|
|
||||||
PROMPT_PLACE_TO,
|
|
||||||
} from "../config";
|
|
||||||
import { msAuth } from "./auth";
|
|
||||||
import { genDeeplFree } from "../apis/deepl";
|
|
||||||
import { genBaidu } from "../apis/baidu";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构造缓存 request
|
|
||||||
* @param {*} request
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const newCacheReq = async (input, init) => {
|
|
||||||
let request = new Request(input, init);
|
|
||||||
if (request.method !== "GET") {
|
|
||||||
const body = await request.text();
|
|
||||||
const cacheUrl = new URL(request.url);
|
|
||||||
cacheUrl.pathname += body;
|
|
||||||
request = new Request(cacheUrl.toString(), { method: "GET" });
|
|
||||||
}
|
|
||||||
|
|
||||||
return request;
|
|
||||||
};
|
|
||||||
|
|
||||||
const genGoogle = ({ text, from, to, url, key }) => {
|
|
||||||
const params = {
|
|
||||||
client: "gtx",
|
|
||||||
dt: "t",
|
|
||||||
dj: 1,
|
|
||||||
ie: "UTF-8",
|
|
||||||
sl: from,
|
|
||||||
tl: to,
|
|
||||||
q: text,
|
|
||||||
};
|
|
||||||
const input = `${url}?${queryString.stringify(params)}`;
|
|
||||||
const init = {
|
|
||||||
headers: {
|
|
||||||
"Content-type": "application/json",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
if (key) {
|
|
||||||
init.headers.Authorization = `Bearer ${key}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [input, init];
|
|
||||||
};
|
|
||||||
|
|
||||||
const genMicrosoft = async ({ text, from, to }) => {
|
|
||||||
const [token] = await msAuth();
|
|
||||||
const params = {
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
"api-version": "3.0",
|
|
||||||
};
|
|
||||||
const input = `${URL_MICROSOFT_TRAN}?${queryString.stringify(params)}`;
|
|
||||||
const init = {
|
|
||||||
headers: {
|
|
||||||
"Content-type": "application/json",
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify([{ Text: text }]),
|
|
||||||
};
|
|
||||||
|
|
||||||
return [input, init];
|
|
||||||
};
|
|
||||||
|
|
||||||
const genDeepl = ({ text, from, to, url, key }) => {
|
|
||||||
const data = {
|
|
||||||
text: [text],
|
|
||||||
target_lang: to,
|
|
||||||
source_lang: from,
|
|
||||||
// split_sentences: "0",
|
|
||||||
};
|
|
||||||
const init = {
|
|
||||||
headers: {
|
|
||||||
"Content-type": "application/json",
|
|
||||||
Authorization: `DeepL-Auth-Key ${key}`,
|
|
||||||
},
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
};
|
|
||||||
|
|
||||||
return [url, init];
|
|
||||||
};
|
|
||||||
|
|
||||||
const genDeeplX = ({ text, from, to, url, key }) => {
|
|
||||||
const data = {
|
|
||||||
text,
|
|
||||||
target_lang: to,
|
|
||||||
source_lang: from,
|
|
||||||
};
|
|
||||||
|
|
||||||
const init = {
|
|
||||||
headers: {
|
|
||||||
"Content-type": "application/json",
|
|
||||||
},
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
};
|
|
||||||
if (key) {
|
|
||||||
init.headers.Authorization = `Bearer ${key}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [url, init];
|
|
||||||
};
|
|
||||||
|
|
||||||
const genTencent = ({ text, from, to }) => {
|
|
||||||
const data = {
|
|
||||||
header: {
|
|
||||||
fn: "auto_translation_block",
|
|
||||||
},
|
|
||||||
source: {
|
|
||||||
text_block: text,
|
|
||||||
lang: from,
|
|
||||||
},
|
|
||||||
target: {
|
|
||||||
lang: to,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const init = {
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
};
|
|
||||||
|
|
||||||
return [URL_TENCENT_TRANSMART, init];
|
|
||||||
};
|
|
||||||
|
|
||||||
const genOpenAI = ({ text, from, to, url, key, prompt, model }) => {
|
|
||||||
prompt = prompt
|
|
||||||
.replaceAll(PROMPT_PLACE_FROM, from)
|
|
||||||
.replaceAll(PROMPT_PLACE_TO, to);
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
model,
|
|
||||||
messages: [
|
|
||||||
{
|
|
||||||
role: "system",
|
|
||||||
content: prompt,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: "user",
|
|
||||||
content: text,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
temperature: 0,
|
|
||||||
max_tokens: 256,
|
|
||||||
};
|
|
||||||
|
|
||||||
const init = {
|
|
||||||
headers: {
|
|
||||||
"Content-type": "application/json",
|
|
||||||
Authorization: `Bearer ${key}`, // OpenAI
|
|
||||||
"api-key": key, // Azure OpenAI
|
|
||||||
},
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
};
|
|
||||||
|
|
||||||
return [url, init];
|
|
||||||
};
|
|
||||||
|
|
||||||
const genCloudflareAI = ({ text, from, to, url, key }) => {
|
|
||||||
const data = {
|
|
||||||
text,
|
|
||||||
source_lang: from,
|
|
||||||
target_lang: to,
|
|
||||||
};
|
|
||||||
|
|
||||||
const init = {
|
|
||||||
headers: {
|
|
||||||
"Content-type": "application/json",
|
|
||||||
Authorization: `Bearer ${key}`,
|
|
||||||
},
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
};
|
|
||||||
|
|
||||||
return [url, init];
|
|
||||||
};
|
|
||||||
|
|
||||||
const genCustom = ({ text, from, to, url, key }) => {
|
|
||||||
const data = {
|
|
||||||
text,
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
};
|
|
||||||
const init = {
|
|
||||||
headers: {
|
|
||||||
"Content-type": "application/json",
|
|
||||||
},
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
};
|
|
||||||
if (key) {
|
|
||||||
init.headers.Authorization = `Bearer ${key}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [url, init];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构造翻译接口 request
|
|
||||||
* @param {*}
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const newTransReq = ({ translator, text, from, to }, apiSetting) => {
|
|
||||||
const args = { text, from, to, ...apiSetting };
|
|
||||||
switch (translator) {
|
|
||||||
case OPT_TRANS_GOOGLE:
|
|
||||||
return genGoogle(args);
|
|
||||||
case OPT_TRANS_MICROSOFT:
|
|
||||||
return genMicrosoft(args);
|
|
||||||
case OPT_TRANS_DEEPL:
|
|
||||||
return genDeepl(args);
|
|
||||||
case OPT_TRANS_DEEPLFREE:
|
|
||||||
return genDeeplFree(args);
|
|
||||||
case OPT_TRANS_DEEPLX:
|
|
||||||
return genDeeplX(args);
|
|
||||||
case OPT_TRANS_BAIDU:
|
|
||||||
return genBaidu(args);
|
|
||||||
case OPT_TRANS_TENCENT:
|
|
||||||
return genTencent(args);
|
|
||||||
case OPT_TRANS_OPENAI:
|
|
||||||
return genOpenAI(args);
|
|
||||||
case OPT_TRANS_CLOUDFLAREAI:
|
|
||||||
return genCloudflareAI(args);
|
|
||||||
case OPT_TRANS_CUSTOMIZE:
|
|
||||||
return genCustom(args);
|
|
||||||
default:
|
|
||||||
throw new Error(`[trans] translator: ${translator} not support`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -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匹配规则
|
||||||
@@ -21,15 +22,10 @@ import { trySyncRules } from "./sync";
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const matchRule = async (
|
export const matchRule = async (
|
||||||
rules,
|
|
||||||
href,
|
href,
|
||||||
{
|
{ injectRules, subrulesList, owSubrule }
|
||||||
injectRules = true,
|
|
||||||
subrulesList = DEFAULT_SUBRULES_LIST,
|
|
||||||
owSubrule = DEFAULT_OW_RULE,
|
|
||||||
}
|
|
||||||
) => {
|
) => {
|
||||||
rules = [...rules];
|
const rules = await getRulesWithDefault();
|
||||||
if (injectRules) {
|
if (injectRules) {
|
||||||
try {
|
try {
|
||||||
const selectedSub = subrulesList.find((item) => item.selected);
|
const selectedSub = subrulesList.find((item) => item.selected);
|
||||||
@@ -54,19 +50,60 @@ 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;
|
[
|
||||||
|
"selector",
|
||||||
|
"keepSelector",
|
||||||
|
"terms",
|
||||||
|
"selectStyle",
|
||||||
|
"parentStyle",
|
||||||
|
"injectJs",
|
||||||
|
"injectCss",
|
||||||
|
"fixerSelector",
|
||||||
|
"transStartHook",
|
||||||
|
"transEndHook",
|
||||||
|
"transRemoveHook",
|
||||||
|
].forEach((key) => {
|
||||||
|
if (!rule[key]?.trim()) {
|
||||||
|
rule[key] = globalRule[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
[
|
||||||
|
"translator",
|
||||||
|
"fromLang",
|
||||||
|
"toLang",
|
||||||
|
"transOpen",
|
||||||
|
"transOnly",
|
||||||
|
"transTiming",
|
||||||
|
"transTag",
|
||||||
|
"transTitle",
|
||||||
|
"transSelected",
|
||||||
|
"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 +112,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;
|
||||||
};
|
};
|
||||||
@@ -113,6 +145,12 @@ export const checkRules = (rules) => {
|
|||||||
({
|
({
|
||||||
pattern,
|
pattern,
|
||||||
selector,
|
selector,
|
||||||
|
keepSelector,
|
||||||
|
terms,
|
||||||
|
selectStyle,
|
||||||
|
parentStyle,
|
||||||
|
injectJs,
|
||||||
|
injectCss,
|
||||||
translator,
|
translator,
|
||||||
fromLang,
|
fromLang,
|
||||||
toLang,
|
toLang,
|
||||||
@@ -120,9 +158,27 @@ export const checkRules = (rules) => {
|
|||||||
transOpen,
|
transOpen,
|
||||||
bgColor,
|
bgColor,
|
||||||
textDiyStyle,
|
textDiyStyle,
|
||||||
|
transOnly,
|
||||||
|
transTiming,
|
||||||
|
transTag,
|
||||||
|
transTitle,
|
||||||
|
transSelected,
|
||||||
|
detectRemote,
|
||||||
|
skipLangs,
|
||||||
|
fixerSelector,
|
||||||
|
fixerFunc,
|
||||||
|
transStartHook,
|
||||||
|
transEndHook,
|
||||||
|
transRemoveHook,
|
||||||
}) => ({
|
}) => ({
|
||||||
pattern: pattern.trim(),
|
pattern: pattern.trim(),
|
||||||
selector: type(selector) === "string" ? selector : "",
|
selector: type(selector) === "string" ? selector : "",
|
||||||
|
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),
|
||||||
@@ -130,6 +186,19 @@ 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),
|
||||||
|
transSelected: matchValue([GLOBAL_KEY, "true", "false"], transSelected),
|
||||||
|
detectRemote: matchValue([GLOBAL_KEY, "true", "false"], detectRemote),
|
||||||
|
skipLangs: type(skipLangs) === "array" ? skipLangs : [],
|
||||||
|
fixerSelector: type(fixerSelector) === "string" ? fixerSelector : "",
|
||||||
|
transStartHook: type(transStartHook) === "string" ? transStartHook : "",
|
||||||
|
transEndHook: type(transEndHook) === "string" ? transEndHook : "",
|
||||||
|
transRemoveHook:
|
||||||
|
type(transRemoveHook) === "string" ? transRemoveHook : "",
|
||||||
|
fixerFunc: matchValue([GLOBAL_KEY, ...FIXER_ALL], fixerFunc),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ export const shortcutListener = (fn, target = document, timeout = 3000) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
target.addEventListener("keydown", handleKeydown);
|
target.addEventListener("keydown", handleKeydown, true);
|
||||||
target.addEventListener("keyup", handleKeyup);
|
target.addEventListener("keyup", handleKeyup, true);
|
||||||
return () => {
|
return () => {
|
||||||
if (timer) {
|
if (timer) {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
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,
|
||||||
@@ -15,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) {
|
||||||
@@ -85,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);
|
||||||
|
|
||||||
@@ -114,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位置
|
||||||
*/
|
*/
|
||||||
@@ -162,6 +156,6 @@ export const tryInitDefaultData = async () => {
|
|||||||
BUILTIN_RULES
|
BUILTIN_RULES
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("[init default]", err);
|
kissLog(err, "init default");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export const loadingSvg = `
|
export const loadingSvg = `
|
||||||
<svg viewBox="0 0 100 100" style="display:inline-block; width:100%; height: 100%;">
|
<svg viewBox="0 0 100 100" style="display:inline-block; width:100%; height: 100%; max-width: 24; max-height: 24;">
|
||||||
<circle fill="#209CEE" stroke="none" cx="6" cy="50" r="6">
|
<circle fill="#209CEE" stroke="none" cx="6" cy="50" r="6">
|
||||||
<animateTransform
|
<animateTransform
|
||||||
attributeName="transform"
|
attributeName="transform"
|
||||||
|
|||||||
@@ -20,12 +20,14 @@ import {
|
|||||||
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 { fetchPatcher } from "./fetch";
|
||||||
|
import { kissLog } from "./log";
|
||||||
|
|
||||||
getPatcher().patch("request", (opts) => {
|
getPatcher().patch("request", (opts) => {
|
||||||
return fetchApi({
|
return fetchPatcher(opts.url, {
|
||||||
input: opts.url,
|
method: opts.method,
|
||||||
init: { method: opts.method, headers: opts.headers, body: opts.data },
|
headers: opts.headers,
|
||||||
|
body: opts.data,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -115,7 +117,7 @@ export const trySyncSetting = async () => {
|
|||||||
try {
|
try {
|
||||||
await syncSetting();
|
await syncSetting();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("[sync setting]", err);
|
kissLog(err, "sync setting");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -134,7 +136,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");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -153,7 +155,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");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
12
src/libs/touch.js
Normal file
12
src/libs/touch.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export function touchTapListener(fn, touchsLength) {
|
||||||
|
const handleTouchend = (e) => {
|
||||||
|
if (e.touches.length === touchsLength) {
|
||||||
|
fn();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("touchstart", handleTouchend);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("touchstart", handleTouchend);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -4,106 +4,34 @@ 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,
|
||||||
DEFAULT_INPUT_RULE,
|
OPT_TIMING_MOUSEOVER,
|
||||||
DEFAULT_TRANS_APIS,
|
DEFAULT_TRANS_APIS,
|
||||||
DEFAULT_INPUT_SHORTCUT,
|
DEFAULT_FETCH_LIMIT,
|
||||||
OPT_LANGS_LIST,
|
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 {
|
import { debounce, genEventName, getHtmlText } from "./utils";
|
||||||
debounce,
|
import { runFixer } from "./webfix";
|
||||||
genEventName,
|
|
||||||
removeEndchar,
|
|
||||||
matchInputStr,
|
|
||||||
sleep,
|
|
||||||
} from "./utils";
|
|
||||||
import { stepShortcutRegister } from "./shortcut";
|
|
||||||
import { apiTranslate } from "../apis";
|
import { apiTranslate } from "../apis";
|
||||||
import { tryDetectLang } from ".";
|
import { sendBgMsg } from "./msg";
|
||||||
import { loadingSvg } from "./svg";
|
import { isExt } from "./client";
|
||||||
|
import { injectInlineJs, injectInternalCss } from "./injector";
|
||||||
function isInputNode(node) {
|
import { kissLog } from "./log";
|
||||||
return node.nodeName === "INPUT" || node.nodeName === "TEXTAREA";
|
import interpreter from "./interpreter";
|
||||||
}
|
|
||||||
|
|
||||||
function isEditAbleNode(node) {
|
|
||||||
return node.hasAttribute("contenteditable");
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectContent(node) {
|
|
||||||
node.focus();
|
|
||||||
const range = document.createRange();
|
|
||||||
range.selectNodeContents(node);
|
|
||||||
|
|
||||||
const selection = window.getSelection();
|
|
||||||
selection.removeAllRanges();
|
|
||||||
selection.addRange(range);
|
|
||||||
}
|
|
||||||
|
|
||||||
function pasteContentEvent(node, text) {
|
|
||||||
node.focus();
|
|
||||||
const data = new DataTransfer();
|
|
||||||
data.setData("text/plain", text);
|
|
||||||
|
|
||||||
const event = new ClipboardEvent("paste", { clipboardData: data });
|
|
||||||
document.dispatchEvent(event);
|
|
||||||
data.clearData();
|
|
||||||
}
|
|
||||||
|
|
||||||
function pasteContentCommand(node, text) {
|
|
||||||
node.focus();
|
|
||||||
document.execCommand("insertText", false, text);
|
|
||||||
}
|
|
||||||
|
|
||||||
function collapseToEnd(node) {
|
|
||||||
node.focus();
|
|
||||||
const selection = window.getSelection();
|
|
||||||
selection.collapseToEnd();
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNodeText(node) {
|
|
||||||
if (isInputNode(node)) {
|
|
||||||
return node.value;
|
|
||||||
}
|
|
||||||
return node.innerText || node.textContent || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function addLoading(node, loadingId) {
|
|
||||||
const div = document.createElement("div");
|
|
||||||
div.id = loadingId;
|
|
||||||
div.innerHTML = loadingSvg;
|
|
||||||
div.style.cssText = `
|
|
||||||
width: ${node.offsetWidth}px;
|
|
||||||
height: ${node.offsetHeight}px;
|
|
||||||
line-height: ${node.offsetHeight}px;
|
|
||||||
position: absolute;
|
|
||||||
text-align: center;
|
|
||||||
left: ${node.offsetLeft}px;
|
|
||||||
top: ${node.offsetTop}px;
|
|
||||||
z-index: 2147483647;
|
|
||||||
`;
|
|
||||||
node.offsetParent?.appendChild(div);
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeLoading(node, loadingId) {
|
|
||||||
const div = node.offsetParent.querySelector(`#${loadingId}`);
|
|
||||||
if (div) {
|
|
||||||
div.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 翻译类
|
* 翻译类
|
||||||
*/
|
*/
|
||||||
export class Translator {
|
export class Translator {
|
||||||
_rule = {};
|
_rule = {};
|
||||||
_inputRule = {};
|
|
||||||
_setting = {};
|
_setting = {};
|
||||||
_rootNodes = new Set();
|
_rootNodes = new Set();
|
||||||
_tranNodes = new Map();
|
_tranNodes = new Map();
|
||||||
@@ -125,14 +53,17 @@ export class Translator {
|
|||||||
];
|
];
|
||||||
_eventName = genEventName();
|
_eventName = genEventName();
|
||||||
_mouseoverNode = null;
|
_mouseoverNode = null;
|
||||||
|
_keepSelector = "";
|
||||||
|
_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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -176,22 +107,35 @@ export class Translator {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(rule, setting) {
|
_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._keepSelector = rule.keepSelector || "";
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
this._inputRule = setting.inputRule || DEFAULT_INPUT_RULE;
|
|
||||||
if (this._inputRule.transOpen) {
|
|
||||||
this._registerInput();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get setting() {
|
get setting() {
|
||||||
@@ -225,6 +169,7 @@ export class Translator {
|
|||||||
|
|
||||||
updateRule = (obj) => {
|
updateRule = (obj) => {
|
||||||
this.rule = { ...this.rule, ...obj };
|
this.rule = { ...this.rule, ...obj };
|
||||||
|
this._updatePool(obj.translator);
|
||||||
};
|
};
|
||||||
|
|
||||||
toggle = () => {
|
toggle = () => {
|
||||||
@@ -245,11 +190,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 [];
|
||||||
};
|
};
|
||||||
@@ -321,10 +280,26 @@ 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
|
||||||
|
if (fixerSelector && fixerFunc !== "-") {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
// 搜索节点
|
// 搜索节点
|
||||||
this._queryNodes();
|
this._queryNodes();
|
||||||
|
|
||||||
@@ -338,152 +313,47 @@ 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);
|
||||||
this._tranNodes.forEach((_, node) => {
|
this._tranNodes.forEach((_, node) => {
|
||||||
node.addEventListener("mouseover", this._handleMouseover);
|
node.addEventListener("mouseenter", this._handleMouseover);
|
||||||
node.addEventListener("mouseout", 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}`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_registerInput = () => {
|
|
||||||
const {
|
|
||||||
triggerShortcut: initTriggerShortcut,
|
|
||||||
translator,
|
|
||||||
fromLang,
|
|
||||||
toLang: initToLang,
|
|
||||||
triggerCount: initTriggerCount,
|
|
||||||
triggerTime,
|
|
||||||
transSign,
|
|
||||||
} = this._inputRule;
|
|
||||||
const apiSetting =
|
|
||||||
this._setting.transApis?.[translator] || DEFAULT_TRANS_APIS[translator];
|
|
||||||
const { detectRemote } = this._setting;
|
|
||||||
|
|
||||||
let triggerShortcut = initTriggerShortcut;
|
|
||||||
let triggerCount = initTriggerCount;
|
|
||||||
if (triggerShortcut.length === 0) {
|
|
||||||
triggerShortcut = DEFAULT_INPUT_SHORTCUT;
|
|
||||||
triggerCount = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
stepShortcutRegister(
|
|
||||||
triggerShortcut,
|
|
||||||
async () => {
|
|
||||||
let node = document.activeElement;
|
|
||||||
|
|
||||||
if (!node) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (node.shadowRoot) {
|
|
||||||
node = node.shadowRoot.activeElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isInputNode(node) && !isEditAbleNode(node)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let initText = getNodeText(node);
|
|
||||||
if (triggerShortcut.length === 1 && triggerShortcut[0].length === 1) {
|
|
||||||
// todo: remove multiple char
|
|
||||||
initText = removeEndchar(initText, triggerShortcut[0], triggerCount);
|
|
||||||
}
|
|
||||||
if (!initText.trim()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let text = initText;
|
|
||||||
let toLang = initToLang;
|
|
||||||
if (transSign) {
|
|
||||||
const res = matchInputStr(text, transSign);
|
|
||||||
if (res) {
|
|
||||||
let lang = res[1];
|
|
||||||
if (lang === "zh" || lang === "cn") {
|
|
||||||
lang = "zh-CN";
|
|
||||||
} else if (lang === "tw" || lang === "hk") {
|
|
||||||
lang = "zh-TW";
|
|
||||||
}
|
|
||||||
if (lang && OPT_LANGS_LIST.includes(lang)) {
|
|
||||||
toLang = lang;
|
|
||||||
}
|
|
||||||
text = res[2];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// console.log("input -->", text);
|
|
||||||
|
|
||||||
const loadingId = "kiss-" + genEventName();
|
|
||||||
try {
|
|
||||||
addLoading(node, loadingId);
|
|
||||||
|
|
||||||
const deLang = await tryDetectLang(text, detectRemote);
|
|
||||||
if (deLang && toLang.includes(deLang)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [trText, isSame] = await apiTranslate({
|
|
||||||
translator,
|
|
||||||
text,
|
|
||||||
fromLang,
|
|
||||||
toLang,
|
|
||||||
apiSetting,
|
|
||||||
});
|
|
||||||
if (!trText || isSame) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isInputNode(node)) {
|
|
||||||
node.value = trText;
|
|
||||||
node.dispatchEvent(
|
|
||||||
new Event("input", { bubbles: true, cancelable: true })
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
selectContent(node);
|
|
||||||
await sleep(200);
|
|
||||||
|
|
||||||
pasteContentEvent(node, trText);
|
|
||||||
await sleep(200);
|
|
||||||
|
|
||||||
// todo: use includes?
|
|
||||||
if (getNodeText(node).startsWith(initText)) {
|
|
||||||
pasteContentCommand(node, trText);
|
|
||||||
await sleep(100);
|
|
||||||
} else {
|
|
||||||
collapseToEnd(node);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.log("[translate input]", err.message);
|
|
||||||
} finally {
|
|
||||||
removeLoading(node, loadingId);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
triggerCount,
|
|
||||||
triggerTime
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
_handleMouseover = (e) => {
|
_handleMouseover = (e) => {
|
||||||
// console.log("mouseover", e);
|
// console.log("mouseenter", e);
|
||||||
if (!this._tranNodes.has(e.target)) {
|
if (!this._tranNodes.has(e.target)) {
|
||||||
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("mouseover", this._handleMouseover);
|
e.target.removeEventListener("mouseenter", this._handleMouseover);
|
||||||
e.target.removeEventListener("mouseout", this._handleMouseout);
|
e.target.removeEventListener("mouseleave", this._handleMouseout);
|
||||||
this._render(e.target);
|
this._render(e.target);
|
||||||
} else {
|
} else {
|
||||||
this._mouseoverNode = e.target;
|
this._mouseoverNode = e.target;
|
||||||
@@ -491,7 +361,7 @@ export class Translator {
|
|||||||
};
|
};
|
||||||
|
|
||||||
_handleMouseout = (e) => {
|
_handleMouseout = (e) => {
|
||||||
// console.log("mouseout", e);
|
// console.log("mouseleave", e);
|
||||||
if (!this._tranNodes.has(e.target)) {
|
if (!this._tranNodes.has(e.target)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -501,13 +371,16 @@ 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(
|
||||||
"mouseover",
|
"mouseenter",
|
||||||
this._handleMouseover
|
this._handleMouseover
|
||||||
);
|
);
|
||||||
this._mouseoverNode.removeEventListener("mouseout", this._handleMouseout);
|
this._mouseoverNode.removeEventListener(
|
||||||
|
"mouseleave",
|
||||||
|
this._handleMouseout
|
||||||
|
);
|
||||||
|
|
||||||
const node = this._mouseoverNode;
|
const node = this._mouseoverNode;
|
||||||
this._render(node);
|
this._render(node);
|
||||||
@@ -516,32 +389,53 @@ 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
|
|
||||||
) {
|
const { transRemoveHook } = this._rule;
|
||||||
// 解除节点显示监听
|
this._tranNodes.forEach((innerHTML, node) => {
|
||||||
this._tranNodes.forEach((_, node) => {
|
if (
|
||||||
|
!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();
|
// 移除鼠标悬停监听
|
||||||
});
|
// node.style.pointerEvents = "none";
|
||||||
} else {
|
node.removeEventListener("mouseenter", this._handleMouseover);
|
||||||
// 移除鼠标悬停监听
|
node.removeEventListener("mouseleave", this._handleMouseout);
|
||||||
window.removeEventListener("keydown", this._handleKeydown);
|
}
|
||||||
this._tranNodes.forEach((_, node) => {
|
|
||||||
node.removeEventListener("mouseover", this._handleMouseover);
|
// 移除/恢复元素
|
||||||
node.removeEventListener("mouseout", this._handleMouseout);
|
if (innerHTML) {
|
||||||
// 移除已插入元素
|
if (this._rule.transOnly === "true") {
|
||||||
node.querySelector(APP_LCNAME)?.remove();
|
node.innerHTML = innerHTML;
|
||||||
});
|
} else {
|
||||||
}
|
node.querySelector(APP_LCNAME)?.remove();
|
||||||
|
}
|
||||||
|
// 钩子函数
|
||||||
|
if (transRemoveHook?.trim()) {
|
||||||
|
interpreter.run(`exports.transRemoveHook = ${transRemoveHook}`);
|
||||||
|
interpreter.exports.transRemoveHook(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 移除用户JS/CSS
|
||||||
|
this._removeInjector();
|
||||||
|
|
||||||
// 清空节点集合
|
// 清空节点集合
|
||||||
this._rootNodes.clear();
|
this._rootNodes.clear();
|
||||||
@@ -551,56 +445,119 @@ 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) =>
|
||||||
|
!q ||
|
||||||
|
q.length < (this._setting.minLength ?? TRANS_MIN_LENGTH) ||
|
||||||
|
q.length > (this._setting.maxLength ?? TRANS_MAX_LENGTH);
|
||||||
|
|
||||||
_render = (el) => {
|
_render = (el) => {
|
||||||
let traEl = el.querySelector(APP_LCNAME);
|
let traEl = el.querySelector(APP_LCNAME);
|
||||||
|
|
||||||
// 已翻译
|
// 已翻译
|
||||||
if (traEl) {
|
if (traEl) {
|
||||||
const preText = this._tranNodes.get(el);
|
if (this._rule.transOnly === "true") {
|
||||||
const curText = el.innerText.trim();
|
return;
|
||||||
// const traText = traEl.innerText.trim();
|
}
|
||||||
|
|
||||||
// todo
|
const preText = getHtmlText(this._tranNodes.get(el));
|
||||||
// 1. traText when loading
|
const curText = getHtmlText(el.innerHTML, APP_LCNAME);
|
||||||
// 2. replace startsWith
|
if (preText === curText) {
|
||||||
if (curText.startsWith(preText)) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
traEl.remove();
|
traEl.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
const q = el.innerText.trim();
|
// 缓存已翻译元素
|
||||||
this._tranNodes.set(el, q);
|
this._tranNodes.set(el, el.innerHTML);
|
||||||
|
|
||||||
|
let q = el.innerText.trim();
|
||||||
|
const keeps = [];
|
||||||
|
|
||||||
|
// 翻译开始钩子函数
|
||||||
|
const { transStartHook } = this._rule;
|
||||||
|
if (transStartHook?.trim()) {
|
||||||
|
interpreter.run(`exports.transStartHook = ${transStartHook}`);
|
||||||
|
interpreter.exports.transStartHook(el, q);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保留元素
|
||||||
|
const keepSelector = this._keepSelector.trim();
|
||||||
|
if (keepSelector) {
|
||||||
|
let text = "";
|
||||||
|
el.childNodes.forEach((child) => {
|
||||||
|
if (child.nodeType === 1 && child.matches(keepSelector)) {
|
||||||
|
if (child.nodeName === "IMG") {
|
||||||
|
child.style.cssText += `width: ${child.width}px;`;
|
||||||
|
child.style.cssText += `height: ${child.height}px;`;
|
||||||
|
}
|
||||||
|
text += `[${keeps.length}]`;
|
||||||
|
keeps.push(child.outerHTML);
|
||||||
|
} else if (child.nodeType === 1 || child.nodeType === 3) {
|
||||||
|
text += child.textContent;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (keeps.length > 0) {
|
||||||
|
// textContent会保留些无用的换行符,严重影响翻译质量
|
||||||
|
if (q.includes("\n")) {
|
||||||
|
q = text;
|
||||||
|
} else {
|
||||||
|
q = text.replaceAll("\n", " ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 太长或太短
|
// 太长或太短
|
||||||
if (
|
if (this._invalidLength(q.replace(/\[(\d+)\]/g, "").trim())) {
|
||||||
!q ||
|
|
||||||
q.length < (this._setting.minLength ?? TRANS_MIN_LENGTH) ||
|
|
||||||
q.length > (this._setting.maxLength ?? TRANS_MAX_LENGTH)
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log("---> ", q);
|
// 专业术语
|
||||||
|
if (this._terms.length > 0) {
|
||||||
traEl = document.createElement(APP_LCNAME);
|
for (const term of this._terms) {
|
||||||
traEl.style.visibility = "visible";
|
const re = new RegExp(term[0], "g");
|
||||||
el.appendChild(traEl);
|
q = q.replace(re, (t) => {
|
||||||
el.style.cssText +=
|
const text = `[${keeps.length}]`;
|
||||||
"-webkit-line-clamp: unset; max-height: none; height: auto;";
|
keeps.push(`<i class="kiss-trem">${term[1] || t}</i>`);
|
||||||
if (el.parentElement) {
|
return text;
|
||||||
el.parentElement.style.cssText +=
|
});
|
||||||
"-webkit-line-clamp: unset; max-height: none; height: auto;";
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 附加样式
|
||||||
|
const { selectStyle, parentStyle } = this._rule;
|
||||||
|
el.style.cssText += selectStyle;
|
||||||
|
if (el.parentElement) {
|
||||||
|
el.parentElement.style.cssText += parentStyle;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 插入译文节点
|
||||||
|
traEl = document.createElement(APP_LCNAME);
|
||||||
|
traEl.style.visibility = "visible";
|
||||||
|
// if (this._rule.transOnly === "true") {
|
||||||
|
// el.innerHTML = "";
|
||||||
|
// }
|
||||||
|
el.appendChild(traEl);
|
||||||
|
|
||||||
|
// 渲染译文节点
|
||||||
const root = createRoot(traEl);
|
const root = createRoot(traEl);
|
||||||
root.render(<Content q={q} translator={this} />);
|
root.render(<Content q={q} keeps={keeps} translator={this} $el={el} />);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,16 @@ export const limitNumber = (num, min = 0, max = 100) => {
|
|||||||
return number;
|
return number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const limitFloat = (num, min = 0, max = 100) => {
|
||||||
|
const number = parseFloat(num);
|
||||||
|
if (Number.isNaN(number) || number < min) {
|
||||||
|
return min;
|
||||||
|
} else if (number > max) {
|
||||||
|
return max;
|
||||||
|
}
|
||||||
|
return number;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 匹配是否为数组中的值
|
* 匹配是否为数组中的值
|
||||||
* @param {*} arr
|
* @param {*} arr
|
||||||
@@ -202,26 +212,20 @@ export const removeEndchar = (s, c, count = 1) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const matchInputStr = (str, sign) => {
|
export const matchInputStr = (str, sign) => {
|
||||||
let reg = /\/([\w-]+)\s+([^]+)/;
|
|
||||||
switch (sign) {
|
switch (sign) {
|
||||||
case "//":
|
case "//":
|
||||||
reg = /\/\/([\w-]+)\s+([^]+)/;
|
return str.match(/\/\/([\w-]+)\s+([^]+)/);
|
||||||
break;
|
|
||||||
case "\\":
|
case "\\":
|
||||||
reg = /\\([\w-]+)\s+([^]+)/;
|
return str.match(/\\([\w-]+)\s+([^]+)/);
|
||||||
break;
|
|
||||||
case "\\\\":
|
case "\\\\":
|
||||||
reg = /\\\\([\w-]+)\s+([^]+)/;
|
return str.match(/\\\\([\w-]+)\s+([^]+)/);
|
||||||
break;
|
|
||||||
case ">":
|
case ">":
|
||||||
reg = />([\w-]+)\s+([^]+)/;
|
return str.match(/>([\w-]+)\s+([^]+)/);
|
||||||
break;
|
|
||||||
case ">>":
|
case ">>":
|
||||||
reg = />>([\w-]+)\s+([^]+)/;
|
return str.match(/>>([\w-]+)\s+([^]+)/);
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
return str.match(reg);
|
return str.match(/\/([\w-]+)\s+([^]+)/);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -233,3 +237,33 @@ 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);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取html内的文本
|
||||||
|
* @param {*} htmlStr
|
||||||
|
* @param {*} skipTag
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getHtmlText = (htmlStr, skipTag = "") => {
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(htmlStr, "text/html");
|
||||||
|
|
||||||
|
if (skipTag) {
|
||||||
|
doc.querySelectorAll(skipTag).forEach((el) => el.remove());
|
||||||
|
}
|
||||||
|
|
||||||
|
return doc.body.innerText.trim();
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,52 +1,24 @@
|
|||||||
import { isMatch } from "./utils";
|
|
||||||
import { getWebfix, setWebfix } 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_FONTSIZE = "fontSize";
|
export const FIXER_BN = "bn";
|
||||||
|
export const FIXER_BR_DIV = "brToDiv";
|
||||||
|
export const FIXER_BN_DIV = "bnToDiv";
|
||||||
|
|
||||||
/**
|
export const FIXER_ALL = [
|
||||||
* 需要修复的站点列表
|
FIXER_NONE,
|
||||||
* - pattern 匹配网址
|
FIXER_BR,
|
||||||
* - selector 需要修复的选择器
|
FIXER_BN,
|
||||||
* - rootSelector 需要监听的选择器,可留空
|
FIXER_BR_DIV,
|
||||||
* - fixer 修复函数,可针对不同网址,选用不同修复函数
|
FIXER_BN_DIV,
|
||||||
*/
|
|
||||||
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` 换行网站的修复函数
|
||||||
@@ -54,14 +26,14 @@ const fixedSign = "kissfixed";
|
|||||||
* @param {*} node
|
* @param {*} node
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
function brFixer(node) {
|
function brFixer(node, tag = "p") {
|
||||||
if (node.hasAttribute(fixedSign)) {
|
if (node.hasAttribute(fixedSign)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
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",
|
||||||
@@ -76,68 +48,56 @@ function brFixer(node) {
|
|||||||
"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 += "<p>";
|
html += `<${tag} class="kiss-p">`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (gapTags.indexOf(child.nodeName) !== -1) {
|
if (gapTags.indexOf(child.nodeName) !== -1) {
|
||||||
html += "</p><p>";
|
html += `</${tag}><${tag} class="kiss-p">`;
|
||||||
} else if (newlineTags.indexOf(child.nodeName) !== -1) {
|
} else if (newlineTags.indexOf(child.nodeName) !== -1) {
|
||||||
html += "</p>" + child.outerHTML + "<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) {
|
||||||
html += "</p>";
|
html += `</${tag}>`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
node.innerHTML = html;
|
node.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function brDivFixer(node) {
|
||||||
|
return brFixer(node, "div");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 目标是将 `\n` 替换成 `p`
|
* 目标是将 `\n` 替换成 `p`
|
||||||
* @param {*} node
|
* @param {*} node
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
function bnFixer(node) {
|
function bnFixer(node, tag = "p") {
|
||||||
if (node.hasAttribute(fixedSign)) {
|
if (node.hasAttribute(fixedSign)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
node.setAttribute(fixedSign, "true");
|
node.setAttribute(fixedSign, "true");
|
||||||
|
node.innerHTML = node.innerHTML
|
||||||
const childs = node.childNodes;
|
.split("\n")
|
||||||
if (childs.length === 1 && childs[0].nodeName === "#text") {
|
.map((item) => `<${tag} class="kiss-p">${item || " "}</${tag}>`)
|
||||||
node.innerHTML = node.innerHTML
|
.join("");
|
||||||
.split("\n")
|
|
||||||
.map((item) => `<p>${item || " "}</p>`)
|
|
||||||
.join("");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function bnDivFixer(node) {
|
||||||
* 修复字体大小问题,如 baidu.com
|
return bnFixer(node, "div");
|
||||||
* @param {*} node
|
|
||||||
*/
|
|
||||||
function fontSizeFixer(node) {
|
|
||||||
node.style.cssText += "font-size:1em;";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 修复程序映射
|
|
||||||
*/
|
|
||||||
const fixerMap = {
|
|
||||||
[FIXER_BR]: brFixer,
|
|
||||||
[FIXER_BN]: bnFixer,
|
|
||||||
[FIXER_FONTSIZE]: fontSizeFixer,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查找、监听节点,并执行修复函数
|
* 查找、监听节点,并执行修复函数
|
||||||
* @param {*} selector
|
* @param {*} selector
|
||||||
@@ -145,21 +105,27 @@ 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) {
|
||||||
addNode.querySelectorAll(selector).forEach(fixer);
|
if (addNode && addNode.querySelectorAll) {
|
||||||
|
addNode.querySelectorAll(selector).forEach(function (node) {
|
||||||
|
fixer(node);
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
var rootNodes = [document];
|
let rootNodes = [document];
|
||||||
if (rootSelector) {
|
if (rootSelector) {
|
||||||
rootNodes = document.querySelectorAll(rootSelector);
|
rootNodes = document.querySelectorAll(rootSelector);
|
||||||
}
|
}
|
||||||
|
|
||||||
rootNodes.forEach(function (rootNode) {
|
rootNodes.forEach(function (rootNode) {
|
||||||
rootNode.querySelectorAll(selector).forEach(fixer);
|
rootNode.querySelectorAll(selector).forEach(function (node) {
|
||||||
|
fixer(node);
|
||||||
|
});
|
||||||
mutaObserver.observe(rootNode, {
|
mutaObserver.observe(rootNode, {
|
||||||
childList: true,
|
childList: true,
|
||||||
subtree: true,
|
subtree: true,
|
||||||
@@ -168,55 +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,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从缓存或远程加载修复站点
|
* 执行fixer
|
||||||
* @param {*} url
|
* @param {*} param0
|
||||||
* @returns
|
|
||||||
*/
|
*/
|
||||||
export const loadOrFetchWebfix = async (url) => {
|
export function runFixer(selector, fixer = "-", rootSelector) {
|
||||||
try {
|
try {
|
||||||
let sites = await getWebfix(url);
|
if (Object.keys(fixerMap).includes(fixer)) {
|
||||||
if (sites?.length) {
|
run(selector, fixerMap[fixer], rootSelector);
|
||||||
return sites;
|
|
||||||
}
|
|
||||||
return syncWebfix(url);
|
|
||||||
} catch (err) {
|
|
||||||
console.log("[load webfix]", err.message);
|
|
||||||
return DEFAULT_SITES;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 匹配站点
|
|
||||||
*/
|
|
||||||
export async function runWebfix({ injectWebfix }) {
|
|
||||||
try {
|
|
||||||
if (!injectWebfix) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const href = document.location.href;
|
|
||||||
const sites = await loadOrFetchWebfix(process.env.REACT_APP_WEBFIXURL);
|
|
||||||
for (var i = 0; i < sites.length; i++) {
|
|
||||||
var site = sites[i];
|
|
||||||
if (isMatch(href, site.pattern)) {
|
|
||||||
if (fixerMap[site.fixer]) {
|
|
||||||
run(site.selector, fixerMap[site.fixer], site.rootSelector);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`[kiss-webfix]: ${err.message}`);
|
console.error(`[kiss-webfix run]: ${err.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -1,77 +1,3 @@
|
|||||||
import { getSettingWithDefault } from "./libs/storage";
|
import { run } from "./common";
|
||||||
import { trySyncAllSubRules } from "./libs/subRules";
|
|
||||||
import { isIframe } from "./libs/iframe";
|
|
||||||
import { handlePing, injectScript } from "./libs/gm";
|
|
||||||
import { genEventName } from "./libs/utils";
|
|
||||||
import { runWebfix } from "./libs/webfix";
|
|
||||||
import {
|
|
||||||
runIframe,
|
|
||||||
runTranslator,
|
|
||||||
showFab,
|
|
||||||
showTransbox,
|
|
||||||
windowListener,
|
|
||||||
showErr,
|
|
||||||
} from "./common";
|
|
||||||
|
|
||||||
function runSettingPage() {
|
run(true);
|
||||||
if (GM?.info?.script?.grant?.includes("unsafeWindow")) {
|
|
||||||
unsafeWindow.GM = GM;
|
|
||||||
unsafeWindow.APP_INFO = {
|
|
||||||
name: process.env.REACT_APP_NAME,
|
|
||||||
version: process.env.REACT_APP_VERSION,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
const ping = genEventName();
|
|
||||||
window.addEventListener(ping, handlePing);
|
|
||||||
// window.eval(`(${injectScript})("${ping}")`); // eslint-disable-line
|
|
||||||
const script = document.createElement("script");
|
|
||||||
script.textContent = `(${injectScript})("${ping}")`;
|
|
||||||
document.head.append(script);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 入口函数
|
|
||||||
*/
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
// 设置页面
|
|
||||||
if (
|
|
||||||
document.location.href.includes(process.env.REACT_APP_OPTIONSPAGE_DEV) ||
|
|
||||||
document.location.href.includes(process.env.REACT_APP_OPTIONSPAGE) ||
|
|
||||||
document.location.href.includes(process.env.REACT_APP_OPTIONSPAGE2)
|
|
||||||
) {
|
|
||||||
runSettingPage();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 读取设置信息
|
|
||||||
const setting = await getSettingWithDefault();
|
|
||||||
|
|
||||||
// 适配iframe
|
|
||||||
if (isIframe) {
|
|
||||||
runIframe(setting);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 不规范网页修复
|
|
||||||
await runWebfix(setting);
|
|
||||||
|
|
||||||
// 翻译网页
|
|
||||||
const { translator, rule } = await runTranslator(setting);
|
|
||||||
|
|
||||||
// 监听消息
|
|
||||||
windowListener(rule);
|
|
||||||
|
|
||||||
// 划词翻译
|
|
||||||
showTransbox(setting);
|
|
||||||
|
|
||||||
// 浮球按钮
|
|
||||||
await showFab(translator);
|
|
||||||
|
|
||||||
// 同步订阅规则
|
|
||||||
await trySyncAllSubRules(setting);
|
|
||||||
} catch (err) {
|
|
||||||
showErr(err);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ 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";
|
||||||
|
import { getI18n } from "../../hooks/I18n";
|
||||||
|
|
||||||
export default function Action({ translator, fab }) {
|
export default function Action({ translator, fab }) {
|
||||||
const fabWidth = 40;
|
const fabWidth = 40;
|
||||||
@@ -95,40 +97,42 @@ export default function Action({ translator, fab }) {
|
|||||||
// 注册菜单
|
// 注册菜单
|
||||||
try {
|
try {
|
||||||
const menuCommandIds = [];
|
const menuCommandIds = [];
|
||||||
menuCommandIds.push(
|
const { contextMenuType, uiLang } = translator.setting;
|
||||||
GM.registerMenuCommand(
|
contextMenuType !== 0 &&
|
||||||
"Toggle Translate (Alt+q)",
|
menuCommandIds.push(
|
||||||
(event) => {
|
GM.registerMenuCommand(
|
||||||
translator.toggle();
|
getI18n(uiLang, "translate_switch"),
|
||||||
sendIframeMsg(MSG_TRANS_TOGGLE);
|
(event) => {
|
||||||
setShowPopup(false);
|
translator.toggle();
|
||||||
},
|
sendIframeMsg(MSG_TRANS_TOGGLE);
|
||||||
"Q"
|
setShowPopup(false);
|
||||||
),
|
},
|
||||||
GM.registerMenuCommand(
|
"Q"
|
||||||
"Toggle Style (Alt+c)",
|
),
|
||||||
(event) => {
|
GM.registerMenuCommand(
|
||||||
translator.toggleStyle();
|
getI18n(uiLang, "toggle_style"),
|
||||||
sendIframeMsg(MSG_TRANS_TOGGLE_STYLE);
|
(event) => {
|
||||||
setShowPopup(false);
|
translator.toggleStyle();
|
||||||
},
|
sendIframeMsg(MSG_TRANS_TOGGLE_STYLE);
|
||||||
"C"
|
setShowPopup(false);
|
||||||
),
|
},
|
||||||
GM.registerMenuCommand(
|
"C"
|
||||||
"Open Menu (Alt+k)",
|
),
|
||||||
(event) => {
|
GM.registerMenuCommand(
|
||||||
setShowPopup((pre) => !pre);
|
getI18n(uiLang, "open_menu"),
|
||||||
},
|
(event) => {
|
||||||
"K"
|
setShowPopup((pre) => !pre);
|
||||||
),
|
},
|
||||||
GM.registerMenuCommand(
|
"K"
|
||||||
"Open Setting (Alt+o)",
|
),
|
||||||
(event) => {
|
GM.registerMenuCommand(
|
||||||
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank");
|
getI18n(uiLang, "open_setting"),
|
||||||
},
|
(event) => {
|
||||||
"O"
|
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank");
|
||||||
)
|
},
|
||||||
);
|
"O"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
menuCommandIds.forEach((id) => {
|
menuCommandIds.forEach((id) => {
|
||||||
@@ -136,7 +140,7 @@ export default function Action({ translator, fab }) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("[registerMenuCommand]", err);
|
kissLog(err, "registerMenuCommand");
|
||||||
}
|
}
|
||||||
}, [translator]);
|
}, [translator]);
|
||||||
|
|
||||||
@@ -215,7 +219,12 @@ export default function Action({ translator, fab }) {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TranslateIcon />
|
<TranslateIcon
|
||||||
|
sx={{
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Fab>
|
</Fab>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -7,98 +7,96 @@ import {
|
|||||||
OPT_STYLE_WAVYLINE,
|
OPT_STYLE_WAVYLINE,
|
||||||
OPT_STYLE_FUZZY,
|
OPT_STYLE_FUZZY,
|
||||||
OPT_STYLE_HIGHLIGHT,
|
OPT_STYLE_HIGHLIGHT,
|
||||||
|
OPT_STYLE_BLOCKQUOTE,
|
||||||
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";
|
||||||
|
import interpreter from "../../libs/interpreter";
|
||||||
|
|
||||||
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 FuzzySpan = styled("span")`
|
export default function Content({ q, keeps, translator, $el }) {
|
||||||
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_DIY: // 自定义
|
|
||||||
return <DiySpan $diyStyle={textDiyStyle}>{children}</DiySpan>;
|
|
||||||
default:
|
|
||||||
return <span>{children}</span>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Content({ q, 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,
|
||||||
|
transEndHook,
|
||||||
|
} = 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;
|
||||||
@@ -117,27 +115,74 @@ export default function Content({ q, translator }) {
|
|||||||
};
|
};
|
||||||
}, [translator.eventName]);
|
}, [translator.eventName]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 运行钩子函数
|
||||||
|
if (text && transEndHook?.trim()) {
|
||||||
|
interpreter.run(`exports.transEndHook = ${transEndHook}`);
|
||||||
|
interpreter.exports.transEndHook($el, q, text, keeps);
|
||||||
|
}
|
||||||
|
}, [$el, q, text, keeps, transEndHook]);
|
||||||
|
|
||||||
|
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 />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (text && !sameLang) {
|
if (!text || sameLang) {
|
||||||
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{q.length >= newlineLength ? <br /> : " "}
|
{gap}
|
||||||
<StyledSpan
|
<StyledSpan
|
||||||
textStyle={textStyle}
|
{...styles}
|
||||||
textDiyStyle={textDiyStyle}
|
dangerouslySetInnerHTML={{
|
||||||
bgColor={bgColor}
|
__html: text.replace(/\[(\d+)\]/g, (_, p) => keeps[parseInt(p)]),
|
||||||
>
|
}}
|
||||||
{text}
|
/>
|
||||||
</StyledSpan>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{gap}
|
||||||
|
<StyledSpan {...styles}>{text}</StyledSpan>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,39 @@
|
|||||||
import Stack from "@mui/material/Stack";
|
import Stack from "@mui/material/Stack";
|
||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import CircularProgress from "@mui/material/CircularProgress";
|
import LoadingButton from "@mui/lab/LoadingButton";
|
||||||
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
|
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||||
|
import Switch from "@mui/material/Switch";
|
||||||
import {
|
import {
|
||||||
OPT_TRANS_ALL,
|
OPT_TRANS_ALL,
|
||||||
OPT_TRANS_MICROSOFT,
|
OPT_TRANS_MICROSOFT,
|
||||||
|
OPT_TRANS_DEEPL,
|
||||||
|
OPT_TRANS_DEEPLX,
|
||||||
OPT_TRANS_DEEPLFREE,
|
OPT_TRANS_DEEPLFREE,
|
||||||
OPT_TRANS_BAIDU,
|
OPT_TRANS_BAIDU,
|
||||||
OPT_TRANS_TENCENT,
|
OPT_TRANS_TENCENT,
|
||||||
|
OPT_TRANS_VOLCENGINE,
|
||||||
OPT_TRANS_OPENAI,
|
OPT_TRANS_OPENAI,
|
||||||
|
OPT_TRANS_OPENAI_2,
|
||||||
|
OPT_TRANS_OPENAI_3,
|
||||||
|
OPT_TRANS_GEMINI,
|
||||||
|
OPT_TRANS_GEMINI_2,
|
||||||
|
OPT_TRANS_CLAUDE,
|
||||||
|
OPT_TRANS_CLOUDFLAREAI,
|
||||||
|
OPT_TRANS_OLLAMA,
|
||||||
|
OPT_TRANS_OLLAMA_2,
|
||||||
|
OPT_TRANS_OLLAMA_3,
|
||||||
OPT_TRANS_CUSTOMIZE,
|
OPT_TRANS_CUSTOMIZE,
|
||||||
URL_KISS_PROXY,
|
OPT_TRANS_CUSTOMIZE_2,
|
||||||
|
OPT_TRANS_CUSTOMIZE_3,
|
||||||
|
OPT_TRANS_CUSTOMIZE_4,
|
||||||
|
OPT_TRANS_CUSTOMIZE_5,
|
||||||
|
OPT_TRANS_NIUTRANS,
|
||||||
|
URL_NIUTRANS_REG,
|
||||||
|
DEFAULT_FETCH_LIMIT,
|
||||||
|
DEFAULT_FETCH_INTERVAL,
|
||||||
|
DEFAULT_HTTP_TIMEOUT,
|
||||||
} from "../../config";
|
} from "../../config";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useI18n } from "../../hooks/I18n";
|
import { useI18n } from "../../hooks/I18n";
|
||||||
@@ -25,6 +48,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, limitFloat } from "../../libs/utils";
|
||||||
|
|
||||||
function TestButton({ translator, api }) {
|
function TestButton({ translator, api }) {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
@@ -42,49 +66,158 @@ function TestButton({ translator, api }) {
|
|||||||
useCache: false,
|
useCache: false,
|
||||||
});
|
});
|
||||||
if (!text) {
|
if (!text) {
|
||||||
throw new Error("empty reault");
|
throw new Error("empty result");
|
||||||
}
|
}
|
||||||
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(
|
||||||
|
<>
|
||||||
|
<div>{i18n("test_failed")}</div>
|
||||||
|
{msg === err.message ? (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
maxWidth: 400,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{msg}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<pre
|
||||||
|
style={{
|
||||||
|
maxWidth: 400,
|
||||||
|
overflow: "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{msg}
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return <CircularProgress size={16} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button size="small" variant="contained" onClick={handleApiTest}>
|
<LoadingButton
|
||||||
|
size="small"
|
||||||
|
variant="contained"
|
||||||
|
onClick={handleApiTest}
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
{i18n("click_test")}
|
{i18n("click_test")}
|
||||||
</Button>
|
</LoadingButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ApiFields({ translator }) {
|
function ApiFields({ translator, api, updateApi, resetApi }) {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const { api, updateApi, resetApi } = useApi(translator);
|
const {
|
||||||
const { url = "", key = "", model = "", prompt = "" } = api;
|
url = "",
|
||||||
|
key = "",
|
||||||
|
model = "",
|
||||||
|
systemPrompt = "",
|
||||||
|
userPrompt = "",
|
||||||
|
think = false,
|
||||||
|
thinkIgnore = "",
|
||||||
|
fetchLimit = DEFAULT_FETCH_LIMIT,
|
||||||
|
fetchInterval = DEFAULT_FETCH_INTERVAL,
|
||||||
|
httpTimeout = DEFAULT_HTTP_TIMEOUT,
|
||||||
|
dictNo = "",
|
||||||
|
memoryNo = "",
|
||||||
|
reqHook = "",
|
||||||
|
resHook = "",
|
||||||
|
temperature = 0,
|
||||||
|
maxTokens = 256,
|
||||||
|
apiName = "",
|
||||||
|
isDisabled = false,
|
||||||
|
} = 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;
|
||||||
|
case "httpTimeout":
|
||||||
|
value = limitNumber(value, 5000, 30000);
|
||||||
|
break;
|
||||||
|
case "temperature":
|
||||||
|
value = limitFloat(value, 0, 2);
|
||||||
|
break;
|
||||||
|
case "maxTokens":
|
||||||
|
value = limitNumber(value, 0, 2 ** 15);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
updateApi({
|
updateApi({
|
||||||
[name]: value,
|
[name]: value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildinTranslators = [
|
const builtinTranslators = [
|
||||||
OPT_TRANS_MICROSOFT,
|
OPT_TRANS_MICROSOFT,
|
||||||
OPT_TRANS_DEEPLFREE,
|
OPT_TRANS_DEEPLFREE,
|
||||||
OPT_TRANS_BAIDU,
|
OPT_TRANS_BAIDU,
|
||||||
OPT_TRANS_TENCENT,
|
OPT_TRANS_TENCENT,
|
||||||
|
OPT_TRANS_VOLCENGINE,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const mulkeysTranslators = [
|
||||||
|
OPT_TRANS_DEEPL,
|
||||||
|
OPT_TRANS_OPENAI,
|
||||||
|
OPT_TRANS_OPENAI_2,
|
||||||
|
OPT_TRANS_OPENAI_3,
|
||||||
|
OPT_TRANS_GEMINI,
|
||||||
|
OPT_TRANS_GEMINI_2,
|
||||||
|
OPT_TRANS_CLAUDE,
|
||||||
|
OPT_TRANS_CLOUDFLAREAI,
|
||||||
|
OPT_TRANS_OLLAMA,
|
||||||
|
OPT_TRANS_OLLAMA_2,
|
||||||
|
OPT_TRANS_OLLAMA_3,
|
||||||
|
OPT_TRANS_NIUTRANS,
|
||||||
|
OPT_TRANS_CUSTOMIZE,
|
||||||
|
OPT_TRANS_CUSTOMIZE_2,
|
||||||
|
OPT_TRANS_CUSTOMIZE_3,
|
||||||
|
OPT_TRANS_CUSTOMIZE_4,
|
||||||
|
OPT_TRANS_CUSTOMIZE_5,
|
||||||
|
];
|
||||||
|
|
||||||
|
const keyHelper =
|
||||||
|
translator === OPT_TRANS_NIUTRANS ? (
|
||||||
|
<>
|
||||||
|
{i18n("mulkeys_help")}
|
||||||
|
<Link href={URL_NIUTRANS_REG} target="_blank">
|
||||||
|
{i18n("reg_niutrans")}
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
) : mulkeysTranslators.includes(translator) ? (
|
||||||
|
i18n("mulkeys_help")
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing={3}>
|
<Stack spacing={3}>
|
||||||
{!buildinTranslators.includes(translator) && (
|
<TextField
|
||||||
|
size="small"
|
||||||
|
label={i18n("api_name")}
|
||||||
|
name="apiName"
|
||||||
|
value={apiName}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!builtinTranslators.includes(translator) && (
|
||||||
<>
|
<>
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
@@ -92,6 +225,11 @@ function ApiFields({ translator }) {
|
|||||||
name="url"
|
name="url"
|
||||||
value={url}
|
value={url}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
multiline={translator === OPT_TRANS_DEEPLX}
|
||||||
|
maxRows={10}
|
||||||
|
helperText={
|
||||||
|
translator === OPT_TRANS_DEEPLX ? i18n("mulkeys_help") : ""
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
@@ -99,10 +237,17 @@ function ApiFields({ translator }) {
|
|||||||
name="key"
|
name="key"
|
||||||
value={key}
|
value={key}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
multiline={mulkeysTranslators.includes(translator)}
|
||||||
|
maxRows={10}
|
||||||
|
helperText={keyHelper}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{translator === OPT_TRANS_OPENAI && (
|
|
||||||
|
{(translator.startsWith(OPT_TRANS_OPENAI) ||
|
||||||
|
translator.startsWith(OPT_TRANS_OLLAMA) ||
|
||||||
|
translator === OPT_TRANS_CLAUDE ||
|
||||||
|
translator.startsWith(OPT_TRANS_GEMINI)) && (
|
||||||
<>
|
<>
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
@@ -113,31 +258,169 @@ function ApiFields({ translator }) {
|
|||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
label={"PROMPT"}
|
label={"SYSTEM PROMPT"}
|
||||||
name="prompt"
|
name="systemPrompt"
|
||||||
value={prompt}
|
value={systemPrompt}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
multiline
|
multiline
|
||||||
|
maxRows={10}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
label={"USER PROMPT"}
|
||||||
|
name="userPrompt"
|
||||||
|
value={userPrompt}
|
||||||
|
onChange={handleChange}
|
||||||
|
multiline
|
||||||
|
maxRows={10}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{translator.startsWith(OPT_TRANS_OLLAMA) && (
|
||||||
|
<>
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
size="small"
|
||||||
|
name="think"
|
||||||
|
value={think}
|
||||||
|
label={i18n("if_think")}
|
||||||
|
onChange={handleChange}
|
||||||
|
>
|
||||||
|
<MenuItem value={false}>{i18n("nothink")}</MenuItem>
|
||||||
|
<MenuItem value={true}>{i18n("think")}</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
label={i18n("think_ignore")}
|
||||||
|
name="thinkIgnore"
|
||||||
|
value={thinkIgnore}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(translator.startsWith(OPT_TRANS_OPENAI) ||
|
||||||
|
translator === OPT_TRANS_CLAUDE ||
|
||||||
|
translator === OPT_TRANS_GEMINI ||
|
||||||
|
translator === OPT_TRANS_GEMINI_2) && (
|
||||||
|
<>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
label={"Temperature"}
|
||||||
|
type="number"
|
||||||
|
name="temperature"
|
||||||
|
value={temperature}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
label={"Max Tokens"}
|
||||||
|
type="number"
|
||||||
|
name="maxTokens"
|
||||||
|
value={maxTokens}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{translator === OPT_TRANS_NIUTRANS && (
|
||||||
|
<>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
label={"DictNo"}
|
||||||
|
name="dictNo"
|
||||||
|
value={dictNo}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
label={"MemoryNo"}
|
||||||
|
name="memoryNo"
|
||||||
|
value={memoryNo}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{translator.startsWith(OPT_TRANS_CUSTOMIZE) && (
|
||||||
|
<>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
label={"Request Hook"}
|
||||||
|
name="reqHook"
|
||||||
|
value={reqHook}
|
||||||
|
onChange={handleChange}
|
||||||
|
multiline
|
||||||
|
maxRows={10}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
label={"Response Hook"}
|
||||||
|
name="resHook"
|
||||||
|
value={resHook}
|
||||||
|
onChange={handleChange}
|
||||||
|
multiline
|
||||||
|
maxRows={10}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<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}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
label={i18n("http_timeout")}
|
||||||
|
type="number"
|
||||||
|
name="httpTimeout"
|
||||||
|
defaultValue={httpTimeout}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
name="isDisabled"
|
||||||
|
checked={isDisabled}
|
||||||
|
onChange={() => {
|
||||||
|
updateApi({ isDisabled: !isDisabled });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={i18n("is_disabled")}
|
||||||
|
/>
|
||||||
|
|
||||||
<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>
|
||||||
@@ -146,6 +429,7 @@ function ApiFields({ translator }) {
|
|||||||
|
|
||||||
function ApiAccordion({ translator }) {
|
function ApiAccordion({ translator }) {
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
const { api, updateApi, resetApi } = useApi(translator);
|
||||||
|
|
||||||
const handleChange = (e) => {
|
const handleChange = (e) => {
|
||||||
setExpanded((pre) => !pre);
|
setExpanded((pre) => !pre);
|
||||||
@@ -154,10 +438,19 @@ function ApiAccordion({ translator }) {
|
|||||||
return (
|
return (
|
||||||
<Accordion expanded={expanded} onChange={handleChange}>
|
<Accordion expanded={expanded} onChange={handleChange}>
|
||||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||||
<Typography>{translator}</Typography>
|
<Typography>
|
||||||
|
{api.apiName ? `${translator} (${api.apiName})` : translator}
|
||||||
|
</Typography>
|
||||||
</AccordionSummary>
|
</AccordionSummary>
|
||||||
<AccordionDetails>
|
<AccordionDetails>
|
||||||
{expanded && <ApiFields translator={translator} />}
|
{expanded && (
|
||||||
|
<ApiFields
|
||||||
|
translator={translator}
|
||||||
|
api={api}
|
||||||
|
updateApi={updateApi}
|
||||||
|
resetApi={resetApi}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</AccordionDetails>
|
</AccordionDetails>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
);
|
);
|
||||||
@@ -168,11 +461,7 @@ export default function Apis() {
|
|||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Stack spacing={3}>
|
<Stack spacing={3}>
|
||||||
<Alert severity="info">
|
<Alert severity="info">{i18n("about_api")}</Alert>
|
||||||
<Link href={URL_KISS_PROXY} target="_blank">
|
|
||||||
{i18n("about_api_proxy")}
|
|
||||||
</Link>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
{OPT_TRANS_ALL.map((translator) => (
|
{OPT_TRANS_ALL.map((translator) => (
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import FileDownloadIcon from "@mui/icons-material/FileDownload";
|
import FileDownloadIcon from "@mui/icons-material/FileDownload";
|
||||||
import Button from "@mui/material/Button";
|
import LoadingButton from "@mui/lab/LoadingButton";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { kissLog } from "../../libs/log";
|
||||||
|
|
||||||
export default function DownloadButton({ data, text, fileName }) {
|
export default function DownloadButton({ handleData, text, fileName }) {
|
||||||
const handleClick = (e) => {
|
const [loading, setLoading] = useState(false);
|
||||||
|
const handleClick = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (data) {
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const data = await handleData();
|
||||||
const url = window.URL.createObjectURL(new Blob([data]));
|
const url = window.URL.createObjectURL(new Blob([data]));
|
||||||
const link = document.createElement("a");
|
const link = document.createElement("a");
|
||||||
link.href = url;
|
link.href = url;
|
||||||
@@ -12,16 +17,21 @@ export default function DownloadButton({ data, text, fileName }) {
|
|||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
link.click();
|
link.click();
|
||||||
link.remove();
|
link.remove();
|
||||||
|
} catch (err) {
|
||||||
|
kissLog(err, "download");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Button
|
<LoadingButton
|
||||||
size="small"
|
size="small"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
loading={loading}
|
||||||
startIcon={<FileDownloadIcon />}
|
startIcon={<FileDownloadIcon />}
|
||||||
>
|
>
|
||||||
{text}
|
{text}
|
||||||
</Button>
|
</LoadingButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import Stack from "@mui/material/Stack";
|
import Stack from "@mui/material/Stack";
|
||||||
import { OPT_TRANS_BAIDU } from "../../config";
|
import { useState } from "react";
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import Accordion from "@mui/material/Accordion";
|
import Accordion from "@mui/material/Accordion";
|
||||||
import AccordionSummary from "@mui/material/AccordionSummary";
|
import AccordionSummary from "@mui/material/AccordionSummary";
|
||||||
@@ -8,52 +7,18 @@ import AccordionDetails from "@mui/material/AccordionDetails";
|
|||||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||||
import CircularProgress from "@mui/material/CircularProgress";
|
import CircularProgress from "@mui/material/CircularProgress";
|
||||||
import { useI18n } from "../../hooks/I18n";
|
import { useI18n } from "../../hooks/I18n";
|
||||||
import Alert from "@mui/material/Alert";
|
|
||||||
import { apiTranslate } from "../../apis";
|
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import { useFavWords } from "../../hooks/FavWords";
|
import { useFavWords } from "../../hooks/FavWords";
|
||||||
import DictCont from "../Selection/DictCont";
|
import DictCont from "../Selection/DictCont";
|
||||||
|
import SugCont from "../Selection/SugCont";
|
||||||
import DownloadButton from "./DownloadButton";
|
import DownloadButton from "./DownloadButton";
|
||||||
import UploadButton from "./UploadButton";
|
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 }) {
|
import { apiTranslate } from "../../apis";
|
||||||
const [dictResult, setDictResult] = useState(null);
|
import { OPT_TRANS_BAIDU, PHONIC_MAP } from "../../config";
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState("");
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
setError("");
|
|
||||||
const dictRes = await apiTranslate({
|
|
||||||
text: word,
|
|
||||||
translator: OPT_TRANS_BAIDU,
|
|
||||||
fromLang: "en",
|
|
||||||
toLang: "zh-CN",
|
|
||||||
});
|
|
||||||
setDictResult(dictRes[2].dict_result);
|
|
||||||
} catch (err) {
|
|
||||||
setError(err.message);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}, [word]);
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return <CircularProgress size={24} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return <Alert severity="error">{error}</Alert>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <DictCont dictResult={dictResult} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function FavAccordion({ word, index }) {
|
function FavAccordion({ word, index }) {
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
@@ -71,7 +36,12 @@ function FavAccordion({ word, index }) {
|
|||||||
<Typography>{`${index + 1}. ${word}`}</Typography>
|
<Typography>{`${index + 1}. ${word}`}</Typography>
|
||||||
</AccordionSummary>
|
</AccordionSummary>
|
||||||
<AccordionDetails>
|
<AccordionDetails>
|
||||||
{expanded && <DictField word={word} />}
|
{expanded && (
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<DictCont text={word} />
|
||||||
|
<SugCont text={word} />
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
</AccordionDetails>
|
</AccordionDetails>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
);
|
);
|
||||||
@@ -93,10 +63,47 @@ 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");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleTranslation = async () => {
|
||||||
|
const tranList = [];
|
||||||
|
for (const text of downloadList) {
|
||||||
|
try {
|
||||||
|
const dictRes = await apiTranslate({
|
||||||
|
text,
|
||||||
|
translator: OPT_TRANS_BAIDU,
|
||||||
|
fromLang: "en",
|
||||||
|
toLang: "zh-CN",
|
||||||
|
});
|
||||||
|
if (dictRes[2]?.type === 1) {
|
||||||
|
tranList.push(JSON.parse(dictRes[2].result));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// skip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tranList
|
||||||
|
.map((dictResult) =>
|
||||||
|
[
|
||||||
|
`## ${dictResult.src}`,
|
||||||
|
dictResult.voice
|
||||||
|
?.map(Object.entries)
|
||||||
|
.map((item) => item[0])
|
||||||
|
.map(([key, val]) => `${PHONIC_MAP[key]?.[0] || key} ${val}`)
|
||||||
|
.join(" "),
|
||||||
|
dictResult.content[0].mean
|
||||||
|
.map(({ pre, cont }) => {
|
||||||
|
return ` - ${pre ? `[${pre}] ` : ""}${Object.keys(cont).join("; ")}`;
|
||||||
|
})
|
||||||
|
.join("\n"),
|
||||||
|
].join("\n\n")
|
||||||
|
)
|
||||||
|
.join("\n\n");
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Stack spacing={3}>
|
<Stack spacing={3}>
|
||||||
@@ -114,10 +121,15 @@ export default function FavWords() {
|
|||||||
fileExts={[".txt", ".csv"]}
|
fileExts={[".txt", ".csv"]}
|
||||||
/>
|
/>
|
||||||
<DownloadButton
|
<DownloadButton
|
||||||
data={downloadList.join("\n")}
|
handleData={() => downloadList.join("\n")}
|
||||||
text={i18n("export")}
|
text={i18n("export")}
|
||||||
fileName={`kiss-words_${Date.now()}.txt`}
|
fileName={`kiss-words_${Date.now()}.txt`}
|
||||||
/>
|
/>
|
||||||
|
<DownloadButton
|
||||||
|
handleData={handleTranslation}
|
||||||
|
text={i18n("export_translation")}
|
||||||
|
fileName={`kiss-words_${Date.now()}.md`}
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import Box from "@mui/material/Box";
|
|||||||
import Link from "@mui/material/Link";
|
import Link from "@mui/material/Link";
|
||||||
import { useI18n } from "../../hooks/I18n";
|
import { useI18n } from "../../hooks/I18n";
|
||||||
import DarkModeButton from "./DarkModeButton";
|
import DarkModeButton from "./DarkModeButton";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
|
||||||
function Header(props) {
|
function Header(props) {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
@@ -30,14 +31,14 @@ function Header(props) {
|
|||||||
<MenuIcon />
|
<MenuIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ flexGrow: 1 }}>
|
<Typography component="div" sx={{ flexGrow: 1, fontWeight: "bold" }}>
|
||||||
<Link
|
<Link
|
||||||
underline="none"
|
underline="none"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
href={process.env.REACT_APP_HOMEPAGE}
|
href={process.env.REACT_APP_HOMEPAGE}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>{`${i18n("app_name")} v${process.env.REACT_APP_VERSION}`}</Link>
|
>{`${i18n("app_name")} v${process.env.REACT_APP_VERSION}`}</Link>
|
||||||
</Box>
|
</Typography>
|
||||||
<DarkModeButton />
|
<DarkModeButton />
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
|
|||||||
@@ -11,10 +11,9 @@ 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";
|
||||||
|
|
||||||
function LinkItem({ label, url, icon }) {
|
function LinkItem({ label, url, icon }) {
|
||||||
const match = useMatch(url);
|
const match = useMatch(url);
|
||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -23,6 +27,7 @@ import Accordion from "@mui/material/Accordion";
|
|||||||
import AccordionSummary from "@mui/material/AccordionSummary";
|
import AccordionSummary from "@mui/material/AccordionSummary";
|
||||||
import AccordionDetails from "@mui/material/AccordionDetails";
|
import AccordionDetails from "@mui/material/AccordionDetails";
|
||||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||||
|
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
|
||||||
import { useRules } from "../../hooks/Rules";
|
import { useRules } from "../../hooks/Rules";
|
||||||
import MenuItem from "@mui/material/MenuItem";
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
import Grid from "@mui/material/Grid";
|
import Grid from "@mui/material/Grid";
|
||||||
@@ -50,11 +55,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,9 +73,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 = "",
|
||||||
|
terms = "",
|
||||||
|
selectStyle = "",
|
||||||
|
parentStyle = "",
|
||||||
|
injectJs = "",
|
||||||
|
injectCss = "",
|
||||||
translator,
|
translator,
|
||||||
fromLang,
|
fromLang,
|
||||||
toLang,
|
toLang,
|
||||||
@@ -72,6 +90,18 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
|
|||||||
transOpen,
|
transOpen,
|
||||||
bgColor,
|
bgColor,
|
||||||
textDiyStyle,
|
textDiyStyle,
|
||||||
|
transOnly = "false",
|
||||||
|
transTiming = OPT_TIMING_PAGESCROLL,
|
||||||
|
transTag = DEFAULT_TRANS_TAG,
|
||||||
|
transTitle = "false",
|
||||||
|
transSelected = "true",
|
||||||
|
detectRemote = "false",
|
||||||
|
skipLangs = [],
|
||||||
|
fixerSelector = "",
|
||||||
|
fixerFunc = "-",
|
||||||
|
transStartHook = "",
|
||||||
|
transEndHook = "",
|
||||||
|
transRemoveHook = "",
|
||||||
} = formValues;
|
} = formValues;
|
||||||
|
|
||||||
const hasSamePattern = (str) => {
|
const hasSamePattern = (str) => {
|
||||||
@@ -152,6 +182,30 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const ShowMoreButton = showMore ? (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
onClick={() => {
|
||||||
|
setShowMore(false);
|
||||||
|
}}
|
||||||
|
startIcon={<ExpandLessIcon />}
|
||||||
|
>
|
||||||
|
{i18n("less")}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
onClick={() => {
|
||||||
|
setShowMore(true);
|
||||||
|
}}
|
||||||
|
startIcon={<ExpandMoreIcon />}
|
||||||
|
>
|
||||||
|
{i18n("more")}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<Stack spacing={2}>
|
<Stack spacing={2}>
|
||||||
@@ -179,6 +233,16 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
|
|||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
multiline
|
multiline
|
||||||
/>
|
/>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
label={i18n("keep_selector")}
|
||||||
|
helperText={i18n("keep_selector_helper")}
|
||||||
|
name="keepSelector"
|
||||||
|
value={keepSelector}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={handleChange}
|
||||||
|
multiline
|
||||||
|
/>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Grid container spacing={2} columns={12}>
|
<Grid container spacing={2} columns={12}>
|
||||||
@@ -299,10 +363,258 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
|
|||||||
value={textDiyStyle}
|
value={textDiyStyle}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
maxRows={10}
|
||||||
multiline
|
multiline
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<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("trigger_mode")}
|
||||||
|
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="transSelected"
|
||||||
|
value={transSelected}
|
||||||
|
label={i18n("translate_selected")}
|
||||||
|
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>
|
||||||
|
|
||||||
|
{showMore && (
|
||||||
|
<>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
label={i18n("fixer_selector")}
|
||||||
|
name="fixerSelector"
|
||||||
|
value={fixerSelector}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={handleChange}
|
||||||
|
multiline
|
||||||
|
maxRows={10}
|
||||||
|
/>
|
||||||
|
<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
|
||||||
|
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
|
||||||
|
maxRows={10}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
label={i18n("translate_start_hook")}
|
||||||
|
helperText={i18n("translate_start_hook_helper")}
|
||||||
|
name="transStartHook"
|
||||||
|
value={transStartHook}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={handleChange}
|
||||||
|
multiline
|
||||||
|
maxRows={10}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
label={i18n("translate_end_hook")}
|
||||||
|
helperText={i18n("translate_end_hook_helper")}
|
||||||
|
name="transEndHook"
|
||||||
|
value={transEndHook}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={handleChange}
|
||||||
|
multiline
|
||||||
|
maxRows={10}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
label={i18n("translate_remove_hook")}
|
||||||
|
helperText={i18n("translate_remove_hook_helper")}
|
||||||
|
name="transRemoveHook"
|
||||||
|
value={transRemoveHook}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={handleChange}
|
||||||
|
multiline
|
||||||
|
maxRows={10}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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 ? (
|
||||||
// 编辑
|
// 编辑
|
||||||
@@ -316,6 +628,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setDisabled(false);
|
setDisabled(false);
|
||||||
}}
|
}}
|
||||||
|
startIcon={<EditIcon />}
|
||||||
>
|
>
|
||||||
{i18n("edit")}
|
{i18n("edit")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -327,35 +640,55 @@ 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>
|
||||||
)}
|
)}
|
||||||
|
{ShowMoreButton}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<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>
|
||||||
|
{ShowMoreButton}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</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>
|
||||||
|
{ShowMoreButton}
|
||||||
</Stack>
|
</Stack>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -418,7 +751,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");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -448,7 +781,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");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -485,13 +818,14 @@ function UserRules({ subRules }) {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setShowAdd(true);
|
setShowAdd(true);
|
||||||
}}
|
}}
|
||||||
|
startIcon={<AddIcon />}
|
||||||
>
|
>
|
||||||
{i18n("add")}
|
{i18n("add")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<UploadButton text={i18n("import")} handleImport={handleImport} />
|
<UploadButton text={i18n("import")} handleImport={handleImport} />
|
||||||
<DownloadButton
|
<DownloadButton
|
||||||
data={JSON.stringify([...rules.list].reverse(), null, 2)}
|
handleData={() => JSON.stringify([...rules.list].reverse(), null, 2)}
|
||||||
text={i18n("export")}
|
text={i18n("export")}
|
||||||
fileName={`kiss-rules_${Date.now()}.json`}
|
fileName={`kiss-rules_${Date.now()}.json`}
|
||||||
/>
|
/>
|
||||||
@@ -580,7 +914,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");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -593,7 +927,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);
|
||||||
}
|
}
|
||||||
@@ -672,7 +1006,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);
|
||||||
@@ -700,6 +1034,7 @@ function SubRulesEdit({ subList, addSub, updateDataCache }) {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setShowInput(true);
|
setShowInput(true);
|
||||||
}}
|
}}
|
||||||
|
startIcon={<AddIcon />}
|
||||||
>
|
>
|
||||||
{i18n("add")}
|
{i18n("add")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -724,10 +1059,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>
|
||||||
@@ -815,6 +1156,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" }}>
|
||||||
|
|||||||
@@ -17,16 +17,25 @@ import {
|
|||||||
UI_LANGS,
|
UI_LANGS,
|
||||||
TRANS_NEWLINE_LENGTH,
|
TRANS_NEWLINE_LENGTH,
|
||||||
CACHE_NAME,
|
CACHE_NAME,
|
||||||
OPT_MOUSEKEY_ALL,
|
OPT_TRANS_MICROSOFT,
|
||||||
OPT_MOUSEKEY_DISABLE,
|
OPT_LANGDETECTOR_ALL,
|
||||||
OPT_SHORTCUT_TRANSLATE,
|
OPT_SHORTCUT_TRANSLATE,
|
||||||
OPT_SHORTCUT_STYLE,
|
OPT_SHORTCUT_STYLE,
|
||||||
OPT_SHORTCUT_POPUP,
|
OPT_SHORTCUT_POPUP,
|
||||||
OPT_SHORTCUT_SETTING,
|
OPT_SHORTCUT_SETTING,
|
||||||
|
DEFAULT_BLACKLIST,
|
||||||
|
DEFAULT_CSPLIST,
|
||||||
|
MSG_CONTEXT_MENUS,
|
||||||
|
MSG_UPDATE_CSP,
|
||||||
|
DEFAULT_HTTP_TIMEOUT,
|
||||||
} 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 { kissLog } from "../../libs/log";
|
||||||
|
import UploadButton from "./UploadButton";
|
||||||
|
import DownloadButton from "./DownloadButton";
|
||||||
|
|
||||||
function ShortcutItem({ action, label }) {
|
function ShortcutItem({ action, label }) {
|
||||||
const { shortcut, setShortcut } = useShortcut(action);
|
const { shortcut, setShortcut } = useShortcut(action);
|
||||||
@@ -51,6 +60,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;
|
||||||
@@ -60,6 +72,18 @@ export default function Settings() {
|
|||||||
case "newlineLength":
|
case "newlineLength":
|
||||||
value = limitNumber(value, 1, 1000);
|
value = limitNumber(value, 1, 1000);
|
||||||
break;
|
break;
|
||||||
|
case "httpTimeout":
|
||||||
|
value = limitNumber(value, 5000, 30000);
|
||||||
|
break;
|
||||||
|
case "touchTranslate":
|
||||||
|
value = limitNumber(value, 0, 4);
|
||||||
|
break;
|
||||||
|
case "contextMenuType":
|
||||||
|
isExt && sendBgMsg(MSG_CONTEXT_MENUS, value);
|
||||||
|
break;
|
||||||
|
case "csplist":
|
||||||
|
isExt && sendBgMsg(MSG_UPDATE_CSP, value);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
updateSetting({
|
updateSetting({
|
||||||
@@ -72,26 +96,52 @@ 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 handleImport = async (data) => {
|
||||||
|
try {
|
||||||
|
await updateSetting(JSON.parse(data));
|
||||||
|
} catch (err) {
|
||||||
|
kissLog(err, "import setting");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const {
|
const {
|
||||||
uiLang,
|
uiLang,
|
||||||
fetchLimit,
|
|
||||||
fetchInterval,
|
|
||||||
minLength,
|
minLength,
|
||||||
maxLength,
|
maxLength,
|
||||||
clearCache,
|
clearCache,
|
||||||
newlineLength = TRANS_NEWLINE_LENGTH,
|
newlineLength = TRANS_NEWLINE_LENGTH,
|
||||||
mouseKey = OPT_MOUSEKEY_DISABLE,
|
httpTimeout = DEFAULT_HTTP_TIMEOUT,
|
||||||
detectRemote = false,
|
contextMenuType = 1,
|
||||||
|
touchTranslate = 2,
|
||||||
|
blacklist = DEFAULT_BLACKLIST.join(",\n"),
|
||||||
|
csplist = DEFAULT_CSPLIST.join(",\n"),
|
||||||
|
transInterval = 500,
|
||||||
|
langDetector = OPT_TRANS_MICROSOFT,
|
||||||
} = setting;
|
} = setting;
|
||||||
const { isHide = false } = fab || {};
|
const { isHide = false } = fab || {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Stack spacing={3}>
|
<Stack spacing={3}>
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
alignItems="center"
|
||||||
|
spacing={2}
|
||||||
|
useFlexGap
|
||||||
|
flexWrap="wrap"
|
||||||
|
>
|
||||||
|
<UploadButton text={i18n("import")} handleImport={handleImport} />
|
||||||
|
<DownloadButton
|
||||||
|
handleData={() => JSON.stringify(setting, null, 2)}
|
||||||
|
text={i18n("export")}
|
||||||
|
fileName={`kiss-setting_${Date.now()}.json`}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
<FormControl size="small">
|
<FormControl size="small">
|
||||||
<InputLabel>{i18n("ui_lang")}</InputLabel>
|
<InputLabel>{i18n("ui_lang")}</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
@@ -108,24 +158,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")}
|
||||||
@@ -153,17 +185,33 @@ export default function Settings() {
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
label={i18n("translate_interval")}
|
||||||
|
type="number"
|
||||||
|
name="transInterval"
|
||||||
|
defaultValue={transInterval}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
label={i18n("http_timeout")}
|
||||||
|
type="number"
|
||||||
|
name="httpTimeout"
|
||||||
|
defaultValue={httpTimeout}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
<FormControl size="small">
|
<FormControl size="small">
|
||||||
<InputLabel>{i18n("mouseover_translation")}</InputLabel>
|
<InputLabel>{i18n("touch_translate_shortcut")}</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
name="mouseKey"
|
name="touchTranslate"
|
||||||
value={mouseKey}
|
value={touchTranslate}
|
||||||
label={i18n("mouseover_translation")}
|
label={i18n("touch_translate_shortcut")}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
>
|
>
|
||||||
{OPT_MOUSEKEY_ALL.map((item) => (
|
{[0, 2, 3, 4].map((item) => (
|
||||||
<MenuItem key={item} value={item}>
|
<MenuItem key={item} value={item}>
|
||||||
{i18n(item)}
|
{i18n(`touch_tap_${item}`)}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
@@ -184,38 +232,68 @@ export default function Settings() {
|
|||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl size="small">
|
||||||
|
<InputLabel>{i18n("context_menus")}</InputLabel>
|
||||||
|
<Select
|
||||||
|
name="contextMenuType"
|
||||||
|
value={contextMenuType}
|
||||||
|
label={i18n("context_menus")}
|
||||||
|
onChange={handleChange}
|
||||||
|
>
|
||||||
|
<MenuItem value={0}>{i18n("hide_context_menus")}</MenuItem>
|
||||||
|
<MenuItem value={1}>{i18n("simple_context_menus")}</MenuItem>
|
||||||
|
<MenuItem value={2}>{i18n("secondary_context_menus")}</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
<FormControl size="small">
|
<FormControl size="small">
|
||||||
<InputLabel>{i18n("detect_lang_remote")}</InputLabel>
|
<InputLabel>{i18n("detect_lang_remote")}</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
name="detectRemote"
|
name="langDetector"
|
||||||
value={detectRemote}
|
value={langDetector}
|
||||||
label={i18n("detect_lang_remote")}
|
label={i18n("detect_lang_remote")}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
>
|
>
|
||||||
<MenuItem value={false}>{i18n("disable")}</MenuItem>
|
{OPT_LANGDETECTOR_ALL.map((item) => (
|
||||||
<MenuItem value={true}>{i18n("enable")}</MenuItem>
|
<MenuItem value={item} key={item}>
|
||||||
|
{item}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
<FormHelperText>{i18n("detect_lang_remote_help")}</FormHelperText>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
{isExt ? (
|
{isExt ? (
|
||||||
<FormControl size="small">
|
<>
|
||||||
<InputLabel>{i18n("if_clear_cache")}</InputLabel>
|
<FormControl size="small">
|
||||||
<Select
|
<InputLabel>{i18n("if_clear_cache")}</InputLabel>
|
||||||
name="clearCache"
|
<Select
|
||||||
value={clearCache}
|
name="clearCache"
|
||||||
label={i18n("if_clear_cache")}
|
value={clearCache}
|
||||||
|
label={i18n("if_clear_cache")}
|
||||||
|
onChange={handleChange}
|
||||||
|
>
|
||||||
|
<MenuItem value={false}>{i18n("clear_cache_never")}</MenuItem>
|
||||||
|
<MenuItem value={true}>{i18n("clear_cache_restart")}</MenuItem>
|
||||||
|
</Select>
|
||||||
|
<FormHelperText>
|
||||||
|
<Link component="button" onClick={handleClearCache}>
|
||||||
|
{i18n("clear_all_cache_now")}
|
||||||
|
</Link>
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
label={i18n("disabled_csplist")}
|
||||||
|
helperText={
|
||||||
|
i18n("pattern_helper") + " " + i18n("disabled_csplist_helper")
|
||||||
|
}
|
||||||
|
name="csplist"
|
||||||
|
defaultValue={csplist}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
>
|
multiline
|
||||||
<MenuItem value={false}>{i18n("clear_cache_never")}</MenuItem>
|
/>
|
||||||
<MenuItem value={true}>{i18n("clear_cache_restart")}</MenuItem>
|
</>
|
||||||
</Select>
|
|
||||||
<FormHelperText>
|
|
||||||
<Link component="button" onClick={handleClearCache}>
|
|
||||||
{i18n("clear_all_cache_now")}
|
|
||||||
</Link>
|
|
||||||
</FormHelperText>
|
|
||||||
</FormControl>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Box>
|
<Box>
|
||||||
@@ -248,6 +326,17 @@ export default function Settings() {
|
|||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
label={i18n("translate_blacklist")}
|
||||||
|
helperText={i18n("pattern_helper")}
|
||||||
|
name="blacklist"
|
||||||
|
defaultValue={blacklist}
|
||||||
|
onChange={handleChange}
|
||||||
|
maxRows={10}
|
||||||
|
multiline
|
||||||
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useSync } from "../../hooks/Sync";
|
|||||||
import Alert from "@mui/material/Alert";
|
import Alert from "@mui/material/Alert";
|
||||||
import Link from "@mui/material/Link";
|
import Link from "@mui/material/Link";
|
||||||
import MenuItem from "@mui/material/MenuItem";
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
|
import LoadingButton from "@mui/lab/LoadingButton";
|
||||||
import {
|
import {
|
||||||
URL_KISS_WORKER,
|
URL_KISS_WORKER,
|
||||||
OPT_SYNCTYPE_ALL,
|
OPT_SYNCTYPE_ALL,
|
||||||
@@ -14,11 +15,10 @@ import {
|
|||||||
} from "../../config";
|
} from "../../config";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { syncSettingAndRules } from "../../libs/sync";
|
import { syncSettingAndRules } from "../../libs/sync";
|
||||||
import Button from "@mui/material/Button";
|
|
||||||
import { useAlert } from "../../hooks/Alert";
|
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 { 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 +43,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);
|
||||||
@@ -65,6 +65,7 @@ export default function SyncSetting() {
|
|||||||
<Box>
|
<Box>
|
||||||
<Stack spacing={3}>
|
<Stack spacing={3}>
|
||||||
<Alert severity="warning">{i18n("sync_warn")}</Alert>
|
<Alert severity="warning">{i18n("sync_warn")}</Alert>
|
||||||
|
<Alert severity="warning">{i18n("sync_warn_2")}</Alert>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
select
|
select
|
||||||
@@ -122,16 +123,16 @@ export default function SyncSetting() {
|
|||||||
useFlexGap
|
useFlexGap
|
||||||
flexWrap="wrap"
|
flexWrap="wrap"
|
||||||
>
|
>
|
||||||
<Button
|
<LoadingButton
|
||||||
size="small"
|
size="small"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
disabled={!syncUrl || !syncKey || loading}
|
disabled={!syncUrl || !syncKey || loading}
|
||||||
onClick={handleSyncTest}
|
onClick={handleSyncTest}
|
||||||
startIcon={<SyncIcon />}
|
startIcon={<SyncIcon />}
|
||||||
|
loading={loading}
|
||||||
>
|
>
|
||||||
{i18n("sync_now")}
|
{i18n("sync_now")}
|
||||||
</Button>
|
</LoadingButton>
|
||||||
{loading && <CircularProgress size={16} />}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -3,13 +3,19 @@ import Stack from "@mui/material/Stack";
|
|||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import MenuItem from "@mui/material/MenuItem";
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
import { useI18n } from "../../hooks/I18n";
|
import { useI18n } from "../../hooks/I18n";
|
||||||
import { OPT_TRANS_ALL, OPT_LANGS_FROM, OPT_LANGS_TO } from "../../config";
|
import {
|
||||||
|
OPT_TRANS_ALL,
|
||||||
|
OPT_LANGS_FROM,
|
||||||
|
OPT_LANGS_TO,
|
||||||
|
OPT_TRANBOX_TRIGGER_CLICK,
|
||||||
|
OPT_TRANBOX_TRIGGER_ALL,
|
||||||
|
OPT_DICT_BAIDU,
|
||||||
|
} from "../../config";
|
||||||
import ShortcutInput from "./ShortcutInput";
|
import ShortcutInput from "./ShortcutInput";
|
||||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
|
||||||
import Switch from "@mui/material/Switch";
|
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { limitNumber } from "../../libs/utils";
|
import { limitNumber } from "../../libs/utils";
|
||||||
import { useTranbox } from "../../hooks/Tranbox";
|
import { useTranbox } from "../../hooks/Tranbox";
|
||||||
|
import { isExt } from "../../libs/client";
|
||||||
|
|
||||||
export default function Tranbox() {
|
export default function Tranbox() {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
@@ -20,10 +26,10 @@ export default function Tranbox() {
|
|||||||
let { name, value } = e.target;
|
let { name, value } = e.target;
|
||||||
switch (name) {
|
switch (name) {
|
||||||
case "btnOffsetX":
|
case "btnOffsetX":
|
||||||
value = limitNumber(value, 0, 100);
|
|
||||||
break;
|
|
||||||
case "btnOffsetY":
|
case "btnOffsetY":
|
||||||
value = limitNumber(value, 0, 100);
|
case "boxOffsetX":
|
||||||
|
case "boxOffsetY":
|
||||||
|
value = limitNumber(value, -200, 200);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
@@ -40,32 +46,27 @@ export default function Tranbox() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
transOpen,
|
|
||||||
translator,
|
translator,
|
||||||
fromLang,
|
fromLang,
|
||||||
toLang,
|
toLang,
|
||||||
|
toLang2 = "en",
|
||||||
tranboxShortcut,
|
tranboxShortcut,
|
||||||
btnOffsetX,
|
btnOffsetX,
|
||||||
btnOffsetY,
|
btnOffsetY,
|
||||||
|
boxOffsetX = 0,
|
||||||
|
boxOffsetY = 10,
|
||||||
|
hideTranBtn = false,
|
||||||
|
hideClickAway = false,
|
||||||
|
simpleStyle = false,
|
||||||
|
followSelection = false,
|
||||||
|
triggerMode = OPT_TRANBOX_TRIGGER_CLICK,
|
||||||
|
extStyles = "",
|
||||||
|
enDict = OPT_DICT_BAIDU,
|
||||||
} = tranboxSetting;
|
} = tranboxSetting;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Stack spacing={3}>
|
<Stack spacing={3}>
|
||||||
<FormControlLabel
|
|
||||||
control={
|
|
||||||
<Switch
|
|
||||||
size="small"
|
|
||||||
name="transOpen"
|
|
||||||
checked={transOpen}
|
|
||||||
onChange={() => {
|
|
||||||
updateTranbox({ transOpen: !transOpen });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label={i18n("toggle_selection_translate")}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
select
|
select
|
||||||
size="small"
|
size="small"
|
||||||
@@ -111,6 +112,34 @@ export default function Tranbox() {
|
|||||||
))}
|
))}
|
||||||
</TextField>
|
</TextField>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
size="small"
|
||||||
|
name="toLang2"
|
||||||
|
value={toLang2}
|
||||||
|
label={i18n("to_lang2")}
|
||||||
|
helperText={i18n("to_lang2_helper")}
|
||||||
|
onChange={handleChange}
|
||||||
|
>
|
||||||
|
{[["none", "None"], ...OPT_LANGS_TO].map(([lang, name]) => (
|
||||||
|
<MenuItem key={lang} value={lang}>
|
||||||
|
{name}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
size="small"
|
||||||
|
name="enDict"
|
||||||
|
value={enDict}
|
||||||
|
label={i18n("english_dict")}
|
||||||
|
onChange={handleChange}
|
||||||
|
>
|
||||||
|
<MenuItem value={"-"}>{i18n("disable")}</MenuItem>
|
||||||
|
<MenuItem value={OPT_DICT_BAIDU}>{OPT_DICT_BAIDU}</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
label={i18n("tranbtn_offset_x")}
|
label={i18n("tranbtn_offset_x")}
|
||||||
@@ -129,11 +158,104 @@ export default function Tranbox() {
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ShortcutInput
|
<TextField
|
||||||
value={tranboxShortcut}
|
size="small"
|
||||||
onChange={handleShortcutInput}
|
label={i18n("tranbox_offset_x")}
|
||||||
label={i18n("trigger_tranbox_shortcut")}
|
type="number"
|
||||||
|
name="boxOffsetX"
|
||||||
|
defaultValue={boxOffsetX}
|
||||||
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
label={i18n("tranbox_offset_y")}
|
||||||
|
type="number"
|
||||||
|
name="boxOffsetY"
|
||||||
|
defaultValue={boxOffsetY}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
size="small"
|
||||||
|
name="hideTranBtn"
|
||||||
|
value={hideTranBtn}
|
||||||
|
label={i18n("hide_tran_button")}
|
||||||
|
onChange={handleChange}
|
||||||
|
>
|
||||||
|
<MenuItem value={false}>{i18n("show")}</MenuItem>
|
||||||
|
<MenuItem value={true}>{i18n("hide")}</MenuItem>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
size="small"
|
||||||
|
name="simpleStyle"
|
||||||
|
value={simpleStyle}
|
||||||
|
label={i18n("use_simple_style")}
|
||||||
|
onChange={handleChange}
|
||||||
|
>
|
||||||
|
<MenuItem value={false}>{i18n("disable")}</MenuItem>
|
||||||
|
<MenuItem value={true}>{i18n("enable")}</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
size="small"
|
||||||
|
name="followSelection"
|
||||||
|
value={followSelection}
|
||||||
|
label={i18n("follow_selection")}
|
||||||
|
onChange={handleChange}
|
||||||
|
>
|
||||||
|
<MenuItem value={false}>{i18n("disable")}</MenuItem>
|
||||||
|
<MenuItem value={true}>{i18n("enable")}</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
size="small"
|
||||||
|
name="triggerMode"
|
||||||
|
value={triggerMode}
|
||||||
|
label={i18n("trigger_mode")}
|
||||||
|
onChange={handleChange}
|
||||||
|
>
|
||||||
|
{OPT_TRANBOX_TRIGGER_ALL.map((item) => (
|
||||||
|
<MenuItem key={item} value={item}>
|
||||||
|
{i18n(`trigger_${item}`)}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
label={i18n("extend_styles")}
|
||||||
|
name="extStyles"
|
||||||
|
defaultValue={extStyles}
|
||||||
|
onChange={handleChange}
|
||||||
|
maxRows={10}
|
||||||
|
multiline
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!isExt && (
|
||||||
|
<ShortcutInput
|
||||||
|
value={tranboxShortcut}
|
||||||
|
onChange={handleShortcutInput}
|
||||||
|
label={i18n("trigger_tranbox_shortcut")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,165 +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 } 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";
|
|
||||||
|
|
||||||
function ApiFields({ site }) {
|
|
||||||
const { selector, rootSelector, fixer } = site;
|
|
||||||
return (
|
|
||||||
<Stack spacing={3}>
|
|
||||||
<TextField
|
|
||||||
size="small"
|
|
||||||
label={"rootSelector"}
|
|
||||||
name="rootSelector"
|
|
||||||
value={rootSelector || "document"}
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
size="small"
|
|
||||||
label={"selector"}
|
|
||||||
name="selector"
|
|
||||||
value={selector}
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
size="small"
|
|
||||||
label={"fixer"}
|
|
||||||
name="fixer"
|
|
||||||
value={fixer}
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ApiAccordion({ site }) {
|
|
||||||
const [expanded, setExpanded] = useState(false);
|
|
||||||
|
|
||||||
const handleChange = (e) => {
|
|
||||||
setExpanded((pre) => !pre);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Accordion expanded={expanded} onChange={handleChange}>
|
|
||||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
|
||||||
<Typography>{site.pattern}</Typography>
|
|
||||||
</AccordionSummary>
|
|
||||||
<AccordionDetails>
|
|
||||||
{expanded && <ApiFields site={site} />}
|
|
||||||
</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 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="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>
|
|
||||||
|
|
||||||
{setting.injectWebfix && (
|
|
||||||
<Box>
|
|
||||||
{loading ? (
|
|
||||||
<center>
|
|
||||||
<CircularProgress size={16} />
|
|
||||||
</center>
|
|
||||||
) : (
|
|
||||||
sites.map((site) => (
|
|
||||||
<ApiAccordion key={site.pattern} site={site} />
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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);
|
||||||
})();
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -85,15 +81,9 @@ export default function Options() {
|
|||||||
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}>
|
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}>
|
||||||
Install/Update Userscript for Tampermonkey/Violentmonkey
|
Install/Update Userscript for Tampermonkey/Violentmonkey
|
||||||
</Link>
|
</Link>
|
||||||
{/* <Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL2}>
|
|
||||||
Install/Update Userscript for Tampermonkey/Violentmonkey 2
|
|
||||||
</Link> */}
|
|
||||||
<Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL}>
|
<Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL}>
|
||||||
Install/Update Userscript for iOS Safari
|
Install/Update Userscript for iOS Safari
|
||||||
</Link>
|
</Link>
|
||||||
{/* <Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL2}>
|
|
||||||
Install/Update Userscript for iOS Safari 2
|
|
||||||
</Link> */}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</center>
|
</center>
|
||||||
);
|
);
|
||||||
@@ -125,7 +115,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>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import Box from "@mui/material/Box";
|
|
||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
import CloseIcon from "@mui/icons-material/Close";
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
import HomeIcon from "@mui/icons-material/Home";
|
import HomeIcon from "@mui/icons-material/Home";
|
||||||
import Stack from "@mui/material/Stack";
|
import Stack from "@mui/material/Stack";
|
||||||
import DarkModeButton from "../Options/DarkModeButton";
|
import DarkModeButton from "../Options/DarkModeButton";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
|
||||||
export default function Header({ setShowPopup }) {
|
export default function Header({ setShowPopup }) {
|
||||||
const handleHomepage = () => {
|
const handleHomepage = () => {
|
||||||
@@ -21,14 +21,16 @@ export default function Header({ setShowPopup }) {
|
|||||||
<IconButton onClick={handleHomepage}>
|
<IconButton onClick={handleHomepage}>
|
||||||
<HomeIcon />
|
<HomeIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Box
|
<Typography
|
||||||
|
component="div"
|
||||||
sx={{
|
sx={{
|
||||||
userSelect: "none",
|
userSelect: "none",
|
||||||
WebkitUserSelect: "none",
|
WebkitUserSelect: "none",
|
||||||
|
fontWeight: "bold",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{`${process.env.REACT_APP_NAME} v${process.env.REACT_APP_VERSION}`}
|
{`${process.env.REACT_APP_NAME} v${process.env.REACT_APP_VERSION}`}
|
||||||
</Box>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{setShowPopup ? (
|
{setShowPopup ? (
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useMemo } from "react";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Stack from "@mui/material/Stack";
|
import Stack from "@mui/material/Stack";
|
||||||
import MenuItem from "@mui/material/MenuItem";
|
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,19 +18,23 @@ 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,
|
||||||
OPT_STYLE_ALL,
|
OPT_STYLE_ALL,
|
||||||
OPT_STYLE_USE_COLOR,
|
DEFAULT_TRANS_APIS,
|
||||||
} from "../../config";
|
} from "../../config";
|
||||||
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 [transApis, setTransApis] = useState(tran?.setting?.transApis || []);
|
||||||
|
const [commands, setCommands] = useState({});
|
||||||
|
|
||||||
const handleOpenSetting = () => {
|
const handleOpenSetting = () => {
|
||||||
if (!tran) {
|
if (!tran) {
|
||||||
@@ -54,7 +58,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");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -70,7 +74,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");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -82,17 +86,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");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -104,14 +108,53 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
|||||||
try {
|
try {
|
||||||
const res = await sendTabMsg(MSG_TRANS_GETRULE);
|
const res = await sendTabMsg(MSG_TRANS_GETRULE);
|
||||||
if (!res.error) {
|
if (!res.error) {
|
||||||
setRule(res.data);
|
setRule(res.rule);
|
||||||
|
setTransApis(res.setting.transApis);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("[query rule]", err);
|
kissLog(err, "query rule");
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [tran]);
|
}, [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]);
|
||||||
|
|
||||||
|
const optApis = useMemo(
|
||||||
|
() =>
|
||||||
|
OPT_TRANS_ALL.map((key) => ({
|
||||||
|
...(transApis[key] || DEFAULT_TRANS_APIS[key]),
|
||||||
|
apiKey: key,
|
||||||
|
}))
|
||||||
|
.filter((item) => !item.isDisabled)
|
||||||
|
.map(({ apiKey, apiName }) => ({
|
||||||
|
key: apiKey,
|
||||||
|
name: apiName?.trim() || apiKey,
|
||||||
|
})),
|
||||||
|
[transApis]
|
||||||
|
);
|
||||||
|
|
||||||
if (!rule) {
|
if (!rule) {
|
||||||
return (
|
return (
|
||||||
<Box minWidth={300}>
|
<Box minWidth={300}>
|
||||||
@@ -130,7 +173,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { transOpen, translator, fromLang, toLang, textStyle, bgColor } = rule;
|
const { transOpen, translator, fromLang, toLang, textStyle } = rule;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box minWidth={300}>
|
<Box minWidth={300}>
|
||||||
@@ -154,13 +197,12 @@ 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")
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
{!isExt && (
|
|
||||||
<Button variant="text" onClick={handleClearCache}>
|
|
||||||
{i18n("clear_cache")}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
@@ -172,9 +214,9 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
|||||||
label={i18n("translate_service")}
|
label={i18n("translate_service")}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
>
|
>
|
||||||
{OPT_TRANS_ALL.map((item) => (
|
{optApis.map(({ key, name }) => (
|
||||||
<MenuItem key={item} value={item}>
|
<MenuItem key={key} value={key}>
|
||||||
{item}
|
{name}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</TextField>
|
</TextField>
|
||||||
@@ -217,7 +259,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) => (
|
||||||
@@ -227,7 +273,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
|||||||
))}
|
))}
|
||||||
</TextField>
|
</TextField>
|
||||||
|
|
||||||
{OPT_STYLE_USE_COLOR.includes(textStyle) && (
|
{/* {OPT_STYLE_USE_COLOR.includes(textStyle) && (
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
name="bgColor"
|
name="bgColor"
|
||||||
@@ -235,7 +281,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
|||||||
label={i18n("bg_color")}
|
label={i18n("bg_color")}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
)}
|
)} */}
|
||||||
|
|
||||||
<Stack
|
<Stack
|
||||||
direction="row"
|
direction="row"
|
||||||
@@ -246,6 +292,11 @@ export default function Popup({ setShowPopup, translator: tran }) {
|
|||||||
<Button variant="text" onClick={handleSaveRule}>
|
<Button variant="text" onClick={handleSaveRule}>
|
||||||
{i18n("save_rule")}
|
{i18n("save_rule")}
|
||||||
</Button>
|
</Button>
|
||||||
|
{!isExt && (
|
||||||
|
<Button variant="text" onClick={handleClearCache}>
|
||||||
|
{i18n("clear_cache")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button variant="text" onClick={handleOpenSetting}>
|
<Button variant="text" onClick={handleOpenSetting}>
|
||||||
{i18n("setting")}
|
{i18n("setting")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
29
src/views/Selection/AudioBtn.js
Normal file
29
src/views/Selection/AudioBtn.js
Normal 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 size="small">
|
||||||
|
<VolumeUpIcon fontSize="inherit" />
|
||||||
|
</IconButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playing) {
|
||||||
|
return (
|
||||||
|
<IconButton color="primary" size="small">
|
||||||
|
<VolumeUpIcon fontSize="inherit" />
|
||||||
|
</IconButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton onClick={onPlay} size="small">
|
||||||
|
<VolumeUpIcon fontSize="inherit" />
|
||||||
|
</IconButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,9 +5,9 @@ import { useState } from "react";
|
|||||||
|
|
||||||
export default function CopyBtn({ text }) {
|
export default function CopyBtn({ text }) {
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const handleClick = (e) => {
|
const handleClick = async (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
navigator.clipboard.writeText(text);
|
await navigator.clipboard.writeText(text);
|
||||||
setCopied(true);
|
setCopied(true);
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
|
|||||||
@@ -1,62 +1,113 @@
|
|||||||
import Box from "@mui/material/Box";
|
import { useState, useEffect } from "react";
|
||||||
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 AudioBtn from "./AudioBtn";
|
||||||
|
import CircularProgress from "@mui/material/CircularProgress";
|
||||||
|
import Alert from "@mui/material/Alert";
|
||||||
|
import { OPT_TRANS_BAIDU, PHONIC_MAP } from "../../config";
|
||||||
|
import { apiTranslate } from "../../apis";
|
||||||
|
import { isValidWord } from "../../libs/utils";
|
||||||
|
import CopyBtn from "./CopyBtn";
|
||||||
|
|
||||||
const exchangeMap = {
|
export default function DictCont({ text }) {
|
||||||
word_third: "第三人称单数",
|
const [loading, setLoading] = useState(true);
|
||||||
word_ing: "现在分词",
|
const [error, setError] = useState("");
|
||||||
word_done: "过去式",
|
const [dictResult, setDictResult] = useState(null);
|
||||||
word_past: "过去分词",
|
|
||||||
word_pl: "复数",
|
|
||||||
word_proto: "原词",
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function DictCont({ dictResult }) {
|
useEffect(() => {
|
||||||
if (!dictResult) {
|
(async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError("");
|
||||||
|
setDictResult(null);
|
||||||
|
|
||||||
|
if (!isValidWord(text)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dictRes = await apiTranslate({
|
||||||
|
text,
|
||||||
|
translator: OPT_TRANS_BAIDU,
|
||||||
|
fromLang: "en",
|
||||||
|
toLang: "zh-CN",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (dictRes[2]?.type === 1) {
|
||||||
|
setDictResult(JSON.parse(dictRes[2].result));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [text]);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <Alert severity="error">{error}</Alert>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <CircularProgress size={16} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!text || !dictResult) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const copyText = [
|
||||||
|
dictResult.src,
|
||||||
|
dictResult.voice
|
||||||
|
?.map(Object.entries)
|
||||||
|
.map((item) => item[0])
|
||||||
|
.map(([key, val]) => `${PHONIC_MAP[key]?.[0] || key} ${val}`)
|
||||||
|
.join(" "),
|
||||||
|
dictResult.content[0].mean
|
||||||
|
.map(({ pre, cont }) => {
|
||||||
|
return `${pre ? `[${pre}] ` : ""}${Object.keys(cont).join("; ")}`;
|
||||||
|
})
|
||||||
|
.join("\n"),
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Stack className="KT-transbox-dict" spacing={1}>
|
||||||
<Stack
|
<Stack direction="row" justifyContent="space-between">
|
||||||
direction="row"
|
<Typography variant="subtitle1" style={{ fontWeight: "bold" }}>
|
||||||
justifyContent="space-between"
|
{dictResult.src}
|
||||||
alignItems="flex-start"
|
</Typography>
|
||||||
>
|
<Stack direction="row" justifyContent="space-between">
|
||||||
<div style={{ fontWeight: "bold" }}>
|
<CopyBtn text={copyText} />
|
||||||
{dictResult.simple_means?.word_name}
|
<FavBtn word={dictResult.src} />
|
||||||
</div>
|
</Stack>
|
||||||
<FavBtn word={dictResult.simple_means?.word_name} />
|
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{dictResult.simple_means?.symbols?.map(({ ph_en, ph_am, parts }, idx) => (
|
<Typography component="div">
|
||||||
<div key={idx}>
|
<Typography component="div">
|
||||||
<div>{`英[${ph_en}] 美[${ph_am}]`}</div>
|
{dictResult.voice
|
||||||
<ul style={{ margin: "0.5em 0" }}>
|
?.map(Object.entries)
|
||||||
{parts.map(({ part, means }, idx) => (
|
.map((item) => item[0])
|
||||||
<li key={idx}>
|
.map(([key, val]) => (
|
||||||
{part ? `[${part}] ${means.join("; ")}` : means.join("; ")}
|
<Typography
|
||||||
</li>
|
component="div"
|
||||||
|
key={key}
|
||||||
|
style={{ display: "inline-block" }}
|
||||||
|
>
|
||||||
|
<Typography component="span">{`${PHONIC_MAP[key]?.[0] || key} ${val}`}</Typography>
|
||||||
|
<AudioBtn text={dictResult.src} lan={PHONIC_MAP[key]?.[1]} />
|
||||||
|
</Typography>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</Typography>
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<div>
|
<Typography component="ul">
|
||||||
{Object.entries(dictResult.simple_means?.exchange || {})
|
{dictResult.content[0].mean.map(({ pre, cont }, idx) => (
|
||||||
.map(([key, val]) => `${exchangeMap[key] || key}: ${val.join(", ")}`)
|
<Typography component="li" key={idx}>
|
||||||
.join("; ")}
|
{pre && `[${pre}] `}
|
||||||
</div>
|
{Object.keys(cont).join("; ")}
|
||||||
|
</Typography>
|
||||||
<Stack direction="row" spacing={1}>
|
|
||||||
{Object.values(dictResult.simple_means?.tags || {})
|
|
||||||
.flat()
|
|
||||||
.filter((item) => item)
|
|
||||||
.map((item) => (
|
|
||||||
<Chip label={item} size="small" />
|
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Typography>
|
||||||
</Box>
|
</Typography>
|
||||||
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useState } from "react";
|
||||||
import Paper from "@mui/material/Paper";
|
import Paper from "@mui/material/Paper";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
|
import { isMobile } from "../../libs/mobile";
|
||||||
|
|
||||||
function Pointer({
|
function Pointer({
|
||||||
direction,
|
direction,
|
||||||
@@ -16,21 +17,23 @@ function Pointer({
|
|||||||
const [origin, setOrigin] = useState(null);
|
const [origin, setOrigin] = useState(null);
|
||||||
|
|
||||||
function handlePointerDown(e) {
|
function handlePointerDown(e) {
|
||||||
e.target.setPointerCapture(e.pointerId);
|
!isMobile && e.target.setPointerCapture(e.pointerId);
|
||||||
|
const { clientX, clientY } = isMobile ? e.targetTouches[0] : e;
|
||||||
setOrigin({
|
setOrigin({
|
||||||
x: position.x,
|
x: position.x,
|
||||||
y: position.y,
|
y: position.y,
|
||||||
w: size.w,
|
w: size.w,
|
||||||
h: size.h,
|
h: size.h,
|
||||||
clientX: e.clientX,
|
clientX,
|
||||||
clientY: e.clientY,
|
clientY,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePointerMove(e) {
|
function handlePointerMove(e) {
|
||||||
|
const { clientX, clientY } = isMobile ? e.targetTouches[0] : e;
|
||||||
if (origin) {
|
if (origin) {
|
||||||
const dx = e.clientX - origin.clientX;
|
const dx = clientX - origin.clientX;
|
||||||
const dy = e.clientY - origin.clientY;
|
const dy = clientY - origin.clientY;
|
||||||
let x = position.x;
|
let x = position.x;
|
||||||
let y = position.y;
|
let y = position.y;
|
||||||
let w = size.w;
|
let w = size.w;
|
||||||
@@ -101,16 +104,24 @@ function Pointer({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handlePointerUp(e) {
|
function handlePointerUp(e) {
|
||||||
|
e.stopPropagation();
|
||||||
setOrigin(null);
|
setOrigin(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const touchProps = isMobile
|
||||||
|
? {
|
||||||
|
onTouchStart: handlePointerDown,
|
||||||
|
onTouchMove: handlePointerMove,
|
||||||
|
onTouchEnd: handlePointerUp,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
onPointerDown: handlePointerDown,
|
||||||
|
onPointerMove: handlePointerMove,
|
||||||
|
onPointerUp: handlePointerUp,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div {...props} {...touchProps}>
|
||||||
{...props}
|
|
||||||
onPointerDown={handlePointerDown}
|
|
||||||
onPointerMove={handlePointerMove}
|
|
||||||
onPointerUp={handlePointerUp}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -119,11 +130,11 @@ function Pointer({
|
|||||||
export default function DraggableResizable({
|
export default function DraggableResizable({
|
||||||
header,
|
header,
|
||||||
children,
|
children,
|
||||||
defaultPosition = {
|
position = {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
},
|
},
|
||||||
defaultSize = {
|
size = {
|
||||||
w: 600,
|
w: 600,
|
||||||
h: 400,
|
h: 400,
|
||||||
},
|
},
|
||||||
@@ -135,13 +146,13 @@ export default function DraggableResizable({
|
|||||||
w: 1200,
|
w: 1200,
|
||||||
h: 1200,
|
h: 1200,
|
||||||
},
|
},
|
||||||
|
setSize,
|
||||||
|
setPosition,
|
||||||
onChangeSize,
|
onChangeSize,
|
||||||
onChangePosition,
|
onChangePosition,
|
||||||
|
...props
|
||||||
}) {
|
}) {
|
||||||
const lineWidth = 4;
|
const lineWidth = 4;
|
||||||
const [position, setPosition] = useState(defaultPosition);
|
|
||||||
const [size, setSize] = useState(defaultSize);
|
|
||||||
|
|
||||||
const opts = {
|
const opts = {
|
||||||
size,
|
size,
|
||||||
setSize,
|
setSize,
|
||||||
@@ -151,17 +162,11 @@ export default function DraggableResizable({
|
|||||||
maxSize,
|
maxSize,
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
onChangeSize && onChangeSize(size);
|
|
||||||
}, [size, onChangeSize]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
onChangePosition && onChangePosition(position);
|
|
||||||
}, [position, onChangePosition]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
|
className="KT-draggable"
|
||||||
style={{
|
style={{
|
||||||
|
touchAction: "none",
|
||||||
position: "fixed",
|
position: "fixed",
|
||||||
left: position.x,
|
left: position.x,
|
||||||
top: position.y,
|
top: position.y,
|
||||||
@@ -170,6 +175,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"
|
||||||
@@ -205,11 +211,17 @@ export default function DraggableResizable({
|
|||||||
}}
|
}}
|
||||||
{...opts}
|
{...opts}
|
||||||
/>
|
/>
|
||||||
<Paper elevation={4}>
|
<Paper className="KT-draggable-body" elevation={4}>
|
||||||
<Pointer direction="Header" style={{ cursor: "move" }} {...opts}>
|
<Pointer
|
||||||
|
className="KT-draggable-header"
|
||||||
|
direction="Header"
|
||||||
|
style={{ cursor: "move" }}
|
||||||
|
{...opts}
|
||||||
|
>
|
||||||
{header}
|
{header}
|
||||||
</Pointer>
|
</Pointer>
|
||||||
<div
|
<Box
|
||||||
|
className="KT-draggable-container"
|
||||||
style={{
|
style={{
|
||||||
width: size.w,
|
width: size.w,
|
||||||
height: size.h,
|
height: size.h,
|
||||||
@@ -217,7 +229,7 @@ export default function DraggableResizable({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
<Pointer
|
<Pointer
|
||||||
direction="Right"
|
direction="Right"
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
35
src/views/Selection/SugCont.js
Normal file
35
src/views/Selection/SugCont.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import { apiBaiduSuggest } from "../../apis";
|
||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
|
||||||
|
export default function SugCont({ text }) {
|
||||||
|
const [sugs, setSugs] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
setSugs(await apiBaiduSuggest(text));
|
||||||
|
} catch (err) {
|
||||||
|
// skip
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [text]);
|
||||||
|
|
||||||
|
if (sugs.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack className="KT-transbox-sug" spacing={1}>
|
||||||
|
{sugs.map(({ k, v }) => (
|
||||||
|
<Typography component="div" key={k}>
|
||||||
|
<Typography>{k}</Typography>
|
||||||
|
<Typography component="ul" style={{ margin: "0" }}>
|
||||||
|
<Typography component="li">{v}</Typography>
|
||||||
|
</Typography>
|
||||||
|
</Typography>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { SettingProvider } from "../../hooks/Setting";
|
import { SettingProvider } from "../../hooks/Setting";
|
||||||
import ThemeProvider from "../../hooks/Theme";
|
import ThemeProvider from "../../hooks/Theme";
|
||||||
import DraggableResizable from "./DraggableResizable";
|
import DraggableResizable from "./DraggableResizable";
|
||||||
import Header from "../Popup/Header";
|
|
||||||
import Stack from "@mui/material/Stack";
|
import Stack from "@mui/material/Stack";
|
||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import MenuItem from "@mui/material/MenuItem";
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
@@ -10,13 +9,112 @@ import Box from "@mui/material/Box";
|
|||||||
import Divider from "@mui/material/Divider";
|
import Divider from "@mui/material/Divider";
|
||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
import DoneIcon from "@mui/icons-material/Done";
|
import DoneIcon from "@mui/icons-material/Done";
|
||||||
|
import DragIndicatorIcon from "@mui/icons-material/DragIndicator";
|
||||||
|
import UnfoldLessIcon from "@mui/icons-material/UnfoldLess";
|
||||||
|
import UnfoldMoreIcon from "@mui/icons-material/UnfoldMore";
|
||||||
|
import PushPinIcon from "@mui/icons-material/PushPin";
|
||||||
|
import PushPinOutlinedIcon from "@mui/icons-material/PushPinOutlined";
|
||||||
|
import LockIcon from "@mui/icons-material/Lock";
|
||||||
|
import LockOpenIcon from "@mui/icons-material/LockOpen";
|
||||||
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
import { useI18n } from "../../hooks/I18n";
|
import { useI18n } from "../../hooks/I18n";
|
||||||
import { OPT_TRANS_ALL, OPT_LANGS_FROM, OPT_LANGS_TO } from "../../config";
|
import {
|
||||||
import { useState, useRef } from "react";
|
OPT_TRANS_ALL,
|
||||||
|
OPT_LANGS_FROM,
|
||||||
|
OPT_LANGS_TO,
|
||||||
|
DEFAULT_TRANS_APIS,
|
||||||
|
} from "../../config";
|
||||||
|
import { useState, useRef, useMemo } from "react";
|
||||||
import TranCont from "./TranCont";
|
import TranCont from "./TranCont";
|
||||||
|
import DictCont from "./DictCont";
|
||||||
|
import SugCont from "./SugCont";
|
||||||
import CopyBtn from "./CopyBtn";
|
import CopyBtn from "./CopyBtn";
|
||||||
|
import { isValidWord } from "../../libs/utils";
|
||||||
|
import { isMobile } from "../../libs/mobile";
|
||||||
|
|
||||||
function TranForm({ text, setText, tranboxSetting, transApis }) {
|
function Header({
|
||||||
|
setShowPopup,
|
||||||
|
simpleStyle,
|
||||||
|
setSimpleStyle,
|
||||||
|
hideClickAway,
|
||||||
|
setHideClickAway,
|
||||||
|
followSelection,
|
||||||
|
setFollowSelection,
|
||||||
|
mouseHover,
|
||||||
|
}) {
|
||||||
|
if (!isMobile && simpleStyle && !mouseHover) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
className="KT-transbox-header"
|
||||||
|
onMouseUp={(e) => e.stopPropagation()}
|
||||||
|
onTouchEnd={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
||||||
|
<DragIndicatorIcon fontSize="small" />
|
||||||
|
<Stack direction="row" alignItems="center">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => {
|
||||||
|
setHideClickAway((pre) => !pre);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{hideClickAway ? (
|
||||||
|
<LockOpenIcon fontSize="small" />
|
||||||
|
) : (
|
||||||
|
<LockIcon fontSize="small" />
|
||||||
|
)}
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => {
|
||||||
|
setFollowSelection((pre) => !pre);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{followSelection ? (
|
||||||
|
<PushPinOutlinedIcon fontSize="small" />
|
||||||
|
) : (
|
||||||
|
<PushPinIcon fontSize="small" />
|
||||||
|
)}
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => {
|
||||||
|
setSimpleStyle((pre) => !pre);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{simpleStyle ? (
|
||||||
|
<UnfoldMoreIcon fontSize="small" />
|
||||||
|
) : (
|
||||||
|
<UnfoldLessIcon fontSize="small" />
|
||||||
|
)}
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => {
|
||||||
|
setShowPopup(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CloseIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
<Divider />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TranForm({
|
||||||
|
text,
|
||||||
|
setText,
|
||||||
|
tranboxSetting,
|
||||||
|
transApis,
|
||||||
|
simpleStyle,
|
||||||
|
langDetector,
|
||||||
|
enDict,
|
||||||
|
}) {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
||||||
const [editMode, setEditMode] = useState(false);
|
const [editMode, setEditMode] = useState(false);
|
||||||
@@ -26,131 +124,164 @@ function TranForm({ text, setText, tranboxSetting, transApis }) {
|
|||||||
const [toLang, setToLang] = useState(tranboxSetting.toLang);
|
const [toLang, setToLang] = useState(tranboxSetting.toLang);
|
||||||
const inputRef = useRef(null);
|
const inputRef = useRef(null);
|
||||||
|
|
||||||
return (
|
const optApis = useMemo(
|
||||||
<Stack sx={{ p: 2 }} spacing={2}>
|
() =>
|
||||||
<Box>
|
OPT_TRANS_ALL.map((key) => ({
|
||||||
<Grid container spacing={2} columns={12}>
|
...(transApis[key] || DEFAULT_TRANS_APIS[key]),
|
||||||
<Grid item xs={4} sm={4} md={4} lg={4}>
|
apiKey: key,
|
||||||
<TextField
|
}))
|
||||||
select
|
.filter((item) => !item.isDisabled)
|
||||||
SelectProps={{ MenuProps: { disablePortal: true } }}
|
.map(({ apiKey, apiName }) => ({
|
||||||
fullWidth
|
key: apiKey,
|
||||||
size="small"
|
name: apiName?.trim() || apiKey,
|
||||||
name="fromLang"
|
})),
|
||||||
value={fromLang}
|
[transApis]
|
||||||
label={i18n("from_lang")}
|
);
|
||||||
onChange={(e) => {
|
|
||||||
setFromLang(e.target.value);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{OPT_LANGS_FROM.map(([lang, name]) => (
|
|
||||||
<MenuItem key={lang} value={lang}>
|
|
||||||
{name}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</TextField>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={4} sm={4} md={4} lg={4}>
|
|
||||||
<TextField
|
|
||||||
select
|
|
||||||
SelectProps={{ MenuProps: { disablePortal: true } }}
|
|
||||||
fullWidth
|
|
||||||
size="small"
|
|
||||||
name="toLang"
|
|
||||||
value={toLang}
|
|
||||||
label={i18n("to_lang")}
|
|
||||||
onChange={(e) => {
|
|
||||||
setToLang(e.target.value);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{OPT_LANGS_TO.map(([lang, name]) => (
|
|
||||||
<MenuItem key={lang} value={lang}>
|
|
||||||
{name}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</TextField>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={4} sm={4} md={4} lg={4}>
|
|
||||||
<TextField
|
|
||||||
select
|
|
||||||
SelectProps={{ MenuProps: { disablePortal: true } }}
|
|
||||||
fullWidth
|
|
||||||
size="small"
|
|
||||||
value={translator}
|
|
||||||
name="translator"
|
|
||||||
label={i18n("translate_service")}
|
|
||||||
onChange={(e) => {
|
|
||||||
setTranslator(e.target.value);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{OPT_TRANS_ALL.map((item) => (
|
|
||||||
<MenuItem key={item} value={item}>
|
|
||||||
{item}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</TextField>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box>
|
return (
|
||||||
<TextField
|
<Stack
|
||||||
label={i18n("original_text")}
|
className="KT-transbox-container"
|
||||||
inputRef={inputRef}
|
sx={{ p: simpleStyle ? 1 : 2 }}
|
||||||
fullWidth
|
spacing={simpleStyle ? 1 : 2}
|
||||||
multiline
|
>
|
||||||
value={editMode ? editText : text}
|
{!simpleStyle && (
|
||||||
disabled={!editMode}
|
<>
|
||||||
onChange={(e) => {
|
<Box className="KT-transbox-select">
|
||||||
setEditText(e.target.value);
|
<Grid container spacing={simpleStyle ? 1 : 2} columns={12}>
|
||||||
}}
|
<Grid item xs={4} sm={4} md={4} lg={4}>
|
||||||
onClick={() => {
|
<TextField
|
||||||
setEditMode(true);
|
select
|
||||||
setEditText(text);
|
SelectProps={{ MenuProps: { disablePortal: true } }}
|
||||||
const timer = setTimeout(() => {
|
fullWidth
|
||||||
clearTimeout(timer);
|
size="small"
|
||||||
inputRef.current?.focus();
|
name="fromLang"
|
||||||
}, 100);
|
value={fromLang}
|
||||||
}}
|
label={i18n("from_lang")}
|
||||||
onBlur={() => {
|
onChange={(e) => {
|
||||||
setEditMode(false);
|
setFromLang(e.target.value);
|
||||||
setText(editText.trim());
|
}}
|
||||||
}}
|
>
|
||||||
InputProps={{
|
{OPT_LANGS_FROM.map(([lang, name]) => (
|
||||||
endAdornment: (
|
<MenuItem key={lang} value={lang}>
|
||||||
<Stack
|
{name}
|
||||||
direction="row"
|
</MenuItem>
|
||||||
sx={{
|
))}
|
||||||
position: "absolute",
|
</TextField>
|
||||||
right: 0,
|
</Grid>
|
||||||
top: 0,
|
<Grid item xs={4} sm={4} md={4} lg={4}>
|
||||||
}}
|
<TextField
|
||||||
>
|
select
|
||||||
{editMode ? (
|
SelectProps={{ MenuProps: { disablePortal: true } }}
|
||||||
<IconButton
|
fullWidth
|
||||||
size="small"
|
size="small"
|
||||||
onClick={(e) => {
|
name="toLang"
|
||||||
e.stopPropagation();
|
value={toLang}
|
||||||
|
label={i18n("to_lang")}
|
||||||
|
onChange={(e) => {
|
||||||
|
setToLang(e.target.value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{OPT_LANGS_TO.map(([lang, name]) => (
|
||||||
|
<MenuItem key={lang} value={lang}>
|
||||||
|
{name}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={4} sm={4} md={4} lg={4}>
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
SelectProps={{ MenuProps: { disablePortal: true } }}
|
||||||
|
fullWidth
|
||||||
|
size="small"
|
||||||
|
value={translator}
|
||||||
|
name="translator"
|
||||||
|
label={i18n("translate_service")}
|
||||||
|
onChange={(e) => {
|
||||||
|
setTranslator(e.target.value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{optApis.map(({ key, name }) => (
|
||||||
|
<MenuItem key={key} value={key}>
|
||||||
|
{name}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box className="KT-transbox-origin">
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
label={i18n("original_text")}
|
||||||
|
inputRef={inputRef}
|
||||||
|
fullWidth
|
||||||
|
multiline
|
||||||
|
value={editMode ? editText : text}
|
||||||
|
onChange={(e) => {
|
||||||
|
setEditText(e.target.value);
|
||||||
|
}}
|
||||||
|
onFocus={() => {
|
||||||
|
setEditMode(true);
|
||||||
|
setEditText(text);
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
setEditMode(false);
|
||||||
|
setText(editText.trim());
|
||||||
|
}}
|
||||||
|
InputProps={{
|
||||||
|
endAdornment: (
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DoneIcon fontSize="inherit" />
|
{editMode ? (
|
||||||
</IconButton>
|
<IconButton
|
||||||
) : (
|
size="small"
|
||||||
<CopyBtn text={text} />
|
onClick={(e) => {
|
||||||
)}
|
e.stopPropagation();
|
||||||
</Stack>
|
}}
|
||||||
),
|
>
|
||||||
}}
|
<DoneIcon fontSize="inherit" />
|
||||||
/>
|
</IconButton>
|
||||||
</Box>
|
) : (
|
||||||
|
<CopyBtn text={text} />
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<TranCont
|
{(!simpleStyle ||
|
||||||
text={text}
|
!isValidWord(text) ||
|
||||||
translator={translator}
|
!toLang.startsWith("zh") ||
|
||||||
fromLang={fromLang}
|
enDict === "-") && (
|
||||||
toLang={toLang}
|
<TranCont
|
||||||
transApis={transApis}
|
text={text}
|
||||||
/>
|
translator={translator}
|
||||||
|
fromLang={fromLang}
|
||||||
|
toLang={toLang}
|
||||||
|
toLang2={tranboxSetting.toLang2}
|
||||||
|
transApis={transApis}
|
||||||
|
simpleStyle={simpleStyle}
|
||||||
|
langDetector={langDetector}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{enDict !== "-" && (
|
||||||
|
<>
|
||||||
|
<DictCont text={text} />
|
||||||
|
<SugCont text={text} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -165,23 +296,49 @@ export default function TranBox({
|
|||||||
setBoxSize,
|
setBoxSize,
|
||||||
boxPosition,
|
boxPosition,
|
||||||
setBoxPosition,
|
setBoxPosition,
|
||||||
|
simpleStyle,
|
||||||
|
setSimpleStyle,
|
||||||
|
hideClickAway,
|
||||||
|
setHideClickAway,
|
||||||
|
followSelection,
|
||||||
|
setFollowSelection,
|
||||||
|
extStyles,
|
||||||
|
langDetector,
|
||||||
|
enDict,
|
||||||
}) {
|
}) {
|
||||||
|
const [mouseHover, setMouseHover] = useState(false);
|
||||||
return (
|
return (
|
||||||
<SettingProvider>
|
<SettingProvider>
|
||||||
<ThemeProvider>
|
<ThemeProvider styles={extStyles}>
|
||||||
<DraggableResizable
|
<DraggableResizable
|
||||||
defaultPosition={boxPosition}
|
position={boxPosition}
|
||||||
defaultSize={boxSize}
|
size={boxSize}
|
||||||
header={<Header setShowPopup={setShowBox} />}
|
setSize={setBoxSize}
|
||||||
onChangeSize={setBoxSize}
|
setPosition={setBoxPosition}
|
||||||
onChangePosition={setBoxPosition}
|
header={
|
||||||
|
<Header
|
||||||
|
setShowPopup={setShowBox}
|
||||||
|
simpleStyle={simpleStyle}
|
||||||
|
setSimpleStyle={setSimpleStyle}
|
||||||
|
hideClickAway={hideClickAway}
|
||||||
|
setHideClickAway={setHideClickAway}
|
||||||
|
followSelection={followSelection}
|
||||||
|
setFollowSelection={setFollowSelection}
|
||||||
|
mouseHover={mouseHover}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
onMouseEnter={() => setMouseHover(true)}
|
||||||
|
onMouseLeave={() => setMouseHover(false)}
|
||||||
>
|
>
|
||||||
<Divider />
|
|
||||||
<TranForm
|
<TranForm
|
||||||
text={text}
|
text={text}
|
||||||
setText={setText}
|
setText={setText}
|
||||||
tranboxSetting={tranboxSetting}
|
tranboxSetting={tranboxSetting}
|
||||||
transApis={transApis}
|
transApis={transApis}
|
||||||
|
simpleStyle={simpleStyle}
|
||||||
|
langDetector={langDetector}
|
||||||
|
enDict={enDict}
|
||||||
/>
|
/>
|
||||||
</DraggableResizable>
|
</DraggableResizable>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
|||||||
@@ -1,22 +1,33 @@
|
|||||||
export default function TranBtn({ onClick, position, tranboxSetting }) {
|
import { isMobile } from "../../libs/mobile";
|
||||||
|
import { limitNumber } from "../../libs/utils";
|
||||||
|
|
||||||
|
export default function TranBtn({
|
||||||
|
onTrigger,
|
||||||
|
btnEvent,
|
||||||
|
position,
|
||||||
|
btnOffsetX,
|
||||||
|
btnOffsetY,
|
||||||
|
}) {
|
||||||
|
const left = limitNumber(position.x + btnOffsetX, 0, window.innerWidth - 32);
|
||||||
|
const top = limitNumber(position.y + btnOffsetY, 0, window.innerHeight - 32);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
className="KT-tranbtn"
|
||||||
style={{
|
style={{
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
|
// position: "absolute",
|
||||||
position: "fixed",
|
position: "fixed",
|
||||||
left: position.x + tranboxSetting.btnOffsetX,
|
left,
|
||||||
top: position.y + tranboxSetting.btnOffsetY,
|
top,
|
||||||
zIndex: 2147483647,
|
zIndex: 2147483647,
|
||||||
}}
|
}}
|
||||||
onClick={onClick}
|
{...{ [btnEvent]: onTrigger }}
|
||||||
onMouseUp={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="20"
|
width={isMobile ? "32" : "20"}
|
||||||
height="20"
|
height={isMobile ? "32" : "20"}
|
||||||
viewBox="0 0 32 32"
|
viewBox="0 0 32 32"
|
||||||
version="1.1"
|
version="1.1"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,28 +1,30 @@
|
|||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Alert from "@mui/material/Alert";
|
|
||||||
import CircularProgress from "@mui/material/CircularProgress";
|
import CircularProgress from "@mui/material/CircularProgress";
|
||||||
import Stack from "@mui/material/Stack";
|
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 } from "../../config";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { apiTranslate } from "../../apis";
|
import { apiTranslate } from "../../apis";
|
||||||
import { isValidWord } from "../../libs/utils";
|
|
||||||
import CopyBtn from "./CopyBtn";
|
import CopyBtn from "./CopyBtn";
|
||||||
import DictCont from "./DictCont";
|
import Typography from "@mui/material/Typography";
|
||||||
|
import Alert from "@mui/material/Alert";
|
||||||
|
import { tryDetectLang } from "../../libs";
|
||||||
|
|
||||||
export default function TranCont({
|
export default function TranCont({
|
||||||
text,
|
text,
|
||||||
translator,
|
translator,
|
||||||
fromLang,
|
fromLang,
|
||||||
toLang,
|
toLang,
|
||||||
|
toLang2 = "en",
|
||||||
transApis,
|
transApis,
|
||||||
|
simpleStyle,
|
||||||
|
langDetector,
|
||||||
}) {
|
}) {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const [trText, setTrText] = useState("");
|
const [trText, setTrText] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [dictResult, setDictResult] = useState(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
@@ -30,71 +32,73 @@ export default function TranCont({
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setTrText("");
|
setTrText("");
|
||||||
setError("");
|
setError("");
|
||||||
setDictResult(null);
|
|
||||||
|
|
||||||
const apis = { ...transApis, ...DEFAULT_TRANS_APIS };
|
let to = toLang;
|
||||||
const apiSetting = apis[translator];
|
if (fromLang === "auto" && toLang !== toLang2 && toLang2 !== "none") {
|
||||||
|
const detectLang = await tryDetectLang(text, true, langDetector);
|
||||||
|
if (detectLang === toLang) {
|
||||||
|
to = toLang2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiSetting =
|
||||||
|
transApis[translator] || DEFAULT_TRANS_APIS[translator];
|
||||||
const tranRes = await apiTranslate({
|
const tranRes = await apiTranslate({
|
||||||
text,
|
text,
|
||||||
translator,
|
translator,
|
||||||
fromLang,
|
fromLang,
|
||||||
toLang,
|
toLang: to,
|
||||||
apiSetting,
|
apiSetting,
|
||||||
});
|
});
|
||||||
setTrText(tranRes[0]);
|
setTrText(tranRes[0]);
|
||||||
|
|
||||||
// 词典
|
|
||||||
if (isValidWord(text) && toLang.startsWith("zh")) {
|
|
||||||
if (fromLang === "en" && translator === OPT_TRANS_BAIDU) {
|
|
||||||
setDictResult(tranRes[2].dict_result);
|
|
||||||
} else {
|
|
||||||
const dictRes = await apiTranslate({
|
|
||||||
text,
|
|
||||||
translator: OPT_TRANS_BAIDU,
|
|
||||||
fromLang: "en",
|
|
||||||
toLang: "zh-CN",
|
|
||||||
apiSetting: apis[OPT_TRANS_BAIDU],
|
|
||||||
});
|
|
||||||
setDictResult(dictRes[2].dict_result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message);
|
setError(err.message);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [text, translator, fromLang, toLang, transApis]);
|
}, [text, translator, fromLang, toLang, toLang2, transApis, langDetector]);
|
||||||
|
|
||||||
|
if (simpleStyle) {
|
||||||
|
return (
|
||||||
|
<Box className="KT-transbox-target KT-transbox-target_simple">
|
||||||
|
{error ? (
|
||||||
|
<Alert severity="error">{error}</Alert>
|
||||||
|
) : loading ? (
|
||||||
|
<CircularProgress size={16} />
|
||||||
|
) : (
|
||||||
|
<Typography style={{ whiteSpace: "pre-line" }}>{trText}</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Box className="KT-transbox-target KT-transbox-target_default">
|
||||||
<Box>
|
<TextField
|
||||||
<TextField
|
size="small"
|
||||||
label={i18n("translated_text")}
|
label={i18n("translated_text")}
|
||||||
// disabled
|
// disabled
|
||||||
fullWidth
|
fullWidth
|
||||||
multiline
|
multiline
|
||||||
value={trText}
|
value={trText}
|
||||||
InputProps={{
|
helperText={error}
|
||||||
endAdornment: (
|
InputProps={{
|
||||||
<Stack
|
startAdornment: loading ? <CircularProgress size={16} /> : null,
|
||||||
direction="row"
|
endAdornment: (
|
||||||
sx={{
|
<Stack
|
||||||
position: "absolute",
|
direction="row"
|
||||||
right: 0,
|
sx={{
|
||||||
top: 0,
|
position: "absolute",
|
||||||
}}
|
right: 0,
|
||||||
>
|
top: 0,
|
||||||
<CopyBtn text={trText} />
|
}}
|
||||||
</Stack>
|
>
|
||||||
),
|
<CopyBtn text={trText} />
|
||||||
}}
|
</Stack>
|
||||||
/>
|
),
|
||||||
</Box>
|
}}
|
||||||
|
/>
|
||||||
{loading && <CircularProgress size={24} />}
|
</Box>
|
||||||
{error && <Alert severity="error">{error}</Alert>}
|
|
||||||
{dictResult && <DictCont dictResult={dictResult} />}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,61 +1,221 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useCallback, useMemo } from "react";
|
||||||
import TranBtn from "./TranBtn";
|
import TranBtn from "./TranBtn";
|
||||||
import TranBox from "./TranBox";
|
import TranBox from "./TranBox";
|
||||||
import { shortcutRegister } from "../../libs/shortcut";
|
import { shortcutRegister } from "../../libs/shortcut";
|
||||||
|
import { sleep, limitNumber } from "../../libs/utils";
|
||||||
|
import { isGm, isExt } from "../../libs/client";
|
||||||
|
import {
|
||||||
|
MSG_OPEN_TRANBOX,
|
||||||
|
DEFAULT_TRANBOX_SHORTCUT,
|
||||||
|
OPT_TRANBOX_TRIGGER_CLICK,
|
||||||
|
OPT_TRANBOX_TRIGGER_HOVER,
|
||||||
|
OPT_TRANBOX_TRIGGER_SELECT,
|
||||||
|
OPT_DICT_BAIDU,
|
||||||
|
} from "../../config";
|
||||||
|
import { isMobile } from "../../libs/mobile";
|
||||||
|
import { kissLog } from "../../libs/log";
|
||||||
|
import { useLangMap } from "../../hooks/I18n";
|
||||||
|
|
||||||
export default function Slection({ tranboxSetting, transApis }) {
|
export default function Slection({
|
||||||
|
contextMenuType,
|
||||||
|
tranboxSetting,
|
||||||
|
transApis,
|
||||||
|
uiLang,
|
||||||
|
langDetector,
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
hideTranBtn = false,
|
||||||
|
simpleStyle: initSimpleStyle = false,
|
||||||
|
hideClickAway: initHideClickAway = false,
|
||||||
|
followSelection: initFollowMouse = false,
|
||||||
|
tranboxShortcut = DEFAULT_TRANBOX_SHORTCUT,
|
||||||
|
triggerMode = OPT_TRANBOX_TRIGGER_CLICK,
|
||||||
|
extStyles,
|
||||||
|
btnOffsetX,
|
||||||
|
btnOffsetY,
|
||||||
|
boxOffsetX = 0,
|
||||||
|
boxOffsetY = 10,
|
||||||
|
enDict = OPT_DICT_BAIDU,
|
||||||
|
} = tranboxSetting;
|
||||||
|
|
||||||
|
const boxWidth =
|
||||||
|
isMobile || initSimpleStyle
|
||||||
|
? 300
|
||||||
|
: limitNumber(window.innerWidth, 300, 600);
|
||||||
|
const boxHeight =
|
||||||
|
isMobile || initSimpleStyle
|
||||||
|
? 200
|
||||||
|
: limitNumber(window.innerHeight, 200, 400);
|
||||||
|
|
||||||
|
const langMap = useLangMap(uiLang);
|
||||||
const [showBox, setShowBox] = useState(false);
|
const [showBox, setShowBox] = useState(false);
|
||||||
const [showBtn, setShowBtn] = useState(false);
|
const [showBtn, setShowBtn] = useState(false);
|
||||||
const [selectedText, setSelText] = useState("");
|
const [selectedText, setSelText] = useState("");
|
||||||
const [text, setText] = useState("");
|
const [text, setText] = useState("");
|
||||||
const [position, setPosition] = useState({ x: 0, y: 0 });
|
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||||
const [boxSize, setBoxSize] = useState({ w: 600, h: 400 });
|
const [boxSize, setBoxSize] = useState({
|
||||||
const [boxPosition, setBoxPosition] = useState({
|
w: boxWidth,
|
||||||
x: (window.innerWidth - 600) / 2,
|
h: boxHeight,
|
||||||
y: (window.innerHeight - 400) / 2,
|
|
||||||
});
|
});
|
||||||
|
const [boxPosition, setBoxPosition] = useState({
|
||||||
|
x: (window.innerWidth - boxWidth) / 2,
|
||||||
|
y: (window.innerHeight - boxHeight) / 2,
|
||||||
|
});
|
||||||
|
const [simpleStyle, setSimpleStyle] = useState(initSimpleStyle);
|
||||||
|
const [hideClickAway, setHideClickAway] = useState(initHideClickAway);
|
||||||
|
const [followSelection, setFollowSelection] = useState(initFollowMouse);
|
||||||
|
|
||||||
function handleMouseup(e) {
|
const handleTrigger = useCallback(
|
||||||
const selectedText = window.getSelection()?.toString()?.trim() || "";
|
(text) => {
|
||||||
if (!selectedText) {
|
|
||||||
setShowBtn(false);
|
setShowBtn(false);
|
||||||
|
setText(text || selectedText);
|
||||||
|
setShowBox(true);
|
||||||
|
},
|
||||||
|
[selectedText]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleTranbox = useCallback(() => {
|
||||||
|
setShowBtn(false);
|
||||||
|
|
||||||
|
const selection = window.getSelection();
|
||||||
|
const selectedText = selection?.toString()?.trim() || "";
|
||||||
|
if (!selectedText) {
|
||||||
|
setShowBox((pre) => !pre);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSelText(selectedText);
|
const rect = selection?.getRangeAt(0)?.getBoundingClientRect();
|
||||||
setShowBtn(true);
|
if (rect && followSelection) {
|
||||||
setPosition({ x: e.clientX, y: e.clientY });
|
const x = (rect.left + rect.right) / 2 + boxOffsetX;
|
||||||
}
|
const y = rect.bottom + boxOffsetY;
|
||||||
|
setBoxPosition({
|
||||||
const handleClick = (e) => {
|
x: limitNumber(x, 0, window.innerWidth - 300),
|
||||||
e.stopPropagation();
|
y: limitNumber(y, 0, window.innerHeight - 200),
|
||||||
setShowBtn(false);
|
});
|
||||||
|
|
||||||
setText(selectedText);
|
|
||||||
if (!showBox) {
|
|
||||||
setShowBox(true);
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
setSelText(selectedText);
|
||||||
|
setText(selectedText);
|
||||||
|
setShowBox(true);
|
||||||
|
}, [followSelection, boxOffsetX, boxOffsetY]);
|
||||||
|
|
||||||
|
const btnEvent = useMemo(() => {
|
||||||
|
if (isMobile) {
|
||||||
|
return "onTouchEnd";
|
||||||
|
} else if (triggerMode === OPT_TRANBOX_TRIGGER_HOVER) {
|
||||||
|
return "onMouseOver";
|
||||||
|
}
|
||||||
|
return "onMouseUp";
|
||||||
|
}, [triggerMode]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener("mouseup", handleMouseup);
|
async function handleMouseup(e) {
|
||||||
return () => {
|
e.stopPropagation();
|
||||||
window.removeEventListener("mouseup", handleMouseup);
|
await sleep(200);
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const selection = window.getSelection();
|
||||||
const clearShortcut = shortcutRegister(
|
const selectedText = selection?.toString()?.trim() || "";
|
||||||
tranboxSetting.tranboxShortcut,
|
setSelText(selectedText);
|
||||||
() => {
|
if (!selectedText) {
|
||||||
setShowBox((pre) => !pre);
|
setShowBtn(false);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
|
const rect = selection?.getRangeAt(0)?.getBoundingClientRect();
|
||||||
|
if (rect && followSelection) {
|
||||||
|
const x = (rect.left + rect.right) / 2 + boxOffsetX;
|
||||||
|
const y = rect.bottom + boxOffsetY;
|
||||||
|
setBoxPosition({
|
||||||
|
x: limitNumber(x, 0, window.innerWidth - 300),
|
||||||
|
y: limitNumber(y, 0, window.innerHeight - 200),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (triggerMode === OPT_TRANBOX_TRIGGER_SELECT) {
|
||||||
|
handleTrigger(selectedText);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { clientX, clientY } = isMobile ? e.changedTouches[0] : e;
|
||||||
|
setShowBtn(!hideTranBtn);
|
||||||
|
setPosition({ x: clientX, y: clientY });
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo: mobile support
|
||||||
|
// window.addEventListener("mouseup", handleMouseup);
|
||||||
|
window.addEventListener(isMobile ? "touchend" : "mouseup", handleMouseup);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener(
|
||||||
|
isMobile ? "touchend" : "mouseup",
|
||||||
|
handleMouseup
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
hideTranBtn,
|
||||||
|
triggerMode,
|
||||||
|
followSelection,
|
||||||
|
boxOffsetX,
|
||||||
|
boxOffsetY,
|
||||||
|
handleTrigger,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isExt) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const clearShortcut = shortcutRegister(tranboxShortcut, handleTranbox);
|
||||||
return () => {
|
return () => {
|
||||||
clearShortcut();
|
clearShortcut();
|
||||||
};
|
};
|
||||||
}, [tranboxSetting.tranboxShortcut, setShowBox]);
|
}, [tranboxShortcut, handleTranbox]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener(MSG_OPEN_TRANBOX, handleTranbox);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener(MSG_OPEN_TRANBOX, handleTranbox);
|
||||||
|
};
|
||||||
|
}, [handleTranbox]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isGm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册菜单
|
||||||
|
try {
|
||||||
|
const menuCommandIds = [];
|
||||||
|
contextMenuType !== 0 &&
|
||||||
|
menuCommandIds.push(
|
||||||
|
GM.registerMenuCommand(
|
||||||
|
langMap("translate_selected_text"),
|
||||||
|
(event) => {
|
||||||
|
handleTranbox();
|
||||||
|
},
|
||||||
|
"S"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
menuCommandIds.forEach((id) => {
|
||||||
|
GM.unregisterMenuCommand(id);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
kissLog(err, "registerMenuCommand");
|
||||||
|
}
|
||||||
|
}, [handleTranbox, contextMenuType, langMap]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (hideClickAway) {
|
||||||
|
const handleHideBox = () => {
|
||||||
|
setShowBox(false);
|
||||||
|
};
|
||||||
|
window.addEventListener("click", handleHideBox);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("click", handleHideBox);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [hideClickAway]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -70,14 +230,28 @@ export default function Slection({ tranboxSetting, transApis }) {
|
|||||||
tranboxSetting={tranboxSetting}
|
tranboxSetting={tranboxSetting}
|
||||||
transApis={transApis}
|
transApis={transApis}
|
||||||
setShowBox={setShowBox}
|
setShowBox={setShowBox}
|
||||||
|
simpleStyle={simpleStyle}
|
||||||
|
setSimpleStyle={setSimpleStyle}
|
||||||
|
hideClickAway={hideClickAway}
|
||||||
|
setHideClickAway={setHideClickAway}
|
||||||
|
followSelection={followSelection}
|
||||||
|
setFollowSelection={setFollowSelection}
|
||||||
|
extStyles={extStyles}
|
||||||
|
langDetector={langDetector}
|
||||||
|
enDict={enDict}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showBtn && (
|
{showBtn && (
|
||||||
<TranBtn
|
<TranBtn
|
||||||
position={position}
|
position={position}
|
||||||
tranboxSetting={tranboxSetting}
|
btnOffsetX={btnOffsetX}
|
||||||
onClick={handleClick}
|
btnOffsetY={btnOffsetY}
|
||||||
|
btnEvent={btnEvent}
|
||||||
|
onTrigger={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleTrigger();
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
Reference in New Issue
Block a user