Compare commits

..

209 Commits

Author SHA1 Message Date
Gabe Yuan
450283b80a v1.7.11 2023-10-27 17:14:13 +08:00
Gabe Yuan
44aeed03a6 fix dict ui 2023-10-27 14:51:53 +08:00
Gabe Yuan
fa4569415d fix dict ui 2023-10-27 14:07:22 +08:00
Gabe Yuan
a341bf30ba fix tranbox apisetting bug 2023-10-27 13:55:24 +08:00
Gabe Yuan
34a7354c84 fix dict tags stack 2023-10-27 13:18:42 +08:00
Gabe Yuan
21b5dfbe98 update readme 2023-10-27 00:11:44 +08:00
Gabe Yuan
c1920f5cdd v1.7.10 2023-10-27 00:01:48 +08:00
Gabe Yuan
3e24568df9 add upload button for fav words 2023-10-26 23:55:05 +08:00
Gabe Yuan
b785cfe854 add download button for fav words 2023-10-26 17:59:49 +08:00
Gabe Yuan
15367bd117 add fav words page 2023-10-26 17:32:55 +08:00
Gabe Yuan
d7eaac5aca update nav icon 2023-10-26 13:22:01 +08:00
Gabe Yuan
d4526d605c fix i18n text 2023-10-26 13:18:02 +08:00
Gabe Yuan
52979356ca fix i18n text 2023-10-26 13:10:25 +08:00
Gabe Yuan
c6d3d6454f add copy button to tranbox 2023-10-26 12:24:24 +08:00
Gabe Yuan
0d7112187d update readme 2023-10-26 11:17:10 +08:00
Gabe Yuan
045ff3c3d9 support: cloudflare ai 2023-10-26 11:13:50 +08:00
Gabe Yuan
dd68a73efd set overflowWrap for long url 2023-10-26 10:41:04 +08:00
Gabe Yuan
5947dc182e remove log 2023-10-25 16:19:58 +08:00
Gabe Yuan
e185bbdb4d v1.7.9 2023-10-25 16:08:06 +08:00
Gabe Yuan
9368320c38 fix btn offset bug 2023-10-25 16:07:09 +08:00
Gabe Yuan
f65314bc2d v1.7.8 2023-10-25 14:55:08 +08:00
Gabe Yuan
1791e36038 fix readme 2023-10-25 13:36:46 +08:00
Gabe Yuan
8d93094af5 fix mouseover translate 2023-10-25 13:26:31 +08:00
Gabe Yuan
43f34fe6ed tranbox 2023-10-25 11:20:05 +08:00
Gabe Yuan
83911b2164 tranbox 2023-10-25 11:14:18 +08:00
Gabe Yuan
94c7494e90 tranbox 2023-10-25 11:13:56 +08:00
Gabe Yuan
65f2177299 tranbox 2023-10-24 23:50:07 +08:00
Gabe Yuan
f033b11e63 tranbox 2023-10-24 23:37:53 +08:00
Gabe Yuan
02f26af592 tranbox... 2023-10-24 17:58:37 +08:00
Gabe Yuan
4125aba808 tranbox... 2023-10-23 18:02:42 +08:00
Gabe Yuan
e89da9120c add common.js 2023-10-23 13:35:57 +08:00
Gabe Yuan
160fc218fc add more translators: baidu/tencent/deeplfree 2023-10-21 11:54:04 +08:00
Gabe Yuan
507d54dba0 add more translators... 2023-10-20 17:44:48 +08:00
Gabe Yuan
f3029a0f76 update task pool 2023-10-19 14:28:18 +08:00
Gabe Yuan
53181588cf support deeplx api 2023-10-17 11:33:26 +08:00
Gabe Yuan
f88aa159fc fix deepl bug: remove split_sentences param 2023-10-16 16:16:33 +08:00
Gabe Yuan
fb2b517a67 add webfix: FIXER_BN 2023-10-16 15:48:53 +08:00
Gabe Yuan
6e952a9530 update readme 2023-10-13 15:14:43 +08:00
Gabe Yuan
d9acef8d56 v1.7.7 2023-10-13 14:41:25 +08:00
Gabe Yuan
113a4d8eca modify langs map 2023-10-13 11:31:11 +08:00
Gabe Yuan
6d5b93c01b modify langs map 2023-10-13 10:48:01 +08:00
Gabe Yuan
5746911651 update app description & readme 2023-10-13 10:20:14 +08:00
Gabe Yuan
7173692db7 correct function name 2023-10-12 17:00:18 +08:00
Gabe Yuan
b13a63e568 fix: openai default url 2023-10-11 14:28:00 +08:00
Gabe Yuan
791ec65579 detect lang remote 2023-10-11 10:27:51 +08:00
Gabe Yuan
dd99fddc07 detect lang remote 2023-10-11 09:48:52 +08:00
Gabe Yuan
5cd6977a6e detect lang remote 2023-10-10 18:03:05 +08:00
Gabe Yuan
5af66204c4 rm wrangler package 2023-10-07 16:21:17 +08:00
Gabe Yuan
595efe808f rm yarn packageManager 2023-10-07 16:17:21 +08:00
Gabe Yuan
7b4b3b020c v1.7.6 2023-10-07 15:09:12 +08:00
Gabe Yuan
3e96540b56 fix createCache options key 2023-10-07 15:08:10 +08:00
Gabe Yuan
88b791bd73 fix createCache options key 2023-10-07 15:01:23 +08:00
Gabe Yuan
7817019e70 fix createCache options key 2023-10-07 14:57:36 +08:00
Gabe Yuan
6013bbd32c v1.7.5 2023-09-28 16:19:39 +08:00
Gabe Yuan
40e0b96f39 fix GM.xmlHttpRequest response 2023-09-28 16:18:28 +08:00
Gabe Yuan
16560fbdf0 fix install links 2023-09-27 15:39:57 +08:00
Gabe Yuan
16fdd704aa v1.7.4 2023-09-27 15:08:45 +08:00
Gabe Yuan
44e84d9259 minor revision 2023-09-25 11:56:46 +08:00
Gabe Yuan
18d29461ce minor revision 2023-09-25 11:47:12 +08:00
Gabe Yuan
31fb749e93 minor revision 2023-09-25 11:43:14 +08:00
Gabe Yuan
e17931493b add fab for ext 2023-09-23 19:16:51 +08:00
Gabe Yuan
a395f0b31b change newline length to 20 2023-09-22 16:37:35 +08:00
Gabe Yuan
c819896b43 change newline length to 30 2023-09-22 16:33:00 +08:00
Gabe Yuan
733ec92c9c v1.7.3 2023-09-22 15:40:02 +08:00
Gabe Yuan
7c67bb7181 change shortcut e.key to e.code 2023-09-22 15:33:37 +08:00
Gabe Yuan
87f099dd7f Solve the problem of multi-layer shadowroot selector and input box translation 2023-09-22 15:33:02 +08:00
Gabe Yuan
5306d81284 fix shortcut bug 2023-09-22 11:21:38 +08:00
Gabe Yuan
471a4a3159 fix workflows: replace yarn to pnpm 2023-09-21 16:58:49 +08:00
Gabe Yuan
a2f99da3b4 v1.7.2 2023-09-21 16:48:51 +08:00
Gabe Yuan
accab22d56 update readme 2023-09-21 16:41:49 +08:00
Gabe Yuan
6ea5228a5f fix sync 2023-09-21 16:13:00 +08:00
Gabe Yuan
a07d2cafb6 fix sync 2023-09-21 11:47:22 +08:00
Gabe Yuan
1b38f19cc1 replace yarn to pnpm 2023-09-21 10:31:45 +08:00
Gabe Yuan
aa5b286e0b replace yarn to pnpm 2023-09-21 10:15:03 +08:00
Gabe Yuan
6b6bbed330 fix sync 2023-09-20 22:15:09 +08:00
Gabe Yuan
489bc9534b fix sync 2023-09-20 17:47:23 +08:00
Gabe Yuan
01ebc184ad add globalThis.ContextType 2023-09-20 16:02:17 +08:00
Gabe Yuan
f591d66365 fix clear cache 2023-09-19 12:18:01 +08:00
Gabe Yuan
80782287d8 sync webdav cors 2023-09-19 11:56:19 +08:00
Gabe Yuan
3494bb1297 sync webdav 2023-09-18 17:36:10 +08:00
Gabe Yuan
92ffda5220 sync by worker 2023-09-18 15:45:32 +08:00
Gabe Yuan
fbaeff6b7b update api pathname 2023-09-18 13:28:36 +08:00
Gabe Yuan
248d3726dd fix storage hook 2023-09-17 22:34:28 +08:00
Gabe Yuan
1553559b1a fix storage hook 2023-09-17 21:50:17 +08:00
Gabe Yuan
8935ced75a fix iframe bug 2023-09-17 20:45:05 +08:00
Gabe Yuan
a865d6d74f update readme 2023-09-16 21:16:04 +08:00
Gabe Yuan
6d976554fd update readme 2023-09-16 20:11:10 +08:00
Gabe Yuan
189b7f480a v1.7.1 2023-09-15 22:04:30 +08:00
Gabe Yuan
5e3aa7e2d1 update readme 2023-09-15 22:03:50 +08:00
Gabe Yuan
730be678ef input box trans 2023-09-15 21:39:41 +08:00
Gabe Yuan
9293f422f3 input box trans 2023-09-15 20:44:01 +08:00
Gabe Yuan
6e8158bb34 input box trans 2023-09-15 17:58:00 +08:00
Gabe Yuan
3078d3ca91 input box trans 2023-09-15 17:52:06 +08:00
Gabe Yuan
947e1c7f08 input box trans 2023-09-15 17:29:42 +08:00
Gabe Yuan
938c123412 input box trans 2023-09-15 17:25:58 +08:00
Gabe Yuan
e7a57ad3b2 fix svg 2023-09-15 15:45:51 +08:00
Gabe Yuan
1e40f81bf7 input box trans 2023-09-14 16:35:42 +08:00
Gabe Yuan
72b2f44e32 input box trans 2023-09-14 14:45:22 +08:00
Gabe Yuan
76f54461e7 input box trans 2023-09-14 10:59:50 +08:00
Gabe Yuan
14ca13e31d input box trans 2023-09-13 23:24:55 +08:00
Gabe Yuan
556fd71275 Merge branch 'dev' of github.com:fishjar/kiss-translator into dev 2023-09-13 22:12:08 +08:00
Gabe Yuan
a8002bba9f input box trans 2023-09-13 22:11:33 +08:00
Gabe Yuan
ddd9371fbd sync webfix interval 2023-09-13 18:02:51 +08:00
Gabe Yuan
0ea97b73e3 input box trans 2023-09-13 15:53:40 +08:00
Gabe Yuan
f8c8a4ebeb ui fix 2023-09-13 11:16:56 +08:00
Gabe Yuan
5f613ab558 sync webfix interval 2023-09-13 10:26:30 +08:00
Gabe Yuan
56281f9e82 fix save rule 2023-09-12 17:20:56 +08:00
Gabe Yuan
5e8743dbb7 change fab ui 2023-09-12 15:44:30 +08:00
Gabe Yuan
f4e4c84712 popup ui 2023-09-12 11:00:54 +08:00
Gabe Yuan
c57a0a11fa v1.7.0 2023-09-11 23:21:15 +08:00
Gabe Yuan
fa244b2097 subrules sync time 2023-09-11 22:53:04 +08:00
Gabe Yuan
79612f8a1b subrules sync time 2023-09-11 17:56:31 +08:00
Gabe Yuan
2bf79dbc51 rootSlector -> rootSelector 2023-09-11 16:12:37 +08:00
Gabe Yuan
c2658d5dd0 v1.6.12 2023-09-11 14:35:52 +08:00
Gabe Yuan
13684884c7 i18n text 2023-09-11 13:45:26 +08:00
Gabe Yuan
f216a9254e update readme 2023-09-11 13:42:44 +08:00
Gabe Yuan
dbdbcbba2d update readme 2023-09-11 13:37:19 +08:00
Gabe Yuan
2ee4609192 fix sync bug 2023-09-11 11:33:28 +08:00
Gabe Yuan
0d93cf78f7 fix i18n text 2023-09-10 21:59:28 +08:00
Gabe Yuan
3398ca0dd7 fix i18n text 2023-09-10 21:56:06 +08:00
Gabe Yuan
c1778fbcbb v1.6.11 2023-09-10 15:23:50 +08:00
Gabe Yuan
1ef9974c05 fix sync webfix 2023-09-10 15:04:41 +08:00
Gabe Yuan
399c6b6fed add global rule text 2023-09-10 14:51:16 +08:00
Gabe Yuan
62a60eee44 update help text 2023-09-10 14:34:55 +08:00
Gabe Yuan
54339af885 update readme 2023-09-10 14:19:31 +08:00
Gabe Yuan
06cfd33e60 add open options shortcut for ext 2023-09-10 13:56:11 +08:00
Gabe Yuan
08c9d78d2a add save rule button 2023-09-10 13:44:34 +08:00
Gabe Yuan
e7a5e5dce1 add save rule button 2023-09-10 13:09:19 +08:00
Gabe Yuan
3a59a127d1 add save rule button 2023-09-10 12:35:03 +08:00
Gabe Yuan
26f213cad2 add fontsize fixer 2023-09-09 23:32:17 +08:00
Gabe Yuan
7b6148302d v1.6.10 2023-09-09 20:15:09 +08:00
Gabe Yuan
38c781b8f3 fix open setting shortcut 2023-09-09 20:13:36 +08:00
Gabe Yuan
64d827fdcd v1.6.9 2023-09-09 19:51:32 +08:00
Gabe Yuan
74ad812f37 text opacity 2023-09-09 19:43:12 +08:00
Gabe Yuan
364c829119 fix sync bug 2023-09-09 19:26:22 +08:00
Gabe Yuan
1ac2c5b61e fix shortcut 2023-09-09 17:15:13 +08:00
Gabe Yuan
0766199353 check shortcut length 2023-09-09 15:26:05 +08:00
Gabe Yuan
878bccf151 hide fab & open setting shortcut 2023-09-09 15:08:34 +08:00
Gabe Yuan
acbd258296 shorten english tab name 2023-09-09 14:10:01 +08:00
Gabe Yuan
54a14e6e5a shortcut set blank 2023-09-09 14:05:45 +08:00
Gabe Yuan
298e4b52f0 v1.6.8 2023-09-08 23:43:34 +08:00
Gabe Yuan
bee1fbcf88 follow global style bug 2023-09-08 23:38:36 +08:00
Gabe Yuan
345a34287e help button 2023-09-08 21:57:42 +08:00
Gabe Yuan
441a2ca2da webfix 2023-09-08 21:41:32 +08:00
Gabe Yuan
1ff1b21355 fix get setting with default 2023-09-08 17:10:54 +08:00
Gabe Yuan
117ca4e05b webfix setting 2023-09-08 16:56:00 +08:00
Gabe Yuan
07d457be4e add clear all rules button 2023-09-08 15:32:42 +08:00
Gabe Yuan
d48296046e show subrule sync time 2023-09-08 15:16:10 +08:00
Gabe Yuan
56350de2cf fix shortcut 2023-09-08 13:53:33 +08:00
Gabe Yuan
850dc0e83b fix throttle func 2023-09-08 10:52:42 +08:00
Gabe Yuan
35f01478b1 add throttle func 2023-09-08 10:32:44 +08:00
Gabe Yuan
f9a3ec012f fix shortcut 2023-09-07 23:51:08 +08:00
Gabe Yuan
3b9b404482 shortcuts dev 2023-09-07 23:47:24 +08:00
Gabe Yuan
d8b0cc4834 shortcuts dev... 2023-09-07 18:12:45 +08:00
Gabe Yuan
da13f5e218 add isAllchar func 2023-09-07 10:20:08 +08:00
Gabe Yuan
08e14ae11c fix i18n text 2023-09-07 00:03:19 +08:00
Gabe Yuan
c2902dff28 fix i18n text 2023-09-06 23:58:11 +08:00
Gabe Yuan
c4fb39f02f fix var text 2023-09-06 23:44:01 +08:00
Gabe Yuan
b7df44c35a hide clear cache in userscript 2023-09-06 23:39:11 +08:00
Gabe Yuan
9a2b21eee5 mouseover dev 2023-09-06 23:35:09 +08:00
Gabe Yuan
bdac67df88 mouseover dev... 2023-09-06 18:00:18 +08:00
Gabe Yuan
0b8f19bfad remove styled-components 2023-09-06 15:12:17 +08:00
Gabe Yuan
c7c5866131 customize api 2023-09-06 14:57:02 +08:00
Gabe Yuan
f772fa000c customize api... 2023-09-06 00:25:46 +08:00
Gabe Yuan
93fd82fcd9 fuzzy style 2023-09-05 17:00:18 +08:00
Gabe Yuan
3ae10bfd04 fix herf match func 2023-09-05 16:11:33 +08:00
Gabe Yuan
a44747ccad fix herf match func 2023-09-05 16:06:48 +08:00
Gabe Yuan
87ab45f936 fix readme text 2023-09-05 13:35:12 +08:00
Gabe Yuan
37b046eb46 fix rule selector 2023-09-05 13:27:04 +08:00
Gabe Yuan
c6f8a45027 v1.6.7 2023-09-04 23:28:41 +08:00
Gabe Yuan
6ec16e1f98 fix popup header 2023-09-04 23:24:50 +08:00
Gabe Yuan
40adf85b20 fix help link 2023-09-04 23:12:35 +08:00
Gabe Yuan
4c78f469c1 update readme 2023-09-04 23:08:00 +08:00
Gabe Yuan
55af58faac add rules help button 2023-09-04 22:29:39 +08:00
Gabe Yuan
4200caa641 fix popup header 2023-09-04 22:03:17 +08:00
Gabe Yuan
0ac06f8e3d use new rules link 2023-09-04 17:04:28 +08:00
Gabe Yuan
966c78fb16 optimize test link 2023-09-03 22:18:33 +08:00
Gabe Yuan
5c5a35d3bb v1.6.6 2023-09-03 21:47:38 +08:00
Gabe Yuan
2c24214f48 fix Violentmonkey --> .connect 2023-09-03 21:45:06 +08:00
Gabe Yuan
67d9e70b3c v1,6,5 2023-09-03 21:10:35 +08:00
Gabe Yuan
000a55f43b modify content.html 2023-09-03 21:08:45 +08:00
Gabe Yuan
4096a6976c add clear cache & api test button 2023-09-03 13:11:04 +08:00
Gabe Yuan
df4c4ebd50 fix i18n text 2023-09-03 00:26:57 +08:00
Gabe Yuan
b43bd4e0e2 add deepl @connect 2023-09-03 00:10:07 +08:00
Gabe Yuan
2660dbf866 update readme 2023-09-02 23:45:40 +08:00
Gabe Yuan
e0b7c60099 v1.6.4 2023-09-02 20:07:24 +08:00
Gabe Yuan
536b58bf67 fix fuzzy style hover bug 2023-09-02 19:55:26 +08:00
Gabe Yuan
6bb742f828 v1.6.3 2023-09-02 19:16:14 +08:00
Gabe Yuan
72742e5e12 fix build:script 2023-09-02 19:09:12 +08:00
Gabe Yuan
3667e0a509 update readme 2023-09-02 18:14:42 +08:00
Gabe Yuan
c2d7668ba7 v1.6.2 2023-09-02 17:26:37 +08:00
Gabe Yuan
aa830f5e20 update readme 2023-09-02 17:22:35 +08:00
Gabe Yuan
b593fa4146 add deepl api 2023-09-02 16:57:09 +08:00
Gabe Yuan
b00b906484 optimize inject script 2023-09-02 15:54:51 +08:00
Gabe Yuan
c1bd6a1be6 use random eventname 2023-09-02 14:14:27 +08:00
Gabe Yuan
36739f04b3 add more shortcut 2023-09-02 13:14:27 +08:00
Gabe Yuan
23eb92853e register a shortcut for userscript 2023-09-02 00:42:15 +08:00
Gabe Yuan
5ab2910dc7 v1.6.1 2023-09-01 22:46:41 +08:00
Gabe Yuan
40d07f6764 fix proxy link 2023-09-01 22:28:28 +08:00
Gabe Yuan
5c8e216169 overwrite subscribe rules 2023-09-01 22:27:25 +08:00
Gabe Yuan
5ba061deda merge yarn 2023-09-01 19:35:48 +08:00
Gabe Yuan
935c83185d fix style text 2023-09-01 17:07:21 +08:00
Gabe Yuan
6327391e65 hide color input when diy style 2023-09-01 17:02:47 +08:00
Gabe Yuan
3d656cf5b0 use debounce to sync setting 2023-09-01 16:39:57 +08:00
Gabe Yuan
d570a0f1a2 replace 'document.documentElement.clientWidth' to 'window.innerWidth' 2023-09-01 16:11:31 +08:00
Gabe Yuan
503a71302c support diy text styles 2023-09-01 15:57:05 +08:00
Gabe Yuan
3e36ceb5b9 num of newline characters 2023-09-01 11:21:06 +08:00
Gabe Yuan
cde7a1d49f add kiss-proxy link 2023-09-01 11:03:53 +08:00
Gabe Yuan
b14a38e4fb sync then reload setting 2023-09-01 10:15:57 +08:00
85 changed files with 16393 additions and 16374 deletions

9
.env
View File

@@ -2,7 +2,7 @@ GENERATE_SOURCEMAP=false
REACT_APP_NAME=KISS Translator REACT_APP_NAME=KISS Translator
REACT_APP_NAME_CN=简约翻译 REACT_APP_NAME_CN=简约翻译
REACT_APP_VERSION=1.6.0 REACT_APP_VERSION=1.7.11
REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator
@@ -13,8 +13,11 @@ 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_LOGOURL2=https://kiss-translator.rayjar.com/images/logo192.png
REACT_APP_RULESURL=https://fishjar.github.io/kiss-translator/kiss-translator-rules.json REACT_APP_RULESURL=https://fishjar.github.io/kiss-rules/kiss-rules.json
REACT_APP_RULESURL2=https://kiss-translator.rayjar.com/kiss-translator-rules.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_WEBFIXURL=https://fishjar.github.io/kiss-rules/kiss-webfix.json
REACT_APP_VERSIONFILE=https://fishjar.github.io/kiss-translator/version.txt REACT_APP_VERSIONFILE=https://fishjar.github.io/kiss-translator/version.txt
REACT_APP_VERSIONFILE2=https://kiss-translator.rayjar.com/version.txt REACT_APP_VERSIONFILE2=https://kiss-translator.rayjar.com/version.txt

View File

@@ -10,12 +10,15 @@ jobs:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 8.7.6
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: "18.17.0" node-version: "18.17.0"
cache: "yarn" cache: "pnpm"
- run: yarn install - run: pnpm install
- run: yarn build - run: pnpm build
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
with: with:
name: build-artifacts name: build-artifacts

View File

@@ -1 +0,0 @@
nodeLinker: node-modules

View File

@@ -1,62 +1,86 @@
## KISS Translator # KISS Translator
A minimalist [bilingual translation Extension & Greasemonkey Script](https://github.com/fishjar/kiss-translator). A simple [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)
### Inspiration ## Features
The inspiration for this project comes from [Immersive Translate](https://github.com/immersive-translate/immersive-translate). After trying it out, I found that it can be used together with the [Webpage Word Translation Extension](https://github.com/fishjar/kiss-dictionary) developed by me earlier, which just forms a very good supplement. - [x] Keep it simple, smart
But the function of this extension is a bit complicated for me, and only the compiled and obfuscated installation package is provided, and the source code is not provided, which cannot meet some of my personalized customization needs.
It just so happens that I am obsessed with translation tools. Based on the concept of "mainly for personal use, as long as you can use it", I made one. At present, the first version is completed, which basically meets the needs of personal use.
If you also like a little more simplicity, welcome to pick it up.
### Features
- Keep it simple, smart
### Schedule
- [x] Provide trial installation package
- [x] Adapt browser
- [x] Chrome
- [x] Edge
- [x] Firefox
- [ ] Safari
- [x] Kiwi
- [x] Support translation services
- [x] Google
- [x] Microsoft
- [x] OpenAI
- [ ] DeepL
- [x] Upload to app Store
- [x] Chrome [Install Link](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof)
- [x] Edge [Install Link](https://microsoftedge.microsoft.com/addons/detail/kiss-translator/jemckldkclkinpjighnoilpbldbdmmlh)
- [x] Firefox [Install Link](https://addons.mozilla.org/en-US/firefox/addon/kiss-translator/)
- [ ] Safari
- [x] Greasy Fork [Install Link](https://greasyfork.org/en/scripts/472840-kiss-translator)
- [x] Open source - [x] Open source
- [x] Data Synchronization Function - [x] Adapt to common browsers
- [x] Greasemonkey Script ([Setting Page 1](https://fishjar.github.io/kiss-translator/options.html)、[Setting Page 2](https://kiss-translator.rayjar.com/options)) - [x] Chrome/Edge/Firefox/Kiwi
- [x] [Tampermonkey](https://www.tampermonkey.net/) (Chrome/Edge/Firefox) [Install Link 1](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)、[Install Link 2](https://kiss-translator.rayjar.com/kiss-translator.user.js) - [ ] Safari
- [x] [Userscripts Safari](https://github.com/quoid/userscripts) (iOS Safari) [Install Link 1](https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js)、[Install Link 2](https://kiss-translator.rayjar.com/kiss-translator.user-ios-safari.js) - [x] Supports multiple translation services
- [x] Google/Microsoft/DeepL/OpenAI/CloudflareAI/Baidu/Tencent
- [x] Custom translation interface
- [x] Covers common translation scenarios
- [x] Web bilingual translation
- [x] Input box translation
- [x] Seletction translation
- [x] Favorite Words
- [x] Mouseover translation
- [x] YouTube subtitle translation
- [x] Cross-client data synchronization
- [x] KISS-Workercloudflare/docker
- [x] WebDAV
- [x] Custom translation rules
- [x] Rule subscription/rule sharing
- [x] Custom translation style
- [x] Custom shortcut keys
- `Alt+Q` Toggle Translation
- `Alt+C` Toggle Styles
- `Alt+K` Open Setting Popup
- `Alt+B` Open Translate Popup
- `Alt+O` Open Options Page
- `Alt+I` Input Box Translation
### Guide ## Install
> Note: For the following reasons, it is recommended to use browser extensions first
>
> - Browser extension can use local language recognition
> - Grease Monkey script will encounter more usage problems
- [x] Browser extension
- [x] Chrome [Installation address](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof?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/)
- [ ] Safari
- [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)
- Greasy Fork [Installation address](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)
## Associated Projects
- Data synchronization service: [https://github.com/fishjar/kiss-worker](https://github.com/fishjar/kiss-worker)
- Data synchronization service available for this project.
- Can also be used to share personal private rule lists.
- Deploy by yourself, manage by yourself, data is private.
- 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.
- 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)
- If you encounter network problems when accessing a certain translation interface, this proxy service may help you.
- 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.
- Supports query of English words, sentences and Chinese characters.
- Supports history records and word collections.
## 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
yarn install pnpm install
yarn build pnpm build
``` ```
### Data Sync ## Discussion
Goto: [https://github.com/fishjar/kiss-worker](https://github.com/fishjar/kiss-worker)
### Discussion
- Join [Telegram Group](https://t.me/+RRCu_4oNwrM2NmFl) - Join [Telegram Group](https://t.me/+RRCu_4oNwrM2NmFl)

110
README.md
View File

@@ -1,62 +1,86 @@
## 简约翻译 # 简约翻译
一个简约的 [双语网页翻译扩展 & 油猴脚本](https://github.com/fishjar/kiss-translator)。 一个简约的 [双语对照翻译扩展 & 油猴脚本](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)
### 缘由 ## 特性
本项目灵感来源于 [Immersive Translate](https://github.com/immersive-translate/immersive-translate),在试用了后,发现搭配本人早前开发的 [网页划词翻译扩展](https://github.com/fishjar/kiss-dictionary) 一起使用,刚好形成很好补充。 - [x] 保持简约
- [x] 开放源代码
但该扩展的功能对我来说有些繁杂了,而且只提供编译混淆后的安装包,没有提供源代码,无法满足我的一些个性化定制需求。 - [x] 适配常见浏览器
- [x] Chrome/Edge/Firefox/Kiwi
恰巧本人对翻译类工具有些执念,本着`“自用为主,能用就行”`的理念,于是动手撸了一个,目前初版完成,基本达到个人使用需求。
如果你也喜欢简约一点的,欢迎自取。
### 特点
- 保持简约
### 进度
- [x] 提供试用安装包
- [x] 适配浏览器
- [x] Chrome
- [x] Edge
- [x] Firefox
- [ ] Safari - [ ] Safari
- [x] Kiwi - [x] 支持多种翻译服务
- [x] 支持翻译服务 - [x] Google/Microsoft/DeepL/OpenAI/CloudflareAI/Baidu/Tencent
- [x] Google - [x] 自定义翻译接口
- [x] Microsoft - [x] 覆盖常见翻译场景
- [x] OpenAI - [x] 网页双语对照翻译
- [ ] DeepL - [x] 输入框翻译
- [x] 上架应用市场 - [x] 划词翻译
- [x] 收藏词汇
- [x] 鼠标悬停翻译
- [x] YouTube 字幕翻译
- [x] 跨客户端数据同步
- [x] KISS-Workercloudflare/docker
- [x] WebDAV
- [x] 自定义翻译规则
- [x] 规则订阅/规则分享
- [x] 自定义译文样式
- [x] 自定义快捷键
- `Alt+Q` 开启翻译
- `Alt+C` 切换样式
- `Alt+K` 打开设置弹窗
- `Alt+B` 打开翻译弹窗
- `Alt+O` 打开设置页面
- `Alt+I` 输入框翻译
## 安装
> 注:基于以下原因,建议优先使用浏览器扩展
>
> - 浏览器扩展可以使用本地的语言识别
> - 油猴脚本会遇到更多使用上的问题
- [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] 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] Greasy Fork [安装地址](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator) - [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] 数据同步功能 - Greasy Fork [安装地址](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator)
- [x] 油猴脚本 ([设置页面 1](https://fishjar.github.io/kiss-translator/options.html)、[设置页面 2](https://kiss-translator.rayjar.com/options)) - [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] [Tampermonkey](https://www.tampermonkey.net/) (Chrome/Edge/Firefox) [安装链接 1](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)、[安装链接 2](https://kiss-translator.rayjar.com/kiss-translator.user.js)
- [x] [Userscripts Safari](https://github.com/quoid/userscripts) (iOS Safari) [安装链接 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)
### 指引 ## 关联项目
- 数据同步服务: [https://github.com/fishjar/kiss-worker](https://github.com/fishjar/kiss-worker)
- 可用于本项目的数据同步服务。
- 亦可用于分享个人的私有规则列表。
- 自己部署,自己管理,数据私有。
- 社区订阅规则: [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-dictionary](https://github.com/fishjar/kiss-dictionary)
- 搭配本项目一起使用的划词翻译插件。
- 支持英文单词、句子、汉字的查询。
- 支持历史记录、单词收藏。
## 开发指引
```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
yarn install pnpm install
yarn build pnpm build
``` ```
### 数据同步 ## 交流
移步: [https://github.com/fishjar/kiss-worker](https://github.com/fishjar/kiss-worker)
### 交流
- 加入 [Telegram 群](https://t.me/+RRCu_4oNwrM2NmFl) - 加入 [Telegram 群](https://t.me/+RRCu_4oNwrM2NmFl)

View File

@@ -75,7 +75,7 @@ const userscriptWebpack = (config, env) => {
// @name ${process.env.REACT_APP_NAME} // @name ${process.env.REACT_APP_NAME}
// @namespace ${process.env.REACT_APP_HOMEPAGE} // @namespace ${process.env.REACT_APP_HOMEPAGE}
// @version ${process.env.REACT_APP_VERSION} // @version ${process.env.REACT_APP_VERSION}
// @description A minimalist bilingual translation Extension & Greasemonkey Script (一个简约的双语网页翻译扩展 & 油猴脚本) // @description A simple bilingual translation extension & Greasemonkey script (一个简约的双语对照翻译扩展 & 油猴脚本)
// @author Gabe<yugang2002@gmail.com> // @author Gabe<yugang2002@gmail.com>
// @homepageURL ${process.env.REACT_APP_HOMEPAGE} // @homepageURL ${process.env.REACT_APP_HOMEPAGE}
// @license GPL-3.0 // @license GPL-3.0
@@ -93,6 +93,9 @@ const userscriptWebpack = (config, env) => {
// @connect translate.googleapis.com // @connect translate.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.deepl.com
// @connect www2.deepl.com
// @connect api.openai.com // @connect api.openai.com
// @connect openai.azure.com // @connect openai.azure.com
// @connect workers.dev // @connect workers.dev
@@ -100,6 +103,13 @@ const userscriptWebpack = (config, env) => {
// @connect githubusercontent.com // @connect githubusercontent.com
// @connect kiss-translator.rayjar.com // @connect kiss-translator.rayjar.com
// @connect ghproxy.com // @connect ghproxy.com
// @connect dav.jianguoyun.com
// @connect fanyi.baidu.com
// @connect transmart.qq.com
// @connect localhost:3000
// @connect 127.0.0.1:3000
// @connect localhost:1188
// @connect 127.0.0.1:1188
// @run-at document-end // @run-at document-end
// ==/UserScript== // ==/UserScript==

View File

@@ -1,10 +1,11 @@
{ {
"name": "kiss-translator", "name": "kiss-translator",
"description": "A minimalist bilingual translation Extension & Greasemonkey Script", "description": "A minimalist bilingual translation Extension & Greasemonkey Script",
"version": "1.6.0", "version": "1.7.11",
"author": "Gabe<yugang2002@gmail.com>", "author": "Gabe<yugang2002@gmail.com>",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@emotion/cache": "^11.11.0",
"@emotion/react": "^11.11.1", "@emotion/react": "^11.11.1",
"@emotion/styled": "^11.10.8", "@emotion/styled": "^11.10.8",
"@mui/icons-material": "^5.11.11", "@mui/icons-material": "^5.11.11",
@@ -15,6 +16,7 @@
"react-markdown": "^8.0.7", "react-markdown": "^8.0.7",
"react-router-dom": "^6.10.0", "react-router-dom": "^6.10.0",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"webdav": "^5.3.0",
"webextension-polyfill": "^0.10.0" "webextension-polyfill": "^0.10.0"
}, },
"scripts": { "scripts": {
@@ -24,10 +26,10 @@
"build:edge": "rm -rf build/edge && cp -r build/chrome build/edge", "build:edge": "rm -rf build/edge && cp -r build/chrome build/edge",
"build:firefox": "rm -rf build/firefox && cp -r build/chrome build/firefox && cat ./build/firefox/manifest.firefox.json > ./build/firefox/manifest.json", "build:firefox": "rm -rf build/firefox && cp -r build/chrome build/firefox && cat ./build/firefox/manifest.firefox.json > ./build/firefox/manifest.json",
"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": "rm -rf build/userscript && mkdir build/userscript && cp build/web/kiss-translator.user.js build/userscript/kiss-translator.user.js", "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/userscript/kiss-translator.user.js file2=build/userscript/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:rules": "babel-node src/rules.js", "build:rules": "babel-node src/rules.js",
"build": "yarn build:chrome && yarn build:edge && yarn build:firefox && yarn build:web && yarn build:userscript && yarn build:userscript-ios && yarn build:rules", "build": "pnpm build:chrome && pnpm build:edge && pnpm build:firefox && pnpm build:web && pnpm build:userscript-ios && pnpm build:userscript && pnpm build:rules",
"deploy:web": "wrangler pages deploy ./build/web --project-name kiss-translator", "deploy:web": "wrangler pages deploy ./build/web --project-name kiss-translator",
"test": "react-app-rewired test", "test": "react-app-rewired test",
"eject": "react-scripts eject" "eject": "react-scripts eject"
@@ -39,7 +41,8 @@
], ],
"globals": { "globals": {
"GM": true, "GM": true,
"unsafeWindow": true "unsafeWindow": true,
"globalThis": true
} }
}, },
"browserslist": { "browserslist": {
@@ -59,7 +62,6 @@
"@babel/node": "^7.22.10", "@babel/node": "^7.22.10",
"@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.10",
"react-app-rewired": "^2.2.1", "react-app-rewired": "^2.2.1"
"wrangler": "^3.4.0"
} }
} }

10267
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -3,12 +3,15 @@
"message": "KISS Translator" "message": "KISS Translator"
}, },
"app_description": { "app_description": {
"message": "A minimalist bilingual translation Extension & Greasemonkey Script" "message": "A simple bilingual translation extension & Greasemonkey script"
}, },
"toggle_translate": { "toggle_translate": {
"message": "Toggle Translate" "message": "Toggle Translate"
}, },
"toggle_style": { "toggle_style": {
"message": "Toggle Style" "message": "Toggle Style"
},
"open_options": {
"message": "Open Options"
} }
} }

View File

@@ -3,12 +3,15 @@
"message": "简约翻译" "message": "简约翻译"
}, },
"app_description": { "app_description": {
"message": "一个简约的双语网页翻译扩展 & 油猴脚本" "message": "一个简约的双语对照翻译扩展 & 油猴脚本"
}, },
"toggle_translate": { "toggle_translate": {
"message": "切换翻译" "message": "开启翻译"
}, },
"toggle_style": { "toggle_style": {
"message": "切换样式" "message": "切换样式"
},
"open_options": {
"message": "打开设置"
} }
} }

View File

@@ -64,9 +64,31 @@
<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">
<div id="content">
<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
coming to Google Cloud with the C3A instances.
<br />
But these upcoming instances for now are only in private preview form.
<br />
<br />
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
Ampere Computing.
<br />
</div>
<h2> <h2>
<p><span>React is a JavaScript library for building user interfaces.</span></p> <p>
<span
>React is a JavaScript library for building user interfaces.</span
>
</p>
</h2> </h2>
<hr />
<input id="input1" style="width: 80%;" />
<hr />
<textarea id="textarea1" style="width: 80%;">test</textarea>
<hr />
<div id="addtitle"></div> <div id="addtitle"></div>
<h2>Shadow 1</h2> <h2>Shadow 1</h2>
<div id="shadow1"></div> <div id="shadow1"></div>

View File

@@ -2,7 +2,7 @@
"manifest_version": 2, "manifest_version": 2,
"name": "__MSG_app_name__", "name": "__MSG_app_name__",
"description": "__MSG_app_description__", "description": "__MSG_app_description__",
"version": "1.6.0", "version": "1.7.11",
"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",
@@ -17,6 +17,11 @@
} }
], ],
"commands": { "commands": {
"_execute_browser_action": {
"suggested_key": {
"default": "Alt+K"
}
},
"toggleTranslate": { "toggleTranslate": {
"suggested_key": { "suggested_key": {
"default": "Alt+Q" "default": "Alt+Q"
@@ -28,6 +33,12 @@
"default": "Alt+C" "default": "Alt+C"
}, },
"description": "__MSG_toggle_style__" "description": "__MSG_toggle_style__"
},
"openOptions": {
"suggested_key": {
"default": "Alt+O"
},
"description": "__MSG_open_options__"
} }
}, },
"permissions": ["<all_urls>", "storage"], "permissions": ["<all_urls>", "storage"],

View File

@@ -2,7 +2,7 @@
"manifest_version": 3, "manifest_version": 3,
"name": "__MSG_app_name__", "name": "__MSG_app_name__",
"description": "__MSG_app_description__", "description": "__MSG_app_description__",
"version": "1.6.0", "version": "1.7.11",
"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",
@@ -18,6 +18,11 @@
} }
], ],
"commands": { "commands": {
"_execute_action": {
"suggested_key": {
"default": "Alt+K"
}
},
"toggleTranslate": { "toggleTranslate": {
"suggested_key": { "suggested_key": {
"default": "Alt+Q" "default": "Alt+Q"
@@ -29,6 +34,12 @@
"default": "Alt+C" "default": "Alt+C"
}, },
"description": "__MSG_toggle_style__" "description": "__MSG_toggle_style__"
},
"openOptions": {
"suggested_key": {
"default": "Alt+O"
},
"description": "__MSG_open_options__"
} }
}, },
"permissions": ["storage"], "permissions": ["storage"],

230
src/apis/baidu.js Normal file
View File

@@ -0,0 +1,230 @@
import queryString from "query-string";
import { getBdauth, setBdauth } from "../libs/storage";
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 }) => {
const { token, gtk } = await bdAuth();
const sign = getSign(text, gtk);
const data = {
from,
to,
query: text,
simple_means_flag: 3,
sign,
token,
domain: "common",
ts: Date.now(),
};
const input = `${URL_BAIDU_TRAN}?from=${from}&to=${to}`;
const init = {
headers: {
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
},
method: "POST",
body: queryString.stringify(data),
};
return [input, init];
};

58
src/apis/deepl.js Normal file
View File

@@ -0,0 +1,58 @@
import { URL_DEEPLFREE_TRAN } from "../config";
let id = 1e4 * Math.round(1e4 * Math.random());
export const genDeeplFree = ({ text, from, to }) => {
const iCount = (text.match(/[i]/g) || []).length + 1;
let timestamp = Date.now();
timestamp = timestamp + (iCount - (timestamp % iCount));
id++;
let body = JSON.stringify({
jsonrpc: "2.0",
method: "LMT_handle_texts",
params: {
splitting: "newlines",
lang: {
target_lang: to,
source_lang_user_selected: from,
},
commonJobParams: {
wasSpoken: false,
transcribe_as: "",
},
id,
timestamp,
texts: [
{
text,
requestAlternatives: 3,
},
],
},
});
body = body.replace(
'method":"',
(id + 3) % 13 === 0 || (id + 5) % 29 === 0 ? 'method" : "' : 'method": "'
);
const init = {
headers: {
"Content-Type": "application/json",
Accept: "*/*",
"x-app-os-name": "iOS",
"x-app-os-version": "16.3.0",
"Accept-Language": "en-US,en;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
"x-app-device": "iPhone13,2",
"User-Agent": "DeepL-iOS/2.9.1 iOS 16.3.0 (iPhone13,2)",
"x-app-build": "510265",
"x-app-version": "2.9.1",
},
method: "POST",
body,
};
return [URL_DEEPLFREE_TRAN, init];
};

View File

@@ -3,14 +3,22 @@ import { fetchPolyfill } from "../libs/fetch";
import { import {
OPT_TRANS_GOOGLE, OPT_TRANS_GOOGLE,
OPT_TRANS_MICROSOFT, OPT_TRANS_MICROSOFT,
OPT_TRANS_DEEPL,
OPT_TRANS_DEEPLFREE,
OPT_TRANS_DEEPLX,
OPT_TRANS_BAIDU,
OPT_TRANS_TENCENT,
OPT_TRANS_OPENAI, OPT_TRANS_OPENAI,
URL_MICROSOFT_TRANS, OPT_TRANS_CLOUDFLAREAI,
OPT_LANGS_SPECIAL, OPT_TRANS_CUSTOMIZE,
PROMPT_PLACE_FROM, URL_CACHE_TRAN,
PROMPT_PLACE_TO,
KV_SALT_SYNC, KV_SALT_SYNC,
URL_BAIDU_LANGDETECT,
OPT_LANGS_BAIDU,
URL_TENCENT_TRANSMART,
OPT_LANGS_TENCENT,
OPT_LANGS_SPECIAL,
} from "../config"; } from "../config";
import { tryDetectLang } from "../libs";
import { sha256 } from "../libs/utils"; import { sha256 } from "../libs/utils";
/** /**
@@ -20,7 +28,7 @@ import { sha256 } from "../libs/utils";
* @param {*} data * @param {*} data
* @returns * @returns
*/ */
export const apiSyncData = async (url, key, data, isBg = false) => export const apiSyncData = async (url, key, data) =>
fetchPolyfill(url, { fetchPolyfill(url, {
headers: { headers: {
"Content-type": "application/json", "Content-type": "application/json",
@@ -28,110 +36,62 @@ export const apiSyncData = async (url, key, data, isBg = false) =>
}, },
method: "POST", method: "POST",
body: JSON.stringify(data), body: JSON.stringify(data),
isBg,
}); });
/** /**
* 下载订阅规则 * 下载数据
* @param {*} url * @param {*} url
* @param {*} isBg
* @returns * @returns
*/ */
export const apiFetchRules = (url, isBg = false) => export const apiFetch = (url) => fetchPolyfill(url);
fetchPolyfill(url, { isBg });
/** /**
* 谷歌翻译 * 百度语言识别
* @param {*} text * @param {*} text
* @param {*} to
* @param {*} from
* @returns * @returns
*/ */
const apiGoogleTranslate = async (translator, text, to, from, setting) => { export const apiBaiduLangdetect = async (text) => {
const { googleUrl } = setting; const res = await fetchPolyfill(URL_BAIDU_LANGDETECT, {
const params = {
client: "gtx",
dt: "t",
dj: 1,
ie: "UTF-8",
sl: from,
tl: to,
q: text,
};
const input = `${googleUrl}?${queryString.stringify(params)}`;
return fetchPolyfill(input, {
headers: {
"Content-type": "application/json",
},
useCache: true,
usePool: true,
translator,
});
};
/**
* 微软翻译
* @param {*} text
* @param {*} to
* @param {*} from
* @returns
*/
const apiMicrosoftTranslate = (translator, text, to, from) => {
const params = {
from,
to,
"api-version": "3.0",
};
const input = `${URL_MICROSOFT_TRANS}?${queryString.stringify(params)}`;
return fetchPolyfill(input, {
headers: {
"Content-type": "application/json",
},
method: "POST",
body: JSON.stringify([{ Text: text }]),
useCache: true,
usePool: true,
translator,
});
};
/**
* OpenAI 翻译
* @param {*} text
* @param {*} to
* @param {*} from
* @returns
*/
const apiOpenaiTranslate = async (translator, text, to, from, setting) => {
const { openaiUrl, openaiKey, openaiModel, openaiPrompt } = setting;
let prompt = openaiPrompt
.replaceAll(PROMPT_PLACE_FROM, from)
.replaceAll(PROMPT_PLACE_TO, to);
return fetchPolyfill(openaiUrl, {
headers: { headers: {
"Content-type": "application/json", "Content-type": "application/json",
}, },
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
model: openaiModel, query: text,
messages: [
{
role: "system",
content: prompt,
},
{
role: "user",
content: text,
},
],
temperature: 0,
max_tokens: 256,
}), }),
useCache: true, useCache: true,
usePool: true,
translator,
token: openaiKey,
}); });
if (res.error === 0) {
return OPT_LANGS_BAIDU.get(res.lan) ?? res.lan;
}
return "";
};
/**
* 腾讯语言识别
* @param {*} text
* @returns
*/
export const apiTencentLangdetect = async (text) => {
const body = JSON.stringify({
header: {
fn: "text_analysis",
},
text,
});
const res = await fetchPolyfill(URL_TENCENT_TRANSMART, {
headers: {
"Content-type": "application/json",
},
method: "POST",
body,
useCache: true,
});
return OPT_LANGS_TENCENT.get(res.language) ?? res.language;
}; };
/** /**
@@ -141,32 +101,92 @@ const apiOpenaiTranslate = async (translator, text, to, from, setting) => {
*/ */
export const apiTranslate = async ({ export const apiTranslate = async ({
translator, translator,
q, text,
fromLang, fromLang,
toLang, toLang,
setting, apiSetting = {},
useCache = true,
usePool = true,
}) => { }) => {
let trText = ""; let trText = "";
let isSame = false; let isSame = false;
let from = OPT_LANGS_SPECIAL?.[translator]?.get(fromLang) ?? fromLang; const from =
let to = OPT_LANGS_SPECIAL?.[translator]?.get(toLang) ?? toLang; OPT_LANGS_SPECIAL[translator].get(fromLang) ??
OPT_LANGS_SPECIAL[translator].get("auto");
if (translator === OPT_TRANS_GOOGLE) { const to = OPT_LANGS_SPECIAL[translator].get(toLang);
const res = await apiGoogleTranslate(translator, q, to, from, setting); if (!text || !to) {
trText = res.sentences.map((item) => item.trans).join(" "); console.log(`[trans] target lang: ${toLang} not support`);
isSame = to === res.src; return [trText, isSame];
} else if (translator === OPT_TRANS_MICROSOFT) {
const res = await apiMicrosoftTranslate(translator, q, to, from);
trText = res[0].translations[0].text;
isSame = to === res[0].detectedLanguage.language;
} else if (translator === OPT_TRANS_OPENAI) {
const res = await apiOpenaiTranslate(translator, q, to, from, setting);
trText = res?.choices?.[0].message.content;
const sLang = await tryDetectLang(q);
const tLang = await tryDetectLang(trText);
isSame = q === trText || (sLang && tLang && sLang === tLang);
} }
return [trText, isSame]; const cacheOpts = {
translator,
text,
fromLang,
toLang,
};
const transOpts = {
translator,
text,
from,
to,
};
const res = await fetchPolyfill(
`${URL_CACHE_TRAN}?${queryString.stringify(cacheOpts)}`,
{
useCache,
usePool,
transOpts,
apiSetting,
}
);
switch (translator) {
case OPT_TRANS_GOOGLE:
trText = res.sentences.map((item) => item.trans).join(" ");
isSame = to === res.src;
break;
case OPT_TRANS_MICROSOFT:
trText = res[0].translations.map((item) => item.text).join(" ");
isSame = text === trText;
break;
case OPT_TRANS_DEEPL:
trText = res.translations.map((item) => item.text).join(" ");
isSame = to === res.translations[0].detected_source_language;
break;
case OPT_TRANS_DEEPLFREE:
trText = res.result?.texts.map((item) => item.text).join(" ");
isSame = to === res.result?.lang;
break;
case OPT_TRANS_DEEPLX:
trText = res.data;
isSame = to === res.source_lang;
break;
case OPT_TRANS_BAIDU:
trText = res.trans_result?.data.map((item) => item.dst).join(" ");
isSame = res.trans_result?.to === res.trans_result?.from;
break;
case OPT_TRANS_TENCENT:
trText = res.auto_translation;
isSame = text === trText;
break;
case OPT_TRANS_OPENAI:
trText = res?.choices?.[0].message.content;
isSame = text === trText;
break;
case OPT_TRANS_CLOUDFLAREAI:
trText = res?.result?.translated_text;
isSame = text === trText;
break;
case OPT_TRANS_CUSTOMIZE:
trText = res.text;
isSame = to === res.from;
break;
default:
}
return [trText, isSame, res];
}; };

View File

@@ -4,9 +4,12 @@ import {
MSG_FETCH_LIMIT, MSG_FETCH_LIMIT,
MSG_FETCH_CLEAR, MSG_FETCH_CLEAR,
MSG_TRANS_TOGGLE, MSG_TRANS_TOGGLE,
MSG_OPEN_OPTIONS,
MSG_SAVE_RULE,
MSG_TRANS_TOGGLE_STYLE, MSG_TRANS_TOGGLE_STYLE,
CMD_TOGGLE_TRANSLATE, CMD_TOGGLE_TRANSLATE,
CMD_TOGGLE_STYLE, CMD_TOGGLE_STYLE,
CMD_OPEN_OPTIONS,
} 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";
@@ -14,6 +17,9 @@ import { fetchData, fetchPool } 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";
globalThis.ContextType = "BACKGROUND";
/** /**
* 插件安装 * 插件安装
@@ -29,7 +35,7 @@ browser.runtime.onStartup.addListener(async () => {
console.log("browser onStartup"); console.log("browser onStartup");
// 同步数据 // 同步数据
await trySyncSettingAndRules(true); await trySyncSettingAndRules();
// 清除缓存 // 清除缓存
const setting = await getSettingWithDefault(); const setting = await getSettingWithDefault();
@@ -38,7 +44,7 @@ browser.runtime.onStartup.addListener(async () => {
} }
// 同步订阅规则 // 同步订阅规则
trySyncAllSubRules(setting, true); trySyncAllSubRules(setting);
}); });
/** /**
@@ -66,6 +72,12 @@ browser.runtime.onMessage.addListener(
fetchPool.clear(); fetchPool.clear();
sendResponse({ data: "ok" }); sendResponse({ data: "ok" });
break; break;
case MSG_OPEN_OPTIONS:
browser.runtime.openOptionsPage();
break;
case MSG_SAVE_RULE:
saveRule(args);
break;
default: default:
sendResponse({ error: `message action is unavailable: ${action}` }); sendResponse({ error: `message action is unavailable: ${action}` });
} }
@@ -85,6 +97,9 @@ browser.commands.onCommand.addListener((command) => {
case CMD_TOGGLE_STYLE: case CMD_TOGGLE_STYLE:
sendTabMsg(MSG_TRANS_TOGGLE_STYLE); sendTabMsg(MSG_TRANS_TOGGLE_STYLE);
break; break;
case CMD_OPEN_OPTIONS:
browser.runtime.openOptionsPage();
break;
default: default:
} }
}); });

129
src/common.js Normal file
View File

@@ -0,0 +1,129 @@
import React from "react";
import ReactDOM from "react-dom/client";
import Action from "./views/Action";
import createCache from "@emotion/cache";
import { CacheProvider } from "@emotion/react";
import {
MSG_TRANS_TOGGLE,
MSG_TRANS_TOGGLE_STYLE,
MSG_TRANS_GETRULE,
MSG_TRANS_PUTRULE,
APP_LCNAME,
DEFAULT_TRANBOX_SETTING,
} from "./config";
import { getRulesWithDefault, getFabWithDefault } from "./libs/storage";
import { Translator } from "./libs/translator";
import { sendIframeMsg, sendParentMsg } from "./libs/iframe";
import { matchRule } from "./libs/rules";
import Slection from "./views/Selection";
export async function runTranslator(setting) {
const href = document.location.href;
const rules = await getRulesWithDefault();
const rule = await matchRule(rules, href, setting);
const translator = new Translator(rule, setting);
return { translator, rule };
}
export function runIframe(setting) {
let translator;
window.addEventListener("message", (e) => {
const { action, args } = e.data || {};
switch (action) {
case MSG_TRANS_TOGGLE:
translator?.toggle();
break;
case MSG_TRANS_TOGGLE_STYLE:
translator?.toggleStyle();
break;
case MSG_TRANS_PUTRULE:
if (!translator) {
translator = new Translator(args, setting);
} else {
translator.updateRule(args || {});
}
break;
default:
}
});
sendParentMsg(MSG_TRANS_GETRULE);
}
export async function showFab(translator) {
const fab = await getFabWithDefault();
if (fab.isHide) {
return;
}
const $action = document.createElement("div");
$action.setAttribute("id", APP_LCNAME);
document.body.parentElement.appendChild($action);
const shadowContainer = $action.attachShadow({ mode: "closed" });
const emotionRoot = document.createElement("style");
const shadowRootElement = document.createElement("div");
shadowContainer.appendChild(emotionRoot);
shadowContainer.appendChild(shadowRootElement);
const cache = createCache({
key: APP_LCNAME,
prepend: true,
container: emotionRoot,
});
ReactDOM.createRoot(shadowRootElement).render(
<React.StrictMode>
<CacheProvider value={cache}>
<Action translator={translator} fab={fab} />
</CacheProvider>
</React.StrictMode>
);
}
export function showTransbox({
tranboxSetting = DEFAULT_TRANBOX_SETTING,
transApis,
}) {
if (!tranboxSetting?.transOpen) {
return;
}
const $tranbox = document.createElement("div");
$tranbox.setAttribute("id", "kiss-transbox");
document.body.parentElement.appendChild($tranbox);
const shadowContainer = $tranbox.attachShadow({ mode: "closed" });
const emotionRoot = document.createElement("style");
const shadowRootElement = document.createElement("div");
shadowContainer.appendChild(emotionRoot);
shadowContainer.appendChild(shadowRootElement);
const cache = createCache({
key: "kiss-transbox",
prepend: true,
container: emotionRoot,
});
ReactDOM.createRoot(shadowRootElement).render(
<React.StrictMode>
<CacheProvider value={cache}>
<Slection tranboxSetting={tranboxSetting} transApis={transApis} />
</CacheProvider>
</React.StrictMode>
);
}
export function windowListener(rule) {
window.addEventListener("message", (e) => {
const { action } = e.data || {};
switch (action) {
case MSG_TRANS_GETRULE:
sendIframeMsg(MSG_TRANS_PUTRULE, rule);
break;
default:
}
});
}
export function showErr(err) {
console.error("[KISS-Translator]", err);
const $err = document.createElement("div");
$err.innerText = `KISS-Translator: ${err.message}`;
$err.style.cssText = "background:red; color:#fff;";
document.body.prepend($err);
}

View File

@@ -3,6 +3,101 @@ export const UI_LANGS = [
["zh", "中文"], ["zh", "中文"],
]; ];
const customApiLangs = `["en", "English - English"],
["zh-CN", "Simplified Chinese - 简体中文"],
["zh-TW", "Traditional Chinese - 繁體中文"],
["ar", "Arabic - العربية"],
["bg", "Bulgarian - Български"],
["ca", "Catalan - Català"],
["hr", "Croatian - Hrvatski"],
["cs", "Czech - Čeština"],
["da", "Danish - Dansk"],
["nl", "Dutch - Nederlands"],
["fi", "Finnish - Suomi"],
["fr", "French - Français"],
["de", "German - Deutsch"],
["el", "Greek - Ελληνικά"],
["hi", "Hindi - हिन्दी"],
["hu", "Hungarian - Magyar"],
["id", "Indonesian - Indonesia"],
["it", "Italian - Italiano"],
["ja", "Japanese - 日本語"],
["ko", "Korean - 한국어"],
["ms", "Malay - Melayu"],
["mt", "Maltese - Malti"],
["nb", "Norwegian - Norsk Bokmål"],
["pl", "Polish - Polski"],
["pt", "Portuguese - Português"],
["ro", "Romanian - Română"],
["ru", "Russian - Русский"],
["sk", "Slovak - Slovenčina"],
["sl", "Slovenian - Slovenščina"],
["es", "Spanish - Español"],
["sv", "Swedish - Svenska"],
["ta", "Tamil - தமிழ்"],
["te", "Telugu - తెలుగు"],
["th", "Thai - ไทย"],
["tr", "Turkish - Türkçe"],
["uk", "Ukrainian - Українська"],
["vi", "Vietnamese - Tiếng Việt"],
`;
const customApiHelpZH = `/// 自定义翻译源接口说明
// 请求Request数据将按下面规范发送
{
url: {{YOUR_URL}},
method: "POST",
headers: {
"Content-type": "application/json",
"Authorization": "Bearer {{YOUR_KEY}}",
},
body: {
text: "", // 需要翻译的文字
from: "", // 源语言,可能为空,表示需要接口自动识别语言
to: "", // 目标语言
}
}
// 返回Response数据需符合下面的JSON规范
{
text: "", // 翻译后的文字
from: "", // 识别的源语言
to: "", // 目标语言(可选)
}
// 支持的语言代码如下
${customApiLangs}
`;
const customApiHelpEN = `/// Custom translation source interface description
// Request data will be sent according to the following specifications
{
url: {{YOUR_URL}},
method: "POST",
headers: {
"Content-type": "application/json",
"Authorization": "Bearer {{YOUR_KEY}}",
},
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
{
text: "", // translated text
from: "", // Recognized source language
to: "", // Target language (optional)
}
// The supported language codes are as follows
${customApiLangs}
`;
export const I18N = { export const I18N = {
app_name: { app_name: {
zh: `简约翻译`, zh: `简约翻译`,
@@ -12,6 +107,10 @@ export const I18N = {
zh: `翻译`, zh: `翻译`,
en: `Translate`, en: `Translate`,
}, },
custom_api_help: {
zh: customApiHelpZH,
en: customApiHelpEN,
},
translate_alt: { translate_alt: {
zh: `翻译 (Alt+Q)`, zh: `翻译 (Alt+Q)`,
en: `Translate (Alt+Q)`, en: `Translate (Alt+Q)`,
@@ -24,10 +123,26 @@ export const I18N = {
zh: `规则设置`, zh: `规则设置`,
en: `Rules Setting`, en: `Rules Setting`,
}, },
apis_setting: {
zh: `接口设置`,
en: `Apis Setting`,
},
sync_setting: { sync_setting: {
zh: `同步设置`, zh: `同步设置`,
en: `Sync Setting`, en: `Sync Setting`,
}, },
patch_setting: {
zh: `补丁设置`,
en: `Patch Setting`,
},
patch_setting_help: {
zh: `针对一些特殊网站的修正脚本,以便翻译软件得到更好的展示效果。`,
en: `Corrected scripts for some special websites so that the translation software can get better display results.`,
},
inject_webfix: {
zh: `注入修复补丁`,
en: `Inject Webfix`,
},
about: { about: {
zh: `关于`, zh: `关于`,
en: `About`, en: `About`,
@@ -60,10 +175,38 @@ export const I18N = {
zh: `最大翻译长度 (100-10000)`, zh: `最大翻译长度 (100-10000)`,
en: `Max Translate Length (100-10000)`, en: `Max Translate Length (100-10000)`,
}, },
num_of_newline_characters: {
zh: `换行字符数 (1-1000)`,
en: `Number of Newline Characters (1-1000)`,
},
translate_service: { translate_service: {
zh: `翻译服务`, zh: `翻译服务`,
en: `Translate Service`, en: `Translate Service`,
}, },
mouseover_translation: {
zh: `鼠标悬停翻译`,
en: `Mouseover translation`,
},
mk_disable: {
zh: `禁用`,
en: `Disable`,
},
mk_mouseover: {
zh: `鼠标悬停`,
en: `Mouseover`,
},
mk_ctrlKey: {
zh: `Control + 鼠标悬停`,
en: `Control + Mouseover`,
},
mk_shiftKey: {
zh: `Shift + 鼠标悬停`,
en: `Shift + Mouseover`,
},
mk_altKey: {
zh: `Alt + 鼠标悬停`,
en: `Alt + Mouseover`,
},
from_lang: { from_lang: {
zh: `原文语言`, zh: `原文语言`,
en: `Source Language`, en: `Source Language`,
@@ -84,6 +227,10 @@ export const I18N = {
zh: `样式颜色`, zh: `样式颜色`,
en: `Style Color`, en: `Style Color`,
}, },
remain_unchanged: {
zh: `保留不变`,
en: `Remain Unchanged`,
},
google_api: { google_api: {
zh: `谷歌翻译接口`, zh: `谷歌翻译接口`,
en: `Google Translate API`, en: `Google Translate API`,
@@ -126,11 +273,15 @@ export const I18N = {
}, },
personal_rules: { personal_rules: {
zh: `个人规则`, zh: `个人规则`,
en: `Personal Rules`, en: `Rules`,
}, },
subscribe_rules: { subscribe_rules: {
zh: `订阅规则`, zh: `订阅规则`,
en: `Subscribe Rules`, en: `Subscribe`,
},
overwrite_subscribe_rules: {
zh: `覆写订阅规则`,
en: `Overwrite`,
}, },
subscribe_url: { subscribe_url: {
zh: `订阅地址`, zh: `订阅地址`,
@@ -152,6 +303,10 @@ export const I18N = {
zh: `查看关于数据同步接口部署`, zh: `查看关于数据同步接口部署`,
en: `View About Data Synchronization Interface Deployment`, en: `View About Data Synchronization Interface Deployment`,
}, },
about_api_proxy: {
zh: `查看自建一个翻译接口代理`,
en: `Check out the self-built translation interface proxy`,
},
style_none: { style_none: {
zh: ``, zh: ``,
en: `None`, en: `None`,
@@ -180,9 +335,17 @@ export const I18N = {
zh: `高亮`, zh: `高亮`,
en: `Highlight`, en: `Highlight`,
}, },
diy_style: {
zh: `自定义样式`,
en: `Custom Style`,
},
diy_style_helper: {
zh: `遵循“CSS”的语法`,
en: `Follow the syntax of "CSS"`,
},
setting: { setting: {
zh: `设置`, zh: `设置 (Alt+O)`,
en: `Setting`, en: `Setting (Alt+O)`,
}, },
pattern: { pattern: {
zh: `匹配网址`, zh: `匹配网址`,
@@ -193,8 +356,8 @@ export const I18N = {
en: `1. The asterisk (*) wildcard is supported. 2. Multiple URLs can be separated by English commas ",".`, en: `1. The asterisk (*) wildcard is supported. 2. Multiple URLs can be separated by English commas ",".`,
}, },
selector_helper: { selector_helper: {
zh: `1、遵循CSS选择器规则。2、留空表示采用全局设置。3、多个CSS选择器之间用“;”隔开。4、“shadow root”选择器和内部选择器用“>>>”隔开。`, zh: `1、遵循CSS选择器语法。2、留空表示采用全局设置。3、多个CSS选择器之间用“;”隔开。4、“shadow root”选择器和内部选择器用“>>>”隔开。`,
en: `1. Follow the CSS selector rules. 2. Leave blank to adopt the global setting. 3. Separate multiple CSS selectors with ";". 4. The "shadow root" selector and the internal selector are separated by ">>>".`, 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 ">>>".`,
}, },
translate_switch: { translate_switch: {
zh: `开启翻译`, zh: `开启翻译`,
@@ -236,6 +399,14 @@ export const I18N = {
zh: `请检查url地址是否正确或稍后再试。`, zh: `请检查url地址是否正确或稍后再试。`,
en: `Please check if the url address is correct or try again later.`, en: `Please check if the url address is correct or try again later.`,
}, },
deepl_api: {
zh: `DeepL 接口`,
en: `DeepL API`,
},
deepl_key: {
zh: `DeepL 密钥`,
en: `DeepL Key`,
},
openai_api: { openai_api: {
zh: `OpenAI 接口`, zh: `OpenAI 接口`,
en: `OpenAI API`, en: `OpenAI API`,
@@ -252,7 +423,7 @@ export const I18N = {
zh: `OpenAI 提示词`, zh: `OpenAI 提示词`,
en: `OpenAI Prompt`, en: `OpenAI Prompt`,
}, },
clear_cache: { if_clear_cache: {
zh: `是否清除缓存`, zh: `是否清除缓存`,
en: `Whether clear cache`, en: `Whether clear cache`,
}, },
@@ -264,32 +435,212 @@ export const I18N = {
zh: `重启浏览器时清除缓存`, zh: `重启浏览器时清除缓存`,
en: `Clear cache when restarting browser`, en: `Clear cache when restarting browser`,
}, },
data_sync_type: {
zh: `数据同步方式`,
en: `Data Sync Type`,
},
data_sync_url: { data_sync_url: {
zh: `数据同步接口`, zh: `数据同步接口`,
en: `Data Sync API`, en: `Data Sync API`,
}, },
data_sync_user: {
zh: `数据同步账户`,
en: `Data Sync User`,
},
data_sync_key: { data_sync_key: {
zh: `数据同步密钥`, zh: `数据同步密钥`,
en: `Data Sync Key`, en: `Data Sync Key`,
}, },
data_sync_test: { sync_now: {
zh: `数据同步测试`, zh: `立即同步`,
en: `Data Sync Test`, en: `Sync Now`,
}, },
data_sync_success: { sync_success: {
zh: `数据同步成功!`, zh: `同步成功!`,
en: `Data Sync Success`, en: `Sync Success`,
}, },
data_sync_error: { sync_failed: {
zh: `数据同步失败!`, zh: `同步失败!`,
en: `Data Sync Error`, en: `Sync Error`,
}, },
error_got_some_wrong: { error_got_some_wrong: {
zh: `抱歉,出错了!`, zh: `抱歉,出错了!`,
en: `Sorry, something went wrong!`, en: `Sorry, something went wrong!`,
}, },
error_sync_setting: { error_sync_setting: {
zh: `您的同步设置未填写,无法在线分享。`, zh: `您的同步类型必须为“KISS-Worker”且需填写完整`,
en: `Your sync settings are missing and cannot be shared online.`, en: `Your sync type must be "KISS-Worker" and must be filled in completely`,
},
click_test: {
zh: `点击测试`,
en: `Click Test`,
},
test_success: {
zh: `测试成功`,
en: `Test success`,
},
test_failed: {
zh: `测试失败`,
en: `Test failed`,
},
clear_all_cache_now: {
zh: `立即清除全部缓存`,
en: `Clear all cache now`,
},
clear_cache: {
zh: `清除缓存`,
en: `Clear Cache`,
},
clear_success: {
zh: `清除成功`,
en: `Clear success`,
},
clear_failed: {
zh: `清除失败`,
en: `Clear failed`,
},
share: {
zh: `分享`,
en: `Share`,
},
clear_all: {
zh: `清空`,
en: `Clear All`,
},
help: {
zh: `求助`,
en: `Help`,
},
restore_default: {
zh: `恢复默认`,
en: `Restore Default`,
},
shortcuts_setting: {
zh: `快捷键设置`,
en: `Shortcuts Setting`,
},
toggle_translate_shortcut: {
zh: `"开启翻译"快捷键`,
en: `"Toggle Translate" Shortcut`,
},
toggle_style_shortcut: {
zh: `"切换样式"快捷键`,
en: `"Toggle Style" Shortcut`,
},
toggle_popup_shortcut: {
zh: `"打开弹窗"快捷键`,
en: `"Open Popup" Shortcut`,
},
open_setting_shortcut: {
zh: `"打开设置"快捷键`,
en: `"Open Setting" Shortcut`,
},
hide_fab_button: {
zh: `隐藏悬浮按钮`,
en: `Hide Fab Button`,
},
show: {
zh: `显示`,
en: `Show`,
},
hide: {
zh: `隐藏`,
en: `Hide`,
},
save_rule: {
zh: `保存规则`,
en: `Save Rule`,
},
global_rule: {
zh: `全局规则`,
en: `Global Rule`,
},
input_translate: {
zh: `输入框翻译`,
en: `Input Box Translation`,
},
use_input_box_translation: {
zh: `启用输入框翻译`,
en: `Input Box Translation`,
},
input_selector: {
zh: `输入框选择器`,
en: `Input Selector`,
},
input_selector_helper: {
zh: `用于输入框翻译。`,
en: `Used for input box translation.`,
},
trigger_trans_shortcut: {
zh: `触发翻译快捷键`,
en: `Trigger Translation Shortcut Keys`,
},
trigger_trans_shortcut_help: {
zh: `默认为单击“AltLeft+KeyI”`,
en: `Default is "AltLeft+KeyI"`,
},
shortcut_press_count: {
zh: `快捷键连击次数`,
en: `Shortcut Press Number`,
},
combo_timeout: {
zh: `连击超时时间 (10-1000ms)`,
en: `Combo Timeout (10-1000ms)`,
},
input_trans_start_sign: {
zh: `翻译起始标识`,
en: `Translation Start Sign`,
},
input_trans_start_sign_help: {
zh: `标识后面可以加目标语言代码,如: “/en 你好”、“/zh hello”`,
en: `The target language code can be added after the sign, such as: "/en 你好", "/zh hello"`,
},
detect_lang_remote: {
zh: `远程语言检测`,
en: `Remote language detection`,
},
detect_lang_remote_help: {
zh: `启用后检测准确度增加,但会降低翻译速度,请酌情开启`,
en: `After enabling, the detection accuracy will increase, but it will reduce the translation speed. Please enable it as appropriate.`,
},
disable: {
zh: `禁用`,
en: `Disable`,
},
enable: {
zh: `启用`,
en: `Enable`,
},
selection_translate: {
zh: `划词翻译`,
en: `Selection Translate`,
},
toggle_selection_translate: {
zh: `启用划词翻译`,
en: `Use Selection Translate`,
},
trigger_tranbox_shortcut: {
zh: `显示翻译框快捷键`,
en: `Toggle Translate Box Shortcut`,
},
tranbtn_offset_x: {
zh: `翻译按钮偏移X0-100`,
en: `Translate Button Offset X (0-100)`,
},
tranbtn_offset_y: {
zh: `翻译按钮偏移Y0-100`,
en: `Translate Button Offset Y (0-100)`,
},
translated_text: {
zh: `译文`,
en: `Translated Text`,
},
original_text: {
zh: `原文`,
en: `Original Text`,
},
favorite_words: {
zh: `收藏词汇`,
en: `Favorite Words`,
}, },
}; };

View File

@@ -1,23 +1,37 @@
import { import {
DEFAULT_SELECTOR, DEFAULT_SELECTOR,
GLOBAL_KEY, GLOBAL_KEY,
REMAIN_KEY,
SHADOW_KEY, SHADOW_KEY,
DEFAULT_RULE, DEFAULT_RULE,
DEFAULT_OW_RULE,
BUILTIN_RULES, BUILTIN_RULES,
} from "./rules"; } from "./rules";
import { APP_NAME, APP_LCNAME } from "./app"; import { APP_NAME, APP_LCNAME } from "./app";
export { I18N, UI_LANGS } from "./i18n"; export { I18N, UI_LANGS } from "./i18n";
export { GLOBAL_KEY, SHADOW_KEY, DEFAULT_RULE, BUILTIN_RULES, APP_LCNAME }; export {
GLOBAL_KEY,
REMAIN_KEY,
SHADOW_KEY,
DEFAULT_RULE,
DEFAULT_OW_RULE,
BUILTIN_RULES,
APP_LCNAME,
};
export const STOKEY_MSAUTH = `${APP_NAME}_msauth`; export const STOKEY_MSAUTH = `${APP_NAME}_msauth`;
export const STOKEY_BDAUTH = `${APP_NAME}_bdauth`;
export const STOKEY_SETTING = `${APP_NAME}_setting`; export const STOKEY_SETTING = `${APP_NAME}_setting`;
export const STOKEY_RULES = `${APP_NAME}_rules`; export const STOKEY_RULES = `${APP_NAME}_rules`;
export const STOKEY_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 CLIENT_WEB = "web"; export const CLIENT_WEB = "web";
export const CLIENT_CHROME = "chrome"; export const CLIENT_CHROME = "chrome";
@@ -26,9 +40,10 @@ 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_EXTS = [CLIENT_CHROME, CLIENT_EDGE, CLIENT_FIREFOX];
export const KV_RULES_KEY = "KT_RULES"; export const KV_RULES_KEY = "kiss-rules.json";
export const KV_RULES_SHARE_KEY = "KT_RULES_SHARE"; export const KV_WORDS_KEY = "kiss-words.json";
export const KV_SETTING_KEY = "KT_SETTING"; export const KV_RULES_SHARE_KEY = "kiss-rules-share.json";
export const KV_SETTING_KEY = "kiss-setting.json";
export const KV_SALT_SYNC = "KISS-Translator-SYNC"; export const KV_SALT_SYNC = "KISS-Translator-SYNC";
export const KV_SALT_SHARE = "KISS-Translator-SHARE"; export const KV_SALT_SHARE = "KISS-Translator-SHARE";
@@ -37,31 +52,56 @@ 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_FETCH_LIMIT = "fetch_limit";
export const MSG_FETCH_CLEAR = "fetch_clear"; export const MSG_FETCH_CLEAR = "fetch_clear";
export const MSG_OPEN_OPTIONS = "open_options";
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_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 EVENT_KISS = "kissEvent";
export const THEME_LIGHT = "light"; export const THEME_LIGHT = "light";
export const THEME_DARK = "dark"; export const THEME_DARK = "dark";
export const URL_KISS_WORKER = "https://github.com/fishjar/kiss-worker"; export const URL_KISS_WORKER = "https://github.com/fishjar/kiss-worker";
export const URL_KISS_PROXY = "https://github.com/fishjar/kiss-proxy";
export const URL_KISS_RULES = "https://github.com/fishjar/kiss-rules";
export const URL_KISS_RULES_NEW_ISSUE =
"https://github.com/fishjar/kiss-rules/issues/new";
export const URL_RAW_PREFIX = export const URL_RAW_PREFIX =
"https://raw.githubusercontent.com/fishjar/kiss-translator/master"; "https://raw.githubusercontent.com/fishjar/kiss-translator/master";
export const URL_MICROSOFT_AUTH = "https://edge.microsoft.com/translate/auth";
export const URL_MICROSOFT_TRANS = export const URL_CACHE_TRAN = `https://${APP_LCNAME}/translate`;
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_BAIDU_LANGDETECT = "https://fanyi.baidu.com/langdetect";
export const URL_BAIDU_WEB = "https://fanyi.baidu.com/";
export const URL_BAIDU_TRAN = "https://fanyi.baidu.com/v2transapi";
export const URL_DEEPLFREE_TRAN = "https://www2.deepl.com/jsonrpc";
export const URL_TENCENT_TRANSMART = "https://transmart.qq.com/api/imt";
export const OPT_TRANS_GOOGLE = "Google"; export const OPT_TRANS_GOOGLE = "Google";
export const OPT_TRANS_MICROSOFT = "Microsoft"; export const OPT_TRANS_MICROSOFT = "Microsoft";
export const OPT_TRANS_DEEPL = "DeepL";
export const OPT_TRANS_DEEPLX = "DeepLX";
export const OPT_TRANS_DEEPLFREE = "DeepLFree";
export const OPT_TRANS_BAIDU = "Baidu";
export const OPT_TRANS_TENCENT = "Tencent";
export const OPT_TRANS_OPENAI = "OpenAI"; export const OPT_TRANS_OPENAI = "OpenAI";
export const OPT_TRANS_CLOUDFLAREAI = "CloudflareAI";
export const OPT_TRANS_CUSTOMIZE = "Custom";
export const OPT_TRANS_ALL = [ export const OPT_TRANS_ALL = [
OPT_TRANS_GOOGLE, OPT_TRANS_GOOGLE,
OPT_TRANS_MICROSOFT, OPT_TRANS_MICROSOFT,
OPT_TRANS_DEEPL,
OPT_TRANS_DEEPLFREE,
OPT_TRANS_DEEPLX,
OPT_TRANS_BAIDU,
OPT_TRANS_TENCENT,
OPT_TRANS_OPENAI, OPT_TRANS_OPENAI,
OPT_TRANS_CLOUDFLAREAI,
OPT_TRANS_CUSTOMIZE,
]; ];
export const OPT_LANGS_TO = [ export const OPT_LANGS_TO = [
@@ -105,15 +145,116 @@ 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_MICROSOFT]: new Map([ [OPT_TRANS_MICROSOFT]: new Map([
...OPT_LANGS_FROM.map(([key]) => [key, key]),
["auto", ""], ["auto", ""],
["zh-CN", "zh-Hans"], ["zh-CN", "zh-Hans"],
["zh-TW", "zh-Hant"], ["zh-TW", "zh-Hant"],
]), ]),
[OPT_TRANS_DEEPL]: new Map([
...OPT_LANGS_FROM.map(([key]) => [key, key.toUpperCase()]),
["auto", ""],
["zh-CN", "ZH"],
["zh-TW", "ZH"],
]),
[OPT_TRANS_DEEPLFREE]: new Map([
...OPT_LANGS_FROM.map(([key]) => [key, key.toUpperCase()]),
["auto", "auto"],
["zh-CN", "ZH"],
["zh-TW", "ZH"],
]),
[OPT_TRANS_DEEPLX]: new Map([
...OPT_LANGS_FROM.map(([key]) => [key, key.toUpperCase()]),
["auto", ""],
["zh-CN", "ZH"],
["zh-TW", "ZH"],
]),
[OPT_TRANS_BAIDU]: new Map([
...OPT_LANGS_FROM.map(([key]) => [key, key]),
["zh-CN", "zh"],
["zh-TW", "cht"],
["ar", "ara"],
["bg", "bul"],
["ca", "cat"],
["hr", "hrv"],
["da", "dan"],
["fi", "fin"],
["fr", "fra"],
["hi", "mai"],
["ja", "jp"],
["ko", "kor"],
["ms", "may"],
["mt", "mlt"],
["nb", "nor"],
["ro", "rom"],
["ru", "ru"],
["sl", "slo"],
["es", "spa"],
["sv", "swe"],
["ta", "tam"],
["te", "tel"],
["uk", "ukr"],
["vi", "vie"],
]),
[OPT_TRANS_TENCENT]: new Map([
["auto", "auto"],
["zh-CN", "zh"],
["zh-TW", "zh"],
["en", "en"],
["ar", "ar"],
["de", "de"],
["ru", "ru"],
["fr", "fr"],
["fi", "fil"],
["ko", "ko"],
["ms", "ms"],
["pt", "pt"],
["ja", "ja"],
["th", "th"],
["tr", "tr"],
["es", "es"],
["it", "it"],
["hi", "hi"],
["id", "id"],
["vi", "vi"],
]),
[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_CLOUDFLAREAI]: new Map([
["auto", ""],
["zh-CN", "chinese"],
["zh-TW", "chinese"],
["en", "english"],
["ar", "arabic"],
["de", "german"],
["ru", "russian"],
["fr", "french"],
["pt", "portuguese"],
["ja", "japanese"],
["es", "spanish"],
["hi", "hindi"],
]),
[OPT_TRANS_CUSTOMIZE]: 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_BAIDU = new Map(
Array.from(OPT_LANGS_SPECIAL[OPT_TRANS_BAIDU].entries()).map(([k, v]) => [
v,
k,
])
);
export const OPT_LANGS_TENCENT = new Map(
Array.from(OPT_LANGS_SPECIAL[OPT_TRANS_TENCENT].entries()).map(([k, v]) => [
v,
k,
])
);
OPT_LANGS_TENCENT.set("zh", "zh-CN");
export const OPT_STYLE_NONE = "style_none"; // 无 export const OPT_STYLE_NONE = "style_none"; // 无
export const OPT_STYLE_LINE = "under_line"; // 下划线 export const OPT_STYLE_LINE = "under_line"; // 下划线
@@ -121,7 +262,8 @@ export const OPT_STYLE_DOTLINE = "dot_line"; // 点状线
export const OPT_STYLE_DASHLINE = "dash_line"; // 虚线 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_HIGHTLIGHT = "highlight"; // 高亮 export const OPT_STYLE_HIGHLIGHT = "highlight"; // 高亮
export const OPT_STYLE_DIY = "diy_style"; // 自定义样式
export const OPT_STYLE_ALL = [ export const OPT_STYLE_ALL = [
OPT_STYLE_NONE, OPT_STYLE_NONE,
OPT_STYLE_LINE, OPT_STYLE_LINE,
@@ -129,7 +271,28 @@ export const OPT_STYLE_ALL = [
OPT_STYLE_DASHLINE, OPT_STYLE_DASHLINE,
OPT_STYLE_WAVYLINE, OPT_STYLE_WAVYLINE,
OPT_STYLE_FUZZY, OPT_STYLE_FUZZY,
OPT_STYLE_HIGHTLIGHT, OPT_STYLE_HIGHLIGHT,
OPT_STYLE_DIY,
];
export const OPT_STYLE_USE_COLOR = [
OPT_STYLE_LINE,
OPT_STYLE_DOTLINE,
OPT_STYLE_DASHLINE,
OPT_STYLE_WAVYLINE,
OPT_STYLE_HIGHLIGHT,
];
export const OPT_MOUSEKEY_DISABLE = "mk_disable";
export const OPT_MOUSEKEY_MOUSEOVER = "mk_mouseover";
export const OPT_MOUSEKEY_CONTROL = "mk_ctrlKey";
export const OPT_MOUSEKEY_SHIFT = "mk_shiftKey";
export const OPT_MOUSEKEY_ALT = "mk_altKey";
export const OPT_MOUSEKEY_ALL = [
OPT_MOUSEKEY_DISABLE,
OPT_MOUSEKEY_MOUSEOVER,
OPT_MOUSEKEY_CONTROL,
OPT_MOUSEKEY_SHIFT,
OPT_MOUSEKEY_ALT,
]; ];
export const DEFAULT_FETCH_LIMIT = 10; // 默认最大任务数量 export const DEFAULT_FETCH_LIMIT = 10; // 默认最大任务数量
@@ -150,22 +313,96 @@ export const GLOBLA_RULE = {
textStyle: OPT_STYLE_DASHLINE, textStyle: OPT_STYLE_DASHLINE,
transOpen: "false", transOpen: "false",
bgColor: "", bgColor: "",
textDiyStyle: "",
};
// 输入框翻译
export const OPT_INPUT_TRANS_SIGNS = ["/", "//", "\\", "\\\\", ">", ">>"];
export const DEFAULT_INPUT_SHORTCUT = ["AltLeft", "KeyI"];
export const DEFAULT_INPUT_RULE = {
transOpen: true,
translator: OPT_TRANS_MICROSOFT,
fromLang: "auto",
toLang: "en",
triggerShortcut: DEFAULT_INPUT_SHORTCUT,
triggerCount: 1,
triggerTime: 200,
transSign: OPT_INPUT_TRANS_SIGNS[0],
};
// 划词翻译
export const DEFAULT_TRANBOX_SHORTCUT = ["AltLeft", "KeyB"];
export const DEFAULT_TRANBOX_SETTING = {
transOpen: true,
translator: OPT_TRANS_MICROSOFT,
fromLang: "auto",
toLang: "zh-CN",
tranboxShortcut: DEFAULT_TRANBOX_SHORTCUT,
btnOffsetX: 10,
btnOffsetY: 10,
}; };
// 订阅列表 // 订阅列表
export const DEFAULT_SUBRULES_LIST = [ export const DEFAULT_SUBRULES_LIST = [
{ {
url: process.env.REACT_APP_RULESURL, url: process.env.REACT_APP_RULESURL,
selected: false,
},
{
url: process.env.REACT_APP_RULESURL_ON,
selected: true, selected: true,
}, },
{ {
url: process.env.REACT_APP_RULESURL2, url: process.env.REACT_APP_RULESURL_OFF,
selected: false, selected: false,
}, },
]; ];
// 翻译接口
export const DEFAULT_TRANS_APIS = {
[OPT_TRANS_GOOGLE]: {
url: "https://translate.googleapis.com/translate_a/single",
key: "",
},
[OPT_TRANS_DEEPL]: {
url: "https://api-free.deepl.com/v2/translate",
key: "",
},
[OPT_TRANS_DEEPLX]: {
url: "http://localhost:1188/translate",
key: "",
},
[OPT_TRANS_OPENAI]: {
url: "https://api.openai.com/v1/chat/completions",
key: "",
model: "gpt-4",
prompt: `You will be provided with a sentence in ${PROMPT_PLACE_FROM}, and your task is to translate it into ${PROMPT_PLACE_TO}.`,
},
[OPT_TRANS_CLOUDFLAREAI]: {
url: "https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/ai/run/@cf/meta/m2m100-1.2b",
key: "",
},
[OPT_TRANS_CUSTOMIZE]: {
url: "",
key: "",
},
};
// 默认快捷键
export const OPT_SHORTCUT_TRANSLATE = "toggleTranslate";
export const OPT_SHORTCUT_STYLE = "toggleStyle";
export const OPT_SHORTCUT_POPUP = "togglePopup";
export const OPT_SHORTCUT_SETTING = "openSetting";
export const DEFAULT_SHORTCUTS = {
[OPT_SHORTCUT_TRANSLATE]: ["AltLeft", "KeyQ"],
[OPT_SHORTCUT_STYLE]: ["AltLeft", "KeyC"],
[OPT_SHORTCUT_POPUP]: ["AltLeft", "KeyK"],
[OPT_SHORTCUT_SETTING]: ["AltLeft", "KeyN"],
};
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 DEFAULT_SETTING = { export const DEFAULT_SETTING = {
darkMode: false, // 深色模式 darkMode: false, // 深色模式
@@ -174,24 +411,32 @@ export const DEFAULT_SETTING = {
fetchInterval: DEFAULT_FETCH_INTERVAL, // 任务间隔时间 fetchInterval: DEFAULT_FETCH_INTERVAL, // 任务间隔时间
minLength: TRANS_MIN_LENGTH, minLength: TRANS_MIN_LENGTH,
maxLength: TRANS_MAX_LENGTH, maxLength: TRANS_MAX_LENGTH,
newlineLength: TRANS_NEWLINE_LENGTH,
clearCache: false, // 是否在浏览器下次启动时清除缓存 clearCache: false, // 是否在浏览器下次启动时清除缓存
injectRules: true, // 是否注入订阅规则 injectRules: true, // 是否注入订阅规则
injectWebfix: true, // 是否注入修复补丁
detectRemote: false, // 是否使用远程语言检测
subrulesList: DEFAULT_SUBRULES_LIST, // 订阅列表 subrulesList: DEFAULT_SUBRULES_LIST, // 订阅列表
googleUrl: "https://translate.googleapis.com/translate_a/single", // 谷歌翻译接口 owSubrule: DEFAULT_OW_RULE, // 覆写订阅规则
openaiUrl: "https://api.openai.com/v1/chat/completions", transApis: DEFAULT_TRANS_APIS, // 翻译接口
openaiKey: "", mouseKey: OPT_MOUSEKEY_DISABLE, // 鼠标悬停翻译
openaiModel: "gpt-4", shortcuts: DEFAULT_SHORTCUTS, // 快捷键
openaiPrompt: `You will be provided with a sentence in ${PROMPT_PLACE_FROM}, and your task is to translate it into ${PROMPT_PLACE_TO}.`, inputRule: DEFAULT_INPUT_RULE, // 输入框设置
tranboxSetting: DEFAULT_TRANBOX_SETTING, // 划词翻译设置
}; };
export const DEFAULT_RULES = [GLOBLA_RULE]; export const DEFAULT_RULES = [GLOBLA_RULE];
export const OPT_SYNCTYPE_WORKER = "KISS-Worker";
export const OPT_SYNCTYPE_WEBDAV = "WebDAV";
export const OPT_SYNCTYPE_ALL = [OPT_SYNCTYPE_WORKER, OPT_SYNCTYPE_WEBDAV];
export const DEFAULT_SYNC = { export const DEFAULT_SYNC = {
syncType: OPT_SYNCTYPE_WORKER, // 同步方式
syncUrl: "", // 数据同步接口 syncUrl: "", // 数据同步接口
syncUser: "", // 数据同步用户名
syncKey: "", // 数据同步密钥 syncKey: "", // 数据同步密钥
settingUpdateAt: 0, syncMeta: {}, // 数据更新及同步信息
settingSyncAt: 0,
rulesUpdateAt: 0,
rulesSyncAt: 0,
subRulesSyncAt: 0, // 订阅规则同步时间 subRulesSyncAt: 0, // 订阅规则同步时间
dataCaches: {}, // 缓存同步时间
}; };

View File

@@ -1,8 +1,9 @@
const els = `li, p, h1, h2, h3, h4, h5, h6, dd`; const els = `li, p, h1, h2, h3, h4, h5, h6, dd, blockquote`;
export const DEFAULT_SELECTOR = `:is(${els})`; export const DEFAULT_SELECTOR = `:is(${els})`;
export const GLOBAL_KEY = "*"; export const GLOBAL_KEY = "*";
export const REMAIN_KEY = "-";
export const SHADOW_KEY = ">>>"; export const SHADOW_KEY = ">>>";
@@ -15,6 +16,30 @@ export const DEFAULT_RULE = {
textStyle: GLOBAL_KEY, textStyle: GLOBAL_KEY,
transOpen: GLOBAL_KEY, transOpen: GLOBAL_KEY,
bgColor: "", bgColor: "",
textDiyStyle: "",
};
const DEFAULT_DIY_STYLE = `color: #666;
background: linear-gradient(
45deg,
LightGreen 20%,
LightPink 20% 40%,
LightSalmon 40% 60%,
LightSeaGreen 60% 80%,
LightSkyBlue 80%
);
&:hover {
color: #333;
};`;
export const DEFAULT_OW_RULE = {
translator: REMAIN_KEY,
fromLang: REMAIN_KEY,
toLang: REMAIN_KEY,
textStyle: REMAIN_KEY,
transOpen: REMAIN_KEY,
bgColor: "",
textDiyStyle: DEFAULT_DIY_STYLE,
}; };
const RULES = [ const RULES = [
@@ -152,7 +177,9 @@ const RULES = [
}, },
]; ];
export const BUILTIN_RULES = RULES.map((item) => ({ export const BUILTIN_RULES = RULES.sort((a, b) =>
a.pattern.localeCompare(b.pattern)
).map((item) => ({
...DEFAULT_RULE, ...DEFAULT_RULE,
...item, ...item,
transOpen: "true", transOpen: "true",

View File

@@ -5,49 +5,72 @@ import {
MSG_TRANS_GETRULE, MSG_TRANS_GETRULE,
MSG_TRANS_PUTRULE, MSG_TRANS_PUTRULE,
} from "./config"; } from "./config";
import { getSettingWithDefault, getRulesWithDefault } from "./libs/storage"; import { getSettingWithDefault } from "./libs/storage";
import { Translator } from "./libs/translator"; import { isIframe, sendIframeMsg } from "./libs/iframe";
import { isIframe } from "./libs/iframe"; import { runWebfix } from "./libs/webfix";
import { matchRule } from "./libs/rules"; import {
runIframe,
runTranslator,
showFab,
showTransbox,
windowListener,
showErr,
} from "./common";
/** function runtimeListener(translator) {
* 入口函数
*/
const init = async () => {
const href = isIframe ? document.referrer : document.location.href;
const setting = await getSettingWithDefault();
const rules = await getRulesWithDefault();
const rule = await matchRule(rules, href, setting);
const translator = new Translator(rule, setting);
// 监听消息
browser?.runtime.onMessage.addListener(async ({ action, args }) => { browser?.runtime.onMessage.addListener(async ({ action, args }) => {
switch (action) { switch (action) {
case MSG_TRANS_TOGGLE: case MSG_TRANS_TOGGLE:
translator.toggle(); translator.toggle();
sendIframeMsg(MSG_TRANS_TOGGLE);
break; break;
case MSG_TRANS_TOGGLE_STYLE: case MSG_TRANS_TOGGLE_STYLE:
translator.toggleStyle(); translator.toggleStyle();
sendIframeMsg(MSG_TRANS_TOGGLE_STYLE);
break; break;
case MSG_TRANS_GETRULE: case MSG_TRANS_GETRULE:
break; break;
case MSG_TRANS_PUTRULE: case MSG_TRANS_PUTRULE:
translator.updateRule(args); translator.updateRule(args);
sendIframeMsg(MSG_TRANS_PUTRULE, args);
break; break;
default: default:
return { error: `message action is unavailable: ${action}` }; return { error: `message action is unavailable: ${action}` };
} }
return { data: translator.rule }; return { data: translator.rule };
}); });
}; }
/**
* 入口函数
*/
(async () => { (async () => {
try { try {
await init(); // 读取设置信息
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) { } catch (err) {
const $err = document.createElement("div"); showErr(err);
$err.innerText = `KISS-Translator: ${err.message}`;
$err.style.cssText = "background:red; color:#fff; z-index:10000;";
document.body.prepend($err);
} }
})(); })();

24
src/hooks/Api.js Normal file
View File

@@ -0,0 +1,24 @@
import { useCallback } from "react";
import { DEFAULT_TRANS_APIS } from "../config";
import { useSetting } from "./Setting";
export function useApi(translator) {
const { setting, updateSetting } = useSetting();
const transApis = setting?.transApis || DEFAULT_TRANS_APIS;
const updateApi = useCallback(
async (obj) => {
const api = transApis[translator] || {};
Object.assign(transApis, { [translator]: { ...api, ...obj } });
await updateSetting({ transApis });
},
[translator, transApis, updateSetting]
);
const resetApi = useCallback(async () => {
Object.assign(transApis, { [translator]: DEFAULT_TRANS_APIS[translator] });
await updateSetting({ transApis });
}, [translator, transApis, updateSetting]);
return { api: transApis[translator] || {}, updateApi, resetApi };
}

11
src/hooks/Fab.js Normal file
View File

@@ -0,0 +1,11 @@
import { STOKEY_FAB } from "../config";
import { useStorage } from "./Storage";
/**
* fab hook
* @returns
*/
export function useFab() {
const { data, update } = useStorage(STOKEY_FAB);
return { fab: data, updateFab: update };
}

67
src/hooks/FavWords.js Normal file
View File

@@ -0,0 +1,67 @@
import { KV_WORDS_KEY } from "../config";
import { useCallback, useEffect, useState } from "react";
import { trySyncWords } from "../libs/sync";
import { getWordsWithDefault, setWords } from "../libs/storage";
import { useSyncMeta } from "./Sync";
export function useFavWords() {
const [loading, setLoading] = useState(false);
const [favWords, setFavWords] = useState({});
const { updateSyncMeta } = useSyncMeta();
const toggleFav = useCallback(
async (word) => {
const favs = { ...favWords };
if (favs[word]) {
delete favs[word];
} else {
favs[word] = { createdAt: Date.now() };
}
await setWords(favs);
await updateSyncMeta(KV_WORDS_KEY);
await trySyncWords();
setFavWords(favs);
},
[updateSyncMeta, favWords]
);
const mergeWords = useCallback(
async (newWords) => {
const favs = { ...favWords };
newWords.forEach((word) => {
if (!favs[word]) {
favs[word] = { createdAt: Date.now() };
}
});
await setWords(favs);
await updateSyncMeta(KV_WORDS_KEY);
await trySyncWords();
setFavWords(favs);
},
[updateSyncMeta, favWords]
);
const clearWords = useCallback(async () => {
await setWords({});
await updateSyncMeta(KV_WORDS_KEY);
await trySyncWords();
setFavWords({});
}, [updateSyncMeta]);
useEffect(() => {
(async () => {
try {
setLoading(true);
await trySyncWords();
const favWords = await getWordsWithDefault();
setFavWords(favWords);
} catch (err) {
console.log("[query fav]", err);
} finally {
setLoading(false);
}
})();
}, []);
return { loading, favWords, toggleFav, mergeWords, clearWords };
}

18
src/hooks/InputRule.js Normal file
View File

@@ -0,0 +1,18 @@
import { useCallback } from "react";
import { DEFAULT_INPUT_RULE } from "../config";
import { useSetting } from "./Setting";
export function useInputRule() {
const { setting, updateSetting } = useSetting();
const inputRule = setting?.inputRule || DEFAULT_INPUT_RULE;
const updateInputRule = useCallback(
async (obj) => {
Object.assign(inputRule, obj);
await updateSetting({ inputRule });
},
[inputRule, updateSetting]
);
return { inputRule, updateInputRule };
}

View File

@@ -1,9 +1,9 @@
import { STOKEY_RULES, DEFAULT_RULES } from "../config"; import { STOKEY_RULES, DEFAULT_RULES, KV_RULES_KEY } from "../config";
import { useStorage } from "./Storage"; import { useStorage } from "./Storage";
import { trySyncRules } from "../libs/sync"; import { trySyncRules } from "../libs/sync";
import { useSync } from "./Sync";
import { checkRules } from "../libs/rules"; import { checkRules } from "../libs/rules";
import { useCallback } from "react"; import { useCallback } from "react";
import { useSyncMeta } from "./Sync";
/** /**
* 规则 hook * 规则 hook
@@ -11,19 +11,15 @@ import { useCallback } from "react";
*/ */
export function useRules() { export function useRules() {
const { data: list, save } = useStorage(STOKEY_RULES, DEFAULT_RULES); const { data: list, save } = useStorage(STOKEY_RULES, DEFAULT_RULES);
const { const { updateSyncMeta } = useSyncMeta();
sync: { rulesUpdateAt },
updateSync,
} = useSync();
const updateRules = useCallback( const updateRules = useCallback(
async (rules) => { async (rules) => {
const updateAt = rulesUpdateAt ? Date.now() : 0;
await save(rules); await save(rules);
await updateSync({ rulesUpdateAt: updateAt }); await updateSyncMeta(KV_RULES_KEY);
trySyncRules(); trySyncRules();
}, },
[rulesUpdateAt, save, updateSync] [save, updateSyncMeta]
); );
const add = useCallback( const add = useCallback(
@@ -53,6 +49,12 @@ export function useRules() {
[list, updateRules] [list, updateRules]
); );
const clear = useCallback(async () => {
let rules = [...list];
rules = rules.filter((item) => item.pattern === "*");
await updateRules(rules);
}, [list, updateRules]);
const put = useCallback( const put = useCallback(
async (pattern, obj) => { async (pattern, obj) => {
const rules = [...list]; const rules = [...list];
@@ -85,5 +87,5 @@ export function useRules() {
[list, updateRules] [list, updateRules]
); );
return { list, add, del, put, merge }; return { list, add, del, clear, put, merge };
} }

View File

@@ -1,36 +1,47 @@
import { STOKEY_SETTING, DEFAULT_SETTING } from "../config"; import { STOKEY_SETTING, DEFAULT_SETTING, KV_SETTING_KEY } from "../config";
import { useStorage } from "./Storage"; import { useStorage } from "./Storage";
import { useSync } from "./Sync";
import { trySyncSetting } from "../libs/sync"; import { trySyncSetting } from "../libs/sync";
import { createContext, useCallback, useContext } from "react"; import { createContext, useCallback, useContext, useMemo } from "react";
import { debounce } from "../libs/utils";
import { useSyncMeta } from "./Sync";
const SettingContext = createContext({ const SettingContext = createContext({
setting: null, setting: null,
updateSetting: async () => {}, updateSetting: async () => {},
reloadSetting: async () => {},
}); });
export function SettingProvider({ children }) { export function SettingProvider({ children }) {
const { data, update } = useStorage(STOKEY_SETTING, DEFAULT_SETTING); const { data, update, reload } = useStorage(STOKEY_SETTING, DEFAULT_SETTING);
const { const { updateSyncMeta } = useSyncMeta();
sync: { settingUpdateAt },
updateSync, const syncSetting = useMemo(
} = useSync(); () =>
debounce(() => {
trySyncSetting();
}, [2000]),
[]
);
const updateSetting = useCallback( const updateSetting = useCallback(
async (obj) => { async (obj) => {
const updateAt = settingUpdateAt ? Date.now() : 0;
await update(obj); await update(obj);
await updateSync({ settingUpdateAt: updateAt }); await updateSyncMeta(KV_SETTING_KEY);
trySyncSetting(); syncSetting();
}, },
[settingUpdateAt, update, updateSync] [update, syncSetting, updateSyncMeta]
); );
if (!data) {
return;
}
return ( return (
<SettingContext.Provider <SettingContext.Provider
value={{ value={{
setting: data, setting: data,
updateSetting, updateSetting,
reloadSetting: reload,
}} }}
> >
{children} {children}

19
src/hooks/Shortcut.js Normal file
View File

@@ -0,0 +1,19 @@
import { useCallback } from "react";
import { DEFAULT_SHORTCUTS } from "../config";
import { useSetting } from "./Setting";
export function useShortcut(action) {
const { setting, updateSetting } = useSetting();
const shortcuts = setting?.shortcuts || DEFAULT_SHORTCUTS;
const shortcut = shortcuts[action] || [];
const setShortcut = useCallback(
async (val) => {
Object.assign(shortcuts, { [action]: val });
await updateSetting({ shortcuts });
},
[action, shortcuts, updateSetting]
);
return { shortcut, setShortcut };
}

View File

@@ -1,8 +1,9 @@
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { storage } from "../libs/storage"; import { storage } from "../libs/storage";
export function useStorage(key, defaultVal = null) { export function useStorage(key, defaultVal) {
const [data, setData] = useState(defaultVal); const [loading, setLoading] = useState(false);
const [data, setData] = useState(null);
const save = useCallback( const save = useCallback(
async (val) => { async (val) => {
@@ -14,7 +15,7 @@ export function useStorage(key, defaultVal = null) {
const update = useCallback( const update = useCallback(
async (obj) => { async (obj) => {
setData((pre) => ({ ...pre, ...obj })); setData((pre = {}) => ({ ...pre, ...obj }));
await storage.putObj(key, obj); await storage.putObj(key, obj);
}, },
[key] [key]
@@ -25,16 +26,38 @@ export function useStorage(key, defaultVal = null) {
await storage.del(key); await storage.del(key);
}, [key]); }, [key]);
useEffect(() => { const reload = useCallback(async () => {
(async () => { try {
setLoading(true);
const val = await storage.getObj(key); const val = await storage.getObj(key);
if (val) { if (val) {
setData(val); setData(val);
} else if (defaultVal) { }
await storage.setObj(key, defaultVal); } catch (err) {
console.log("[storage reload]", err.message);
} finally {
setLoading(false);
}
}, [key]);
useEffect(() => {
(async () => {
try {
setLoading(true);
const val = await storage.getObj(key);
if (val) {
setData(val);
} else if (defaultVal) {
setData(defaultVal);
await storage.setObj(key, defaultVal);
}
} catch (err) {
console.log("[storage load]", err.message);
} finally {
setLoading(false);
} }
})(); })();
}, [key, defaultVal]); }, [key, defaultVal]);
return { data, save, update, remove }; return { data, save, update, remove, reload, loading };
} }

View File

@@ -1,4 +1,4 @@
import { DEFAULT_SUBRULES_LIST } from "../config"; import { DEFAULT_SUBRULES_LIST, DEFAULT_OW_RULE } from "../config";
import { useSetting } from "./Setting"; 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";
@@ -32,6 +32,19 @@ export function useSubRules() {
[list, updateSetting] [list, updateSetting]
); );
const updateSub = useCallback(
async (url, obj) => {
const subrulesList = [...list];
subrulesList.forEach((item) => {
if (item.url === url) {
Object.assign(item, obj);
}
});
await updateSetting({ subrulesList });
},
[list, updateSetting]
);
const addSub = useCallback( const addSub = useCallback(
async (url) => { async (url) => {
const subrulesList = [...list]; const subrulesList = [...list];
@@ -70,6 +83,7 @@ export function useSubRules() {
return { return {
subList: list, subList: list,
selectSub, selectSub,
updateSub,
addSub, addSub,
delSub, delSub,
selectedSub, selectedSub,
@@ -79,3 +93,21 @@ export function useSubRules() {
loading, loading,
}; };
} }
/**
* 覆写订阅规则
* @returns
*/
export function useOwSubRule() {
const { setting, updateSetting } = useSetting();
const { owSubrule = DEFAULT_OW_RULE } = setting;
const updateOwSubrule = useCallback(
async (obj) => {
await updateSetting({ owSubrule: { ...owSubrule, ...obj } });
},
[owSubrule, updateSetting]
);
return { owSubrule, updateOwSubrule };
}

View File

@@ -1,3 +1,4 @@
import { useCallback } from "react";
import { STOKEY_SYNC, DEFAULT_SYNC } from "../config"; import { STOKEY_SYNC, DEFAULT_SYNC } from "../config";
import { useStorage } from "./Storage"; import { useStorage } from "./Storage";
@@ -6,6 +7,57 @@ import { useStorage } from "./Storage";
* @returns * @returns
*/ */
export function useSync() { export function useSync() {
const { data, update } = useStorage(STOKEY_SYNC, DEFAULT_SYNC); const { data, update, reload } = useStorage(STOKEY_SYNC, DEFAULT_SYNC);
return { sync: data, updateSync: update }; return { sync: data, updateSync: update, reloadSync: reload };
}
/**
* update syncmeta hook
* @returns
*/
export function useSyncMeta() {
const { sync, updateSync } = useSync();
const updateSyncMeta = useCallback(
async (key) => {
const syncMeta = sync?.syncMeta || {};
syncMeta[key] = { ...(syncMeta[key] || {}), updateAt: Date.now() };
await updateSync({ syncMeta });
},
[sync?.syncMeta, updateSync]
);
return { updateSyncMeta };
}
/**
* caches sync hook
* @param {*} url
* @returns
*/
export function useSyncCaches() {
const { sync, updateSync, reloadSync } = useSync();
const updateDataCache = useCallback(
async (url) => {
const dataCaches = sync?.dataCaches || {};
dataCaches[url] = Date.now();
await updateSync({ dataCaches });
},
[sync, updateSync]
);
const deleteDataCache = useCallback(
async (url) => {
const dataCaches = sync?.dataCaches || {};
delete dataCaches[url];
await updateSync({ dataCaches });
},
[sync, updateSync]
);
return {
dataCaches: sync?.dataCaches || {},
updateDataCache,
deleteDataCache,
reloadSync,
};
} }

18
src/hooks/Tranbox.js Normal file
View File

@@ -0,0 +1,18 @@
import { useCallback } from "react";
import { DEFAULT_TRANBOX_SETTING } from "../config";
import { useSetting } from "./Setting";
export function useTranbox() {
const { setting, updateSetting } = useSetting();
const tranboxSetting = setting?.tranboxSetting || DEFAULT_TRANBOX_SETTING;
const updateTranbox = useCallback(
async (obj) => {
Object.assign(tranboxSetting, obj);
await updateSetting({ tranboxSetting });
},
[tranboxSetting, updateSetting]
);
return { tranboxSetting, updateTranbox };
}

View File

@@ -2,6 +2,7 @@ import { useEffect } from "react";
import { useState } from "react"; 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";
/** /**
* 翻译hook * 翻译hook
@@ -22,16 +23,17 @@ export function useTranslate(q, rule, setting) {
try { try {
setLoading(true); setLoading(true);
const deLang = await tryDetectLang(q); const deLang = await tryDetectLang(q, setting.detectRemote);
if (deLang && toLang.includes(deLang)) { if (deLang && toLang.includes(deLang)) {
setSamelang(true); setSamelang(true);
} else { } else {
const [trText, isSame] = await apiTranslate({ const [trText, isSame] = await apiTranslate({
translator, translator,
q, text: q,
fromLang, fromLang,
toLang, toLang,
setting, apiSetting:
setting.transApis?.[translator] || DEFAULT_TRANS_APIS[translator],
}); });
setText(trText); setText(trText);
setSamelang(isSame); setSamelang(isSame);

View File

@@ -32,25 +32,23 @@ function App() {
href={process.env.REACT_APP_HOMEPAGE} href={process.env.REACT_APP_HOMEPAGE}
>{`KISS Translator v${process.env.REACT_APP_VERSION}`}</Link> >{`KISS Translator v${process.env.REACT_APP_VERSION}`}</Link>
</Divider> </Divider>
<Stack spacing={2} direction="row" useFlexGap flexWrap="wrap"> <Stack spacing={2}>
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}> <Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}>
Install Userscript 1 Install/Update Userscript for Tampermonkey/Violentmonkey
</Link>
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL2}>
Install Userscript 2
</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 Userscript Safari 1 Install/Update Userscript for iOS Safari
</Link> </Link>
<Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL2}> {/* <Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL2}>
Install Userscript Safari 2 Install/Update Userscript for iOS Safari 2
</Link> </Link> */}
<Link href={process.env.REACT_APP_OPTIONSPAGE}> <Link href={process.env.REACT_APP_OPTIONSPAGE}>Open Options Page</Link>
Open Options Page 1 {/* <Link href={process.env.REACT_APP_OPTIONSPAGE2}>
</Link>
<Link href={process.env.REACT_APP_OPTIONSPAGE2}>
Open Options Page 2 Open Options Page 2
</Link> </Link> */}
</Stack> </Stack>
{loading ? ( {loading ? (

View File

@@ -13,3 +13,5 @@ function _browser() {
} }
export const browser = _browser(); export const browser = _browser();
export const isBg = () => globalThis?.ContextType === "BACKGROUND";

View File

@@ -6,12 +6,11 @@ import {
MSG_FETCH_LIMIT, MSG_FETCH_LIMIT,
MSG_FETCH_CLEAR, MSG_FETCH_CLEAR,
CACHE_NAME, CACHE_NAME,
OPT_TRANS_MICROSOFT,
OPT_TRANS_OPENAI,
DEFAULT_FETCH_INTERVAL, DEFAULT_FETCH_INTERVAL,
DEFAULT_FETCH_LIMIT, DEFAULT_FETCH_LIMIT,
} from "../config"; } from "../config";
import { msAuth } from "./auth"; import { isBg } from "./browser";
import { newCacheReq, newTransReq } from "./req";
/** /**
* 油猴脚本的请求封装 * 油猴脚本的请求封装
@@ -26,51 +25,34 @@ export const fetchGM = async (input, { method = "GET", headers, body } = {}) =>
url: input, url: input,
headers, headers,
data: body, data: body,
onload: (response) => { // withCredentials: true,
if (response.status === 200) { onload: ({ response, responseHeaders, status, statusText, ...opts }) => {
const headers = new Headers(); const headers = {};
response.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());
if (name && value) { if (name && value) {
headers.append(name, value); headers[name] = value;
} }
}); });
resolve(new Response(response.response, { headers })); resolve({
} else { body: response,
reject(new Error(`[${response.status}] ${response.responseText}`)); headers,
} status,
statusText,
});
}, },
onerror: reject, onerror: reject,
}); });
}); });
/**
* 构造缓存 request
* @param {*} request
* @returns
*/
const newCacheReq = async (request) => {
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;
};
/** /**
* 发起请求 * 发起请求
* @param {*} param0 * @param {*} param0
* @returns * @returns
*/ */
const fetchApi = async ({ input, init = {}, translator, token }) => { export const fetchApi = async ({ input, init, transOpts, apiSetting }) => {
if (translator === OPT_TRANS_MICROSOFT) { if (transOpts?.translator) {
init.headers["Authorization"] = `Bearer ${token}`; [input, init] = await newTransReq(transOpts, apiSetting);
} else if (translator === OPT_TRANS_OPENAI) {
init.headers["Authorization"] = `Bearer ${token}`; // // OpenAI
init.headers["api-key"] = token; // Azure OpenAI
} }
if (isGm) { if (isGm) {
@@ -80,17 +62,26 @@ const fetchApi = async ({ input, init = {}, translator, token }) => {
} else { } else {
info = GM.info; info = GM.info;
} }
const connects = info?.script?.connects || [];
// Tampermonkey --> .connects
// Violentmonkey --> .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) {
if (window.KISS_GM) { const { body, headers, status, statusText } = window.KISS_GM
return window.KISS_GM.fetch(input, init); ? await window.KISS_GM.fetch(input, init)
} else { : await fetchGM(input, init);
return fetchGM(input, init);
} return new Response(body, {
headers: new Headers(headers),
status,
statusText,
});
} }
} }
return fetch(input, init); return fetch(input, init);
}; };
@@ -99,13 +90,7 @@ const fetchApi = async ({ input, init = {}, translator, token }) => {
*/ */
export const fetchPool = taskPool( export const fetchPool = taskPool(
fetchApi, fetchApi,
async ({ translator }) => { null,
if (translator === OPT_TRANS_MICROSOFT) {
const [token] = await msAuth();
return { token };
}
return {};
},
DEFAULT_FETCH_INTERVAL, DEFAULT_FETCH_INTERVAL,
DEFAULT_FETCH_LIMIT DEFAULT_FETCH_LIMIT
); );
@@ -118,9 +103,9 @@ export const fetchPool = taskPool(
*/ */
export const fetchData = async ( export const fetchData = async (
input, input,
{ useCache, usePool, translator, token, ...init } = {} { useCache, usePool, transOpts, apiSetting, ...init } = {}
) => { ) => {
const cacheReq = await newCacheReq(new Request(input, init)); const cacheReq = await newCacheReq(input, init);
let res; let res;
// 查询缓存 // 查询缓存
@@ -136,9 +121,9 @@ export const fetchData = async (
if (!res) { if (!res) {
// 发送请求 // 发送请求
if (usePool) { if (usePool) {
res = await fetchPool.push({ input, init, translator, token }); res = await fetchPool.push({ input, init, transOpts, apiSetting });
} else { } else {
res = await fetchApi({ input, init, translator, token }); res = await fetchApi({ input, init, transOpts, apiSetting });
} }
if (!res?.ok) { if (!res?.ok) {
@@ -169,9 +154,13 @@ export const fetchData = async (
* @param {*} opts * @param {*} opts
* @returns * @returns
*/ */
export const fetchPolyfill = async (input, { isBg = false, ...opts } = {}) => { export const fetchPolyfill = async (input, opts) => {
if (!input?.trim()) {
throw new Error("URL is empty");
}
// 插件 // 插件
if (isExt && !isBg) { if (isExt && !isBg()) {
const res = await sendBgMsg(MSG_FETCH, { input, opts }); const res = await sendBgMsg(MSG_FETCH, { input, opts });
if (res.error) { if (res.error) {
throw new Error(res.error); throw new Error(res.error);

View File

@@ -1,20 +1,31 @@
import { fetchGM } from "./fetch"; import { fetchGM } from "./fetch";
import { genEventName } from "./utils";
const MSG_GM_xmlHttpRequest = "xmlHttpRequest";
const MSG_GM_setValue = "setValue";
const MSG_GM_getValue = "getValue";
const MSG_GM_deleteValue = "deleteValue";
const MSG_GM_info = "info";
/** /**
* 注入页面的脚本请求并接受GM接口信息 * 注入页面的脚本请求并接受GM接口信息
* @param {*} param0 * @param {*} param0
*/ */
export const injectScript = (ping) => { export const injectScript = (ping) => {
const MSG_GM_xmlHttpRequest = "xmlHttpRequest"; window.APP_INFO = {
const MSG_GM_setValue = "setValue"; name: process.env.REACT_APP_NAME,
const MSG_GM_getValue = "getValue"; version: process.env.REACT_APP_VERSION,
const MSG_GM_deleteValue = "deleteValue"; eventName: ping,
const MSG_GM_info = "info"; };
let GM_info; };
/**
* 适配GM脚本
*/
export const adaptScript = (ping) => {
const promiseGM = (action, args, timeout = 5000) => const promiseGM = (action, args, timeout = 5000) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
const pong = btoa(Math.random()).slice(3, 11); const pong = genEventName();
const handleEvent = (e) => { const handleEvent = (e) => {
window.removeEventListener(pong, handleEvent); window.removeEventListener(pong, handleEvent);
const { data, error } = e.detail; const { data, error } = e.detail;
@@ -41,14 +52,13 @@ export const injectScript = (ping) => {
setValue: (key, val) => promiseGM(MSG_GM_setValue, { key, val }), setValue: (key, val) => promiseGM(MSG_GM_setValue, { key, val }),
getValue: (key) => promiseGM(MSG_GM_getValue, { key }), getValue: (key) => promiseGM(MSG_GM_getValue, { key }),
deleteValue: (key) => promiseGM(MSG_GM_deleteValue, { key }), deleteValue: (key) => promiseGM(MSG_GM_deleteValue, { key }),
getInfo: () => { getInfo: async () => {
if (GM_info) { if (!window.GM_info) {
return GM_info; window.GM_info = await promiseGM(MSG_GM_info);
} }
return promiseGM(MSG_GM_info); return window.GM_info;
}, },
}; };
window.APP_NAME = process.env.REACT_APP_NAME;
}; };
/** /**
@@ -56,11 +66,6 @@ export const injectScript = (ping) => {
* @param {*} param0 * @param {*} param0
*/ */
export const handlePing = async (e) => { export const handlePing = async (e) => {
const MSG_GM_xmlHttpRequest = "xmlHttpRequest";
const MSG_GM_setValue = "setValue";
const MSG_GM_getValue = "getValue";
const MSG_GM_deleteValue = "deleteValue";
const MSG_GM_info = "info";
const { action, args, pong } = e.detail; const { action, args, pong } = e.detail;
let res; let res;
try { try {

View File

@@ -5,3 +5,7 @@ export const sendIframeMsg = (action, args) => {
iframe.contentWindow.postMessage({ action, args }, "*"); iframe.contentWindow.postMessage({ action, args }, "*");
}); });
}; };
export const sendParentMsg = (action, args) => {
window.parent.postMessage({ action, args }, "*");
};

View File

@@ -1,5 +1,6 @@
import { CACHE_NAME } from "../config"; import { CACHE_NAME } from "../config";
import { browser } from "./browser"; import { browser } from "./browser";
import { apiBaiduLangdetect } from "../apis";
/** /**
* 清除缓存数据 * 清除缓存数据
@@ -13,15 +14,29 @@ export const tryClearCaches = async () => {
}; };
/** /**
* 本地语言识别 * 语言识别
* @param {*} q * @param {*} q
* @returns * @returns
*/ */
export const tryDetectLang = async (q) => { export const tryDetectLang = async (q, useRemote = false) => {
try { let lang = "";
const res = await browser?.i18n?.detectLanguage(q);
return res?.languages?.[0]?.language; if (useRemote) {
} catch (err) { try {
console.log("[detect lang]", err.message); lang = await apiBaiduLangdetect(q);
} catch (err) {
console.log("[detect lang remote]", err.message);
}
} }
if (!lang) {
try {
const res = await browser?.i18n?.detectLanguage(q);
lang = res?.languages?.[0]?.language;
} catch (err) {
console.log("[detect lang local]", err.message);
}
}
return lang;
}; };

View File

@@ -19,3 +19,12 @@ export const sendTabMsg = async (action, args) => {
const tabs = await browser.tabs.query({ active: true, currentWindow: true }); const tabs = await browser.tabs.query({ active: true, currentWindow: true });
return browser.tabs.sendMessage(tabs[0].id, { action, args }); return browser.tabs.sendMessage(tabs[0].id, { action, args });
}; };
/**
* 获取当前tab信息
* @returns
*/
export const getTabInfo = async () => {
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
return tabs[0];
};

View File

@@ -6,7 +6,13 @@
* @param {*} _limit * @param {*} _limit
* @returns * @returns
*/ */
export const taskPool = (fn, preFn, _interval = 100, _limit = 100) => { export const taskPool = (
fn,
preFn,
_interval = 100,
_limit = 100,
_retryInteral = 1000
) => {
const pool = []; const pool = [];
const maxRetry = 2; // 最大重试次数 const maxRetry = 2; // 最大重试次数
let maxCount = _limit; // 最大数量 let maxCount = _limit; // 最大数量
@@ -14,23 +20,6 @@ export const taskPool = (fn, preFn, _interval = 100, _limit = 100) => {
let interval = _interval; // 间隔时间 let interval = _interval; // 间隔时间
let timer = null; let timer = null;
const handleTask = async (item, preArgs) => {
curCount++;
const { args, resolve, reject, retry } = item;
try {
const res = await fn({ ...args, ...preArgs });
resolve(res);
} catch (err) {
if (retry < maxRetry) {
pool.push({ args, resolve, reject, retry: retry + 1 });
} else {
reject(err);
}
} finally {
curCount--;
}
};
const run = async () => { const run = async () => {
// console.log("timer", timer); // console.log("timer", timer);
timer && clearTimeout(timer); timer && clearTimeout(timer);
@@ -39,12 +28,24 @@ export const taskPool = (fn, preFn, _interval = 100, _limit = 100) => {
if (curCount < maxCount) { if (curCount < maxCount) {
const item = pool.shift(); const item = pool.shift();
if (item) { if (item) {
curCount++;
const { args, resolve, reject, retry } = item;
try { try {
const preArgs = await preFn(item.args); const preArgs = preFn ? await preFn(item.args) : {};
handleTask(item, preArgs); const res = await fn({ ...args, ...preArgs });
resolve(res);
} catch (err) { } catch (err) {
console.log("[preFn]", err); console.log("[task]", retry, err);
pool.push(item); if (retry < maxRetry) {
const retryTimer = setTimeout(() => {
clearTimeout(retryTimer);
pool.push({ args, resolve, reject, retry: retry + 1 });
}, _retryInteral);
} else {
reject(err);
}
} finally {
curCount--;
} }
} }
} }

251
src/libs/req.js Normal file
View File

@@ -0,0 +1,251 @@
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`);
}
};

View File

@@ -1,14 +1,18 @@
import { matchValue, type, isMatch } from "./utils"; import { matchValue, type, isMatch } from "./utils";
import { import {
GLOBAL_KEY, GLOBAL_KEY,
REMAIN_KEY,
OPT_TRANS_ALL, OPT_TRANS_ALL,
OPT_STYLE_ALL, OPT_STYLE_ALL,
OPT_LANGS_FROM, OPT_LANGS_FROM,
OPT_LANGS_TO, OPT_LANGS_TO,
GLOBLA_RULE, GLOBLA_RULE,
DEFAULT_SUBRULES_LIST, DEFAULT_SUBRULES_LIST,
DEFAULT_OW_RULE,
} from "../config"; } from "../config";
import { loadOrFetchSubRules } from "./subRules"; import { loadOrFetchSubRules } from "./subRules";
import { getRulesWithDefault, setRules } from "./storage";
import { trySyncRules } from "./sync";
/** /**
* 根据href匹配规则 * 根据href匹配规则
@@ -19,14 +23,34 @@ import { loadOrFetchSubRules } from "./subRules";
export const matchRule = async ( export const matchRule = async (
rules, rules,
href, href,
{ injectRules = true, subrulesList = DEFAULT_SUBRULES_LIST } {
injectRules = true,
subrulesList = DEFAULT_SUBRULES_LIST,
owSubrule = DEFAULT_OW_RULE,
}
) => { ) => {
rules = [...rules]; rules = [...rules];
if (injectRules) { if (injectRules) {
try { try {
const selectedSub = subrulesList.find((item) => item.selected); const selectedSub = subrulesList.find((item) => item.selected);
if (selectedSub?.url) { if (selectedSub?.url) {
const subRules = await loadOrFetchSubRules(selectedSub.url); const mixRule = {};
Object.entries(owSubrule)
.filter(([key, val]) => {
if (
owSubrule.textStyle === REMAIN_KEY &&
(key === "bgColor" || key === "textDiyStyle")
) {
return false;
}
return val !== REMAIN_KEY;
})
.forEach(([key, val]) => {
mixRule[key] = val;
});
let subRules = await loadOrFetchSubRules(selectedSub.url);
subRules = subRules.map((item) => ({ ...item, ...mixRule }));
rules.splice(-1, 0, ...subRules); rules.splice(-1, 0, ...subRules);
} }
} catch (err) { } catch (err) {
@@ -37,29 +61,25 @@ export const matchRule = async (
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 =
rules.find((r) => r.pattern.split(",").some((p) => p.trim() === "*")) ||
GLOBLA_RULE;
if (!rule) { if (!rule) {
return globalRule; return globalRule;
} }
rule.selector = rule.selector = rule.selector?.trim() || globalRule.selector;
rule?.selector?.trim() || if (rule.textStyle === GLOBAL_KEY) {
globalRule?.selector?.trim() || rule.textStyle = globalRule.textStyle;
GLOBLA_RULE.selector; rule.bgColor = globalRule.bgColor;
rule.textDiyStyle = globalRule.textDiyStyle;
rule.bgColor = rule?.bgColor?.trim() || globalRule?.bgColor?.trim(); } else {
rule.bgColor = rule.bgColor?.trim() || globalRule.bgColor;
["translator", "fromLang", "toLang", "textStyle", "transOpen"].forEach( rule.textDiyStyle = rule.textDiyStyle?.trim() || globalRule.textDiyStyle;
(key) => { }
if (rule[key] === GLOBAL_KEY) { ["translator", "fromLang", "toLang", "transOpen"].forEach((key) => {
rule[key] = globalRule[key]; if (rule[key] === GLOBAL_KEY) {
} rule[key] = globalRule[key];
} }
); });
return rule; return rule;
}; };
@@ -99,10 +119,12 @@ export const checkRules = (rules) => {
textStyle, textStyle,
transOpen, transOpen,
bgColor, bgColor,
textDiyStyle,
}) => ({ }) => ({
pattern: pattern.trim(), pattern: pattern.trim(),
selector: type(selector) === "string" ? selector : "", selector: type(selector) === "string" ? selector : "",
bgColor: type(bgColor) === "string" ? bgColor : "", bgColor: type(bgColor) === "string" ? bgColor : "",
textDiyStyle: type(textDiyStyle) === "string" ? textDiyStyle : "",
translator: matchValue([GLOBAL_KEY, ...OPT_TRANS_ALL], translator), translator: matchValue([GLOBAL_KEY, ...OPT_TRANS_ALL], translator),
fromLang: matchValue([GLOBAL_KEY, ...fromLangs], fromLang), fromLang: matchValue([GLOBAL_KEY, ...fromLangs], fromLang),
toLang: matchValue([GLOBAL_KEY, ...toLangs], toLang), toLang: matchValue([GLOBAL_KEY, ...toLangs], toLang),
@@ -113,3 +135,19 @@ export const checkRules = (rules) => {
return rules; return rules;
}; };
/**
* 保存或更新rule
* @param {*} newRule
*/
export const saveRule = async (newRule) => {
const rules = await getRulesWithDefault();
const rule = rules.find((item) => isMatch(newRule.pattern, item.pattern));
if (rule && rule.pattern !== GLOBAL_KEY) {
Object.assign(rule, { ...newRule, pattern: rule.pattern });
} else {
rules.unshift(newRule);
}
await setRules(rules);
trySyncRules();
};

112
src/libs/shortcut.js Normal file
View File

@@ -0,0 +1,112 @@
import { isSameSet } from "./utils";
/**
* 键盘快捷键监听
* @param {*} fn
* @param {*} target
* @param {*} timeout
* @returns
*/
export const shortcutListener = (fn, target = document, timeout = 3000) => {
const allkeys = new Set();
const curkeys = new Set();
let timer = null;
const handleKeydown = (e) => {
timer && clearTimeout(timer);
timer = setTimeout(() => {
allkeys.clear();
curkeys.clear();
clearTimeout(timer);
timer = null;
}, timeout);
if (e.code) {
allkeys.add(e.code);
curkeys.add(e.code);
fn([...curkeys], [...allkeys]);
}
};
const handleKeyup = (e) => {
curkeys.delete(e.code);
if (curkeys.size === 0) {
fn([...curkeys], [...allkeys]);
allkeys.clear();
}
};
target.addEventListener("keydown", handleKeydown);
target.addEventListener("keyup", handleKeyup);
return () => {
if (timer) {
clearTimeout(timer);
timer = null;
}
target.removeEventListener("keydown", handleKeydown);
target.removeEventListener("keyup", handleKeyup);
};
};
/**
* 注册键盘快捷键
* @param {*} targetKeys
* @param {*} fn
* @param {*} target
* @returns
*/
export const shortcutRegister = (targetKeys = [], fn, target = document) => {
return shortcutListener((curkeys) => {
if (
targetKeys.length > 0 &&
isSameSet(new Set(targetKeys), new Set(curkeys))
) {
fn();
}
}, target);
};
/**
* 注册连续快捷键
* @param {*} targetKeys
* @param {*} fn
* @param {*} step
* @param {*} timeout
* @param {*} target
* @returns
*/
export const stepShortcutRegister = (
targetKeys = [],
fn,
step = 3,
timeout = 500,
target = document
) => {
let count = 0;
let pre = Date.now();
let timer;
return shortcutListener((curkeys, allkeys) => {
timer && clearTimeout(timer);
timer = setTimeout(() => {
clearTimeout(timer);
count = 0;
}, timeout);
if (targetKeys.length > 0 && curkeys.length === 0) {
const now = Date.now();
if (
(count === 0 || now - pre < timeout) &&
isSameSet(new Set(targetKeys), new Set(allkeys))
) {
count++;
if (count === step) {
count = 0;
fn();
}
} else {
count = 0;
}
pre = now;
}
}, target);
};

View File

@@ -1,10 +1,13 @@
import { import {
STOKEY_SETTING, STOKEY_SETTING,
STOKEY_RULES, STOKEY_RULES,
STOKEY_WORDS,
STOKEY_FAB, STOKEY_FAB,
STOKEY_SYNC, STOKEY_SYNC,
STOKEY_MSAUTH, STOKEY_MSAUTH,
STOKEY_BDAUTH,
STOKEY_RULESCACHE_PREFIX, STOKEY_RULESCACHE_PREFIX,
STOKEY_WEBFIXCACHE_PREFIX,
DEFAULT_SETTING, DEFAULT_SETTING,
DEFAULT_RULES, DEFAULT_RULES,
DEFAULT_SYNC, DEFAULT_SYNC,
@@ -82,10 +85,8 @@ export const storage = {
* 设置信息 * 设置信息
*/ */
export const getSetting = () => getObj(STOKEY_SETTING); export const getSetting = () => getObj(STOKEY_SETTING);
export const getSettingWithDefault = async () => ({ export const getSettingWithDefault = async () =>
...DEFAULT_SETTING, (await getSetting()) || 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);
@@ -97,6 +98,13 @@ export const getRulesWithDefault = async () =>
(await getRules()) || DEFAULT_RULES; (await getRules()) || DEFAULT_RULES;
export const setRules = (val) => setObj(STOKEY_RULES, val); export const setRules = (val) => setObj(STOKEY_RULES, val);
/**
* 词汇列表
*/
export const getWords = () => getObj(STOKEY_WORDS);
export const getWordsWithDefault = async () => (await getWords()) || {};
export const setWords = (val) => setObj(STOKEY_WORDS, val);
/** /**
* 订阅规则 * 订阅规则
*/ */
@@ -106,12 +114,21 @@ 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位置
*/ */
export const getFab = () => getObj(STOKEY_FAB); export const getFab = () => getObj(STOKEY_FAB);
export const getFabWithDefault = async () => (await getFab()) || {}; export const getFabWithDefault = async () => (await getFab()) || {};
export const setFab = (obj) => setObj(STOKEY_FAB, obj); export const setFab = (obj) => setObj(STOKEY_FAB, obj);
export const updateFab = (obj) => putObj(STOKEY_FAB, obj);
/** /**
* 数据同步 * 数据同步
@@ -126,6 +143,12 @@ export const updateSync = (obj) => putObj(STOKEY_SYNC, obj);
export const getMsauth = () => getObj(STOKEY_MSAUTH); export const getMsauth = () => getObj(STOKEY_MSAUTH);
export const setMsauth = (val) => setObj(STOKEY_MSAUTH, val); export const setMsauth = (val) => setObj(STOKEY_MSAUTH, val);
/**
* baidu auth
*/
export const getBdauth = () => getObj(STOKEY_BDAUTH);
export const setBdauth = (val) => setObj(STOKEY_BDAUTH, val);
/** /**
* 存入默认数据 * 存入默认数据
*/ */

View File

@@ -5,18 +5,30 @@ import {
setSubRules, setSubRules,
getSubRules, getSubRules,
} from "./storage"; } from "./storage";
import { apiFetchRules } from "../apis"; import { apiFetch } from "../apis";
import { checkRules } from "./rules"; import { checkRules } from "./rules";
import { isAllchar } from "./utils";
import { syncWebfix } from "./webfix";
/**
* 更新缓存同步时间
* @param {*} url
*/
const updateSyncDataCache = async (url) => {
const { dataCaches = {} } = await getSyncWithDefault();
dataCaches[url] = Date.now();
await updateSync({ dataCaches });
};
/** /**
* 同步订阅规则 * 同步订阅规则
* @param {*} url * @param {*} url
* @returns * @returns
*/ */
export const syncSubRules = async (url, isBg = false) => { export const syncSubRules = async (url) => {
const res = await apiFetchRules(url, isBg); const res = await apiFetch(url);
const rules = checkRules(res).filter( const rules = checkRules(res).filter(
(rule) => rule.pattern.replaceAll(GLOBAL_KEY, "") !== "" ({ pattern }) => !isAllchar(pattern, GLOBAL_KEY)
); );
if (rules.length > 0) { if (rules.length > 0) {
await setSubRules(url, rules); await setSubRules(url, rules);
@@ -29,10 +41,11 @@ export const syncSubRules = async (url, isBg = false) => {
* @param {*} url * @param {*} url
* @returns * @returns
*/ */
export const syncAllSubRules = async (subrulesList, isBg = false) => { export const syncAllSubRules = async (subrulesList) => {
for (let subrules of subrulesList) { for (let subrules of subrulesList) {
try { try {
await syncSubRules(subrules.url, isBg); await syncSubRules(subrules.url);
await updateSyncDataCache(subrules.url);
} catch (err) { } catch (err) {
console.log(`[sync subrule error]: ${subrules.url}`, err); console.log(`[sync subrule error]: ${subrules.url}`, err);
} }
@@ -44,14 +57,18 @@ export const syncAllSubRules = async (subrulesList, isBg = false) => {
* @param {*} url * @param {*} url
* @returns * @returns
*/ */
export const trySyncAllSubRules = async ({ subrulesList }, isBg = false) => { export const trySyncAllSubRules = async ({ subrulesList }) => {
try { try {
const { subRulesSyncAt } = await getSyncWithDefault(); const { subRulesSyncAt } = await getSyncWithDefault();
const now = Date.now(); const now = Date.now();
const interval = 24 * 60 * 60 * 1000; // 间隔一天 const interval = 24 * 60 * 60 * 1000; // 间隔一天
if (now - subRulesSyncAt > interval) { if (now - subRulesSyncAt > interval) {
await syncAllSubRules(subrulesList, isBg); // 同步订阅规则
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); console.log("[try sync all subrules]", err);
@@ -64,9 +81,10 @@ export const trySyncAllSubRules = async ({ subrulesList }, isBg = false) => {
* @returns * @returns
*/ */
export const loadOrFetchSubRules = async (url) => { export const loadOrFetchSubRules = async (url) => {
const rules = await getSubRules(url); let rules = await getSubRules(url);
if (rules?.length) { if (!rules || rules.length === 0) {
return rules; rules = await syncSubRules(url);
await updateSyncDataCache(url);
} }
return syncSubRules(url); return rules || [];
}; };

34
src/libs/svg.js Normal file
View File

@@ -0,0 +1,34 @@
export const loadingSvg = `
<svg viewBox="0 0 100 100" style="display:inline-block; width:100%; height: 100%;">
<circle fill="#209CEE" stroke="none" cx="6" cy="50" r="6">
<animateTransform
attributeName="transform"
dur="1s"
type="translate"
values="0 15 ; 0 -15; 0 15"
repeatCount="indefinite"
begin="0.1"
/>
</circle>
<circle fill="#209CEE" stroke="none" cx="30" cy="50" r="6">
<animateTransform
attributeName="transform"
dur="1s"
type="translate"
values="0 10 ; 0 -10; 0 10"
repeatCount="indefinite"
begin="0.2"
/>
</circle>
<circle fill="#209CEE" stroke="none" cx="54" cy="50" r="6">
<animateTransform
attributeName="transform"
dur="1s"
type="translate"
values="0 5 ; 0 -5; 0 5"
repeatCount="indefinite"
begin="0.3"
/>
</circle>
</svg>
`;

View File

@@ -1,56 +1,119 @@
import { import {
APP_LCNAME,
KV_SETTING_KEY, KV_SETTING_KEY,
KV_RULES_KEY, KV_RULES_KEY,
KV_WORDS_KEY,
KV_RULES_SHARE_KEY, KV_RULES_SHARE_KEY,
KV_SALT_SHARE, KV_SALT_SHARE,
OPT_SYNCTYPE_WEBDAV,
} from "../config"; } from "../config";
import { import {
getSyncWithDefault, getSyncWithDefault,
updateSync, updateSync,
getSettingWithDefault, getSettingWithDefault,
getRulesWithDefault, getRulesWithDefault,
getWordsWithDefault,
setSetting, setSetting,
setRules, setRules,
setWords,
} from "./storage"; } from "./storage";
import { apiSyncData } from "../apis"; import { apiSyncData } from "../apis";
import { sha256 } from "./utils"; import { sha256, removeEndchar } from "./utils";
import { createClient, getPatcher } from "webdav";
import { fetchApi } from "./fetch";
getPatcher().patch("request", (opts) => {
return fetchApi({
input: opts.url,
init: { method: opts.method, headers: opts.headers, body: opts.data },
});
});
const syncByWebdav = async (data, { syncUrl, syncUser, syncKey }) => {
const client = createClient(syncUrl, {
username: syncUser,
password: syncKey,
});
const pathname = `/${APP_LCNAME}`;
const filename = `/${APP_LCNAME}/${data.key}`;
if ((await client.exists(pathname)) === false) {
await client.createDirectory(pathname);
}
const isExist = await client.exists(filename);
if (isExist) {
const cont = await client.getFileContents(filename, { format: "text" });
const webData = JSON.parse(cont);
if (webData.updateAt >= data.updateAt) {
return webData;
}
}
await client.putFileContents(filename, JSON.stringify(data, null, 2));
return data;
};
const syncByWorker = async (data, { syncUrl, syncKey }) => {
syncUrl = removeEndchar(syncUrl, "/");
return await apiSyncData(`${syncUrl}/sync`, syncKey, data);
};
const syncData = async (key, valueFn) => {
const {
syncType,
syncUrl,
syncUser,
syncKey,
syncMeta = {},
} = await getSyncWithDefault();
if (!syncUrl || !syncKey || (syncType === OPT_SYNCTYPE_WEBDAV && !syncUser)) {
return;
}
let { updateAt = 0, syncAt = 0 } = syncMeta[key] || {};
syncAt === 0 && (updateAt = 0);
const value = await valueFn();
const data = {
key,
value: JSON.stringify(value),
updateAt,
};
const args = {
syncUrl,
syncUser,
syncKey,
};
const res =
syncType === OPT_SYNCTYPE_WEBDAV
? await syncByWebdav(data, args)
: await syncByWorker(data, args);
syncMeta[key] = {
updateAt: res.updateAt,
syncAt: Date.now(),
};
await updateSync({ syncMeta });
return { value: JSON.parse(res.value), isNew: res.updateAt > updateAt };
};
/** /**
* 同步设置 * 同步设置
* @returns * @returns
*/ */
const syncSetting = async (isBg = false) => { const syncSetting = async () => {
const { syncUrl, syncKey, settingUpdateAt } = await getSyncWithDefault(); const res = await syncData(KV_SETTING_KEY, getSettingWithDefault);
if (!syncUrl || !syncKey) { if (res?.isNew) {
return;
}
const setting = await getSettingWithDefault();
const res = await apiSyncData(
syncUrl,
syncKey,
{
key: KV_SETTING_KEY,
value: setting,
updateAt: settingUpdateAt,
},
isBg
);
if (res && res.updateAt > settingUpdateAt) {
await updateSync({
settingUpdateAt: res.updateAt,
settingSyncAt: res.updateAt,
});
await setSetting(res.value); await setSetting(res.value);
} else {
await updateSync({ settingSyncAt: res.updateAt });
} }
}; };
export const trySyncSetting = async (isBg = false) => { export const trySyncSetting = async () => {
try { try {
await syncSetting(isBg); await syncSetting();
} catch (err) { } catch (err) {
console.log("[sync setting]", err); console.log("[sync setting]", err);
} }
@@ -60,56 +123,58 @@ export const trySyncSetting = async (isBg = false) => {
* 同步规则 * 同步规则
* @returns * @returns
*/ */
const syncRules = async (isBg = false) => { const syncRules = async () => {
const { syncUrl, syncKey, rulesUpdateAt } = await getSyncWithDefault(); const res = await syncData(KV_RULES_KEY, getRulesWithDefault);
if (!syncUrl || !syncKey) { if (res?.isNew) {
return;
}
const rules = await getRulesWithDefault();
const res = await apiSyncData(
syncUrl,
syncKey,
{
key: KV_RULES_KEY,
value: rules,
updateAt: rulesUpdateAt,
},
isBg
);
if (res && res.updateAt > rulesUpdateAt) {
await updateSync({
rulesUpdateAt: res.updateAt,
rulesSyncAt: res.updateAt,
});
await setRules(res.value); await setRules(res.value);
} else {
await updateSync({ rulesSyncAt: res.updateAt });
} }
}; };
export const trySyncRules = async (isBg = false) => { export const trySyncRules = async () => {
try { try {
await syncRules(isBg); await syncRules();
} catch (err) { } catch (err) {
console.log("[sync user rules]", err); console.log("[sync user rules]", err);
} }
}; };
/**
* 同步词汇
* @returns
*/
const syncWords = async () => {
const res = await syncData(KV_WORDS_KEY, getWordsWithDefault);
if (res?.isNew) {
await setWords(res.value);
}
};
export const trySyncWords = async () => {
try {
await syncWords();
} catch (err) {
console.log("[sync fav words]", err);
}
};
/** /**
* 同步分享规则 * 同步分享规则
* @param {*} param0 * @param {*} param0
* @returns * @returns
*/ */
export const syncShareRules = async ({ rules, syncUrl, syncKey }) => { export const syncShareRules = async ({ rules, syncUrl, syncKey }) => {
await apiSyncData(syncUrl, syncKey, { const data = {
key: KV_RULES_SHARE_KEY, key: KV_RULES_SHARE_KEY,
value: rules, value: JSON.stringify(rules, null, 2),
updateAt: Date.now(), updateAt: Date.now(),
}); };
const args = {
syncUrl,
syncKey,
};
await syncByWorker(data, args);
const psk = await sha256(syncKey, KV_SALT_SHARE); const psk = await sha256(syncKey, KV_SALT_SHARE);
const shareUrl = `${syncUrl}?psk=${psk}`; const shareUrl = `${syncUrl}/rules?psk=${psk}`;
return shareUrl; return shareUrl;
}; };
@@ -117,12 +182,14 @@ export const syncShareRules = async ({ rules, syncUrl, syncKey }) => {
* 同步个人设置和规则 * 同步个人设置和规则
* @returns * @returns
*/ */
export const syncSettingAndRules = async (isBg = false) => { export const syncSettingAndRules = async () => {
await syncSetting(isBg); await syncSetting();
await syncRules(isBg); await syncRules();
await syncWords();
}; };
export const trySyncSettingAndRules = async (isBg = false) => { export const trySyncSettingAndRules = async () => {
await trySyncSetting(isBg); await trySyncSetting();
await trySyncRules(isBg); await trySyncRules();
await trySyncWords();
}; };

View File

@@ -3,21 +3,107 @@ import {
APP_LCNAME, APP_LCNAME,
TRANS_MIN_LENGTH, TRANS_MIN_LENGTH,
TRANS_MAX_LENGTH, TRANS_MAX_LENGTH,
EVENT_KISS,
MSG_TRANS_CURRULE, MSG_TRANS_CURRULE,
OPT_STYLE_DASHLINE, OPT_STYLE_DASHLINE,
OPT_STYLE_FUZZY, OPT_STYLE_FUZZY,
SHADOW_KEY, SHADOW_KEY,
OPT_MOUSEKEY_DISABLE,
OPT_MOUSEKEY_MOUSEOVER,
DEFAULT_INPUT_RULE,
DEFAULT_TRANS_APIS,
DEFAULT_INPUT_SHORTCUT,
OPT_LANGS_LIST,
} from "../config"; } from "../config";
import Content from "../views/Content"; import Content from "../views/Content";
import { updateFetchPool, clearFetchPool } from "./fetch"; import { updateFetchPool, clearFetchPool } from "./fetch";
import { debounce } from "./utils"; import {
debounce,
genEventName,
removeEndchar,
matchInputStr,
sleep,
} from "./utils";
import { stepShortcutRegister } from "./shortcut";
import { apiTranslate } from "../apis";
import { tryDetectLang } from ".";
import { loadingSvg } from "./svg";
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 class Translator { export class Translator {
_rule = {}; _rule = {};
_inputRule = {};
_setting = {}; _setting = {};
_rootNodes = new Set(); _rootNodes = new Set();
_tranNodes = new Map(); _tranNodes = new Map();
@@ -37,6 +123,8 @@ export class Translator {
"script", "script",
"iframe", "iframe",
]; ];
_eventName = genEventName();
_mouseoverNode = null;
// 显示 // 显示
_interseObserver = new IntersectionObserver( _interseObserver = new IntersectionObserver(
@@ -99,12 +187,21 @@ export class 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() {
return this._setting; return this._setting;
} }
get eventName() {
return this._eventName;
}
get rule() { get rule() {
// console.log("get rule", this._rule); // console.log("get rule", this._rule);
return this._rule; return this._rule;
@@ -115,8 +212,9 @@ export class Translator {
this._rule = rule; this._rule = rule;
// 广播消息 // 广播消息
const eventName = this._eventName;
window.dispatchEvent( window.dispatchEvent(
new CustomEvent(EVENT_KISS, { new CustomEvent(eventName, {
detail: { detail: {
action: MSG_TRANS_CURRULE, action: MSG_TRANS_CURRULE,
args: rule, args: rule,
@@ -162,6 +260,22 @@ export class Translator {
); );
}; };
_queryShadowNodes = (selector, rootNode) => {
this._rootNodes.add(rootNode);
this._queryFilter(selector, rootNode).forEach((item) => {
if (!this._tranNodes.has(item)) {
this._tranNodes.set(item, "");
}
});
Array.from(rootNode.querySelectorAll("*"))
.map((item) => item.shadowRoot)
.filter(Boolean)
.forEach((item) => {
this._queryShadowNodes(selector, item);
});
};
_queryNodes = (rootNode = document) => { _queryNodes = (rootNode = document) => {
// const childRoots = Array.from(rootNode.querySelectorAll("*")) // const childRoots = Array.from(rootNode.querySelectorAll("*"))
// .map((item) => item.shadowRoot) // .map((item) => item.shadowRoot)
@@ -184,14 +298,15 @@ export class Translator {
const outNodes = this._querySelectorAll(outSelector, rootNode); const outNodes = this._querySelectorAll(outSelector, rootNode);
outNodes.forEach((outNode) => { outNodes.forEach((outNode) => {
if (outNode.shadowRoot) { if (outNode.shadowRoot) {
this._rootNodes.add(outNode.shadowRoot); // this._rootNodes.add(outNode.shadowRoot);
this._queryFilter(inSelector, outNode.shadowRoot).forEach( // this._queryFilter(inSelector, outNode.shadowRoot).forEach(
(item) => { // (item) => {
if (!this._tranNodes.has(item)) { // if (!this._tranNodes.has(item)) {
this._tranNodes.set(item, ""); // this._tranNodes.set(item, "");
} // }
} // }
); // );
this._queryShadowNodes(inSelector, outNode.shadowRoot);
} }
}); });
} }
@@ -206,6 +321,10 @@ export class Translator {
}; };
_register = () => { _register = () => {
if (this._rule.fromLang === this._rule.toLang) {
return;
}
// 搜索节点 // 搜索节点
this._queryNodes(); this._queryNodes();
@@ -218,10 +337,182 @@ export class Translator {
}); });
}); });
this._tranNodes.forEach((_, node) => { if (
!this._setting.mouseKey ||
this._setting.mouseKey === OPT_MOUSEKEY_DISABLE
) {
// 监听节点显示 // 监听节点显示
this._interseObserver.observe(node); this._tranNodes.forEach((_, node) => {
}); this._interseObserver.observe(node);
});
} else {
// 监听鼠标悬停
window.addEventListener("keydown", this._handleKeydown);
this._tranNodes.forEach((_, node) => {
node.addEventListener("mouseover", this._handleMouseover);
node.addEventListener("mouseout", this._handleMouseout);
});
}
};
_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) => {
// console.log("mouseover", e);
if (!this._tranNodes.has(e.target)) {
return;
}
const key = this._setting.mouseKey.slice(3);
if (this._setting.mouseKey === OPT_MOUSEKEY_MOUSEOVER || e[key]) {
e.target.removeEventListener("mouseover", this._handleMouseover);
e.target.removeEventListener("mouseout", this._handleMouseout);
this._render(e.target);
} else {
this._mouseoverNode = e.target;
}
};
_handleMouseout = (e) => {
// console.log("mouseout", e);
if (!this._tranNodes.has(e.target)) {
return;
}
this._mouseoverNode = null;
};
_handleKeydown = (e) => {
// console.log("keydown", e);
const key = this._setting.mouseKey.slice(3);
if (e[key] && this._mouseoverNode) {
this._mouseoverNode.removeEventListener(
"mouseover",
this._handleMouseover
);
this._mouseoverNode.removeEventListener("mouseout", this._handleMouseout);
const node = this._mouseoverNode;
this._render(node);
this._mouseoverNode = null;
}
}; };
_unRegister = () => { _unRegister = () => {
@@ -229,12 +520,28 @@ export class Translator {
this._mutaObserver.disconnect(); this._mutaObserver.disconnect();
// 解除节点显示监听 // 解除节点显示监听
this._interseObserver.disconnect(); // this._interseObserver.disconnect();
// 移除已插入元素 if (
this._tranNodes.forEach((_, node) => { !this._setting.mouseKey ||
node.querySelector(APP_LCNAME)?.remove(); this._setting.mouseKey === OPT_MOUSEKEY_DISABLE
}); ) {
// 解除节点显示监听
this._tranNodes.forEach((_, node) => {
this._interseObserver.unobserve(node);
// 移除已插入元素
node.querySelector(APP_LCNAME)?.remove();
});
} else {
// 移除鼠标悬停监听
window.removeEventListener("keydown", this._handleKeydown);
this._tranNodes.forEach((_, node) => {
node.removeEventListener("mouseover", this._handleMouseover);
node.removeEventListener("mouseout", this._handleMouseout);
// 移除已插入元素
node.querySelector(APP_LCNAME)?.remove();
});
}
// 清空节点集合 // 清空节点集合
this._rootNodes.clear(); this._rootNodes.clear();

View File

@@ -48,15 +48,61 @@ export const sleep = (delay) =>
* @returns * @returns
*/ */
export const debounce = (func, delay = 200) => { export const debounce = (func, delay = 200) => {
let timer; let timer = null;
return (...args) => { return (...args) => {
timer && clearTimeout(timer); timer && clearTimeout(timer);
timer = setTimeout(() => { timer = setTimeout(() => {
func(...args); func(...args);
clearTimeout(timer);
timer = null;
}, delay); }, delay);
}; };
}; };
/**
* 节流函数
* @param {*} func
* @param {*} delay
* @returns
*/
export const throttle = (func, delay = 200) => {
let timer = null;
let cache = null;
return (...args) => {
if (!timer) {
func(...args);
cache = null;
timer = setTimeout(() => {
if (cache) {
func(...cache);
cache = null;
}
clearTimeout(timer);
timer = null;
}, delay);
} else {
cache = args;
}
};
};
/**
* 判断字符串全是某个字符
* @param {*} s
* @param {*} c
* @param {*} i
* @returns
*/
export const isAllchar = (s, c, i = 0) => {
while (i < s.length) {
if (s[i] !== c) {
return false;
}
i++;
}
return true;
};
/** /**
* 字符串通配符(*)匹配 * 字符串通配符(*)匹配
* @param {*} s * @param {*} s
@@ -68,7 +114,7 @@ export const isMatch = (s, p) => {
return false; return false;
} }
p = `*${p}*`; p = "*" + p + "*";
let [sIndex, pIndex] = [0, 0]; let [sIndex, pIndex] = [0, 0];
let [sRecord, pRecord] = [-1, -1]; let [sRecord, pRecord] = [-1, -1];
@@ -91,7 +137,7 @@ export const isMatch = (s, p) => {
return true; return true;
} }
return p.slice(pIndex).replaceAll("*", "") === ""; return isAllchar(p, "*", pIndex);
}; };
/** /**
@@ -116,3 +162,74 @@ export const sha256 = async (text, salt) => {
.map((b) => b.toString(16).padStart(2, "0")) .map((b) => b.toString(16).padStart(2, "0"))
.join(""); .join("");
}; };
/**
* 生成随机事件名称
* @returns
*/
export const genEventName = () => btoa(Math.random()).slice(3, 11);
/**
* 判断两个 Set 是否相同
* @param {*} a
* @param {*} b
* @returns
*/
export const isSameSet = (a, b) => {
const s = new Set([...a, ...b]);
return s.size === a.size && s.size === b.size;
};
/**
* 去掉字符串末尾某个字符
* @param {*} s
* @param {*} c
* @param {*} count
* @returns
*/
export const removeEndchar = (s, c, count = 1) => {
let i = s.length;
while (i > s.length - count && s[i - 1] === c) {
i--;
}
return s.slice(0, i);
};
/**
* 匹配字符串及语言标识
* @param {*} str
* @param {*} sign
* @returns
*/
export const matchInputStr = (str, sign) => {
let reg = /\/([\w-]+)\s+([^]+)/;
switch (sign) {
case "//":
reg = /\/\/([\w-]+)\s+([^]+)/;
break;
case "\\":
reg = /\\([\w-]+)\s+([^]+)/;
break;
case "\\\\":
reg = /\\\\([\w-]+)\s+([^]+)/;
break;
case ">":
reg = />([\w-]+)\s+([^]+)/;
break;
case ">>":
reg = />>([\w-]+)\s+([^]+)/;
break;
default:
}
return str.match(reg);
};
/**
* 判断是否英文单词
* @param {*} str
* @returns
*/
export const isValidWord = (str) => {
const regex = /^[a-zA-Z-]+$/;
return regex.test(str);
};

222
src/libs/webfix.js Normal file
View File

@@ -0,0 +1,222 @@
import { isMatch } from "./utils";
import { getWebfix, setWebfix } from "./storage";
import { apiFetch } from "../apis";
/**
* 修复程序类型
*/
const FIXER_BR = "br";
const FIXER_BN = "bn";
const FIXER_FONTSIZE = "fontSize";
/**
* 需要修复的站点列表
* - pattern 匹配网址
* - selector 需要修复的选择器
* - rootSelector 需要监听的选择器,可留空
* - fixer 修复函数,可针对不同网址,选用不同修复函数
*/
const DEFAULT_SITES = [
{
pattern: "www.phoronix.com",
selector: ".content",
rootSelector: "",
fixer: FIXER_BR,
},
{
pattern: "t.me/s/",
selector: ".tgme_widget_message_text",
rootSelector: ".tgme_channel_history",
fixer: FIXER_BR,
},
{
pattern: "baidu.com",
selector: "html",
rootSelector: "",
fixer: FIXER_FONTSIZE,
},
{
pattern: "chat.openai.com",
selector: "div[data-testid^=conversation-turn] .items-start > div",
rootSelector: "",
fixer: FIXER_BN,
},
];
/**
* 修复过的标记
*/
const fixedSign = "kissfixed";
/**
* 采用 `br` 换行网站的修复函数
* 目标是将 `br` 替换成 `p`
* @param {*} node
* @returns
*/
function brFixer(node) {
if (node.hasAttribute(fixedSign)) {
return;
}
node.setAttribute(fixedSign, "true");
var gapTags = ["BR", "WBR"];
var newlineTags = [
"DIV",
"UL",
"OL",
"LI",
"H1",
"H2",
"H3",
"H4",
"H5",
"H6",
"P",
"HR",
"PRE",
"TABLE",
];
var html = "";
node.childNodes.forEach(function (child, index) {
if (index === 0) {
html += "<p>";
}
if (gapTags.indexOf(child.nodeName) !== -1) {
html += "</p><p>";
} else if (newlineTags.indexOf(child.nodeName) !== -1) {
html += "</p>" + child.outerHTML + "<p>";
} else if (child.outerHTML) {
html += child.outerHTML;
} else if (child.nodeValue) {
html += child.nodeValue;
}
if (index === node.childNodes.length - 1) {
html += "</p>";
}
});
node.innerHTML = html;
}
/**
* 目标是将 `\n` 替换成 `p`
* @param {*} node
* @returns
*/
function bnFixer(node) {
if (node.hasAttribute(fixedSign)) {
return;
}
node.setAttribute(fixedSign, "true");
const childs = node.childNodes;
if (childs.length === 1 && childs[0].nodeName === "#text") {
node.innerHTML = node.innerHTML
.split("\n")
.map((item) => `<p>${item || "&nbsp;"}</p>`)
.join("");
}
}
/**
* 修复字体大小问题,如 baidu.com
* @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 {*} fixer
* @param {*} rootSelector
*/
function run(selector, fixer, rootSelector) {
var mutaObserver = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
mutation.addedNodes.forEach(function (addNode) {
addNode.querySelectorAll(selector).forEach(fixer);
});
});
});
var rootNodes = [document];
if (rootSelector) {
rootNodes = document.querySelectorAll(rootSelector);
}
rootNodes.forEach(function (rootNode) {
rootNode.querySelectorAll(selector).forEach(fixer);
mutaObserver.observe(rootNode, {
childList: true,
subtree: true,
});
});
}
/**
* 同步远程数据
* @param {*} url
* @returns
*/
export const syncWebfix = async (url) => {
const sites = await apiFetch(url);
await setWebfix(url, sites);
return sites;
};
/**
* 从缓存或远程加载修复站点
* @param {*} url
* @returns
*/
export const loadOrFetchWebfix = async (url) => {
try {
let sites = await getWebfix(url);
if (sites?.length) {
return sites;
}
return syncWebfix(url);
} catch (err) {
console.log("[load webfix]", err.message);
return DEFAULT_SITES;
}
};
/**
* 匹配站点
*/
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) {
console.error(`[kiss-webfix]: ${err.message}`);
}
}

View File

@@ -1,126 +1,77 @@
import React from "react"; import { getSettingWithDefault } from "./libs/storage";
import ReactDOM from "react-dom/client";
import Action from "./views/Action";
import createCache from "@emotion/cache";
import { CacheProvider } from "@emotion/react";
import {
getSettingWithDefault,
getRulesWithDefault,
getFabWithDefault,
} from "./libs/storage";
import { Translator } from "./libs/translator";
import { trySyncAllSubRules } from "./libs/subRules"; import { trySyncAllSubRules } from "./libs/subRules";
import { isGm } from "./libs/client";
import { MSG_TRANS_TOGGLE, MSG_TRANS_PUTRULE } from "./config";
import { isIframe } from "./libs/iframe"; import { isIframe } from "./libs/iframe";
import { handlePing, injectScript } from "./libs/gm"; import { handlePing, injectScript } from "./libs/gm";
import { matchRule } from "./libs/rules"; import { genEventName } from "./libs/utils";
import { runWebfix } from "./libs/webfix";
import {
runIframe,
runTranslator,
showFab,
showTransbox,
windowListener,
showErr,
} from "./common";
function runSettingPage() {
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);
}
}
/** /**
* 入口函数 * 入口函数
*/ */
const init = async () => {
// 设置页面
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)
) {
if (GM?.info?.script?.grant?.includes("unsafeWindow")) {
unsafeWindow.GM = GM;
unsafeWindow.APP_NAME = process.env.REACT_APP_NAME;
} else {
const ping = btoa(Math.random()).slice(3, 11);
window.addEventListener(ping, handlePing);
// window.eval(`(${injectScript})("${ping}")`); // eslint-disable-line
const script = document.createElement("script");
script.textContent = `(${injectScript})("${ping}")`;
document.head.append(script);
}
return;
}
// 翻译页面
const href = isIframe ? document.referrer : document.location.href;
const setting = await getSettingWithDefault();
const rules = await getRulesWithDefault();
const rule = await matchRule(rules, href, setting);
const translator = new Translator(rule, setting);
if (isIframe) {
// iframe
window.addEventListener("message", (e) => {
const action = e?.data?.action;
switch (action) {
case MSG_TRANS_TOGGLE:
translator.toggle();
break;
case MSG_TRANS_PUTRULE:
translator.updateRule(e.data.args || {});
break;
default:
}
});
return;
}
// 浮球按钮
const fab = await getFabWithDefault();
const $action = document.createElement("div");
$action.setAttribute("id", "kiss-translator");
document.body.parentElement.appendChild($action);
const shadowContainer = $action.attachShadow({ mode: "closed" });
const emotionRoot = document.createElement("style");
const shadowRootElement = document.createElement("div");
shadowContainer.appendChild(emotionRoot);
shadowContainer.appendChild(shadowRootElement);
const cache = createCache({
key: "css",
prepend: true,
container: emotionRoot,
});
ReactDOM.createRoot(shadowRootElement).render(
<React.StrictMode>
<CacheProvider value={cache}>
<Action translator={translator} fab={fab} />
</CacheProvider>
</React.StrictMode>
);
// 注册菜单
if (isGm) {
try {
GM.registerMenuCommand(
"Toggle Translate",
(event) => {
translator.toggle();
},
"Q"
);
GM.registerMenuCommand(
"Toggle Style",
(event) => {
translator.toggleStyle();
},
"C"
);
} catch (err) {
console.log("[registerMenuCommand]", err);
}
}
// 同步订阅规则
trySyncAllSubRules(setting);
};
(async () => { (async () => {
try { try {
await init(); // 设置页面
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) { } catch (err) {
const $err = document.createElement("div"); showErr(err);
$err.innerText = `KISS-Translator: ${err.message}`;
$err.style.cssText = "background:red; color:#fff; z-index:10000;";
document.body.prepend($err);
} }
})(); })();

View File

@@ -1,65 +1,51 @@
import { useCallback, useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { limitNumber } from "../../libs/utils"; import { limitNumber } from "../../libs/utils";
import { isMobile } from "../../libs/mobile"; import { isMobile } from "../../libs/mobile";
import { setFab } from "../../libs/storage"; import { updateFab } from "../../libs/storage";
import { debounce } from "../../libs/utils";
import Paper from "@mui/material/Paper";
const getEdgePosition = ( const getEdgePosition = ({
{ x: left, y: top, edge }, x: left,
y: top,
width,
height,
windowWidth, windowWidth,
windowHeight, windowHeight,
width, hover,
height }) => {
) => {
const right = windowWidth - left - width; const right = windowWidth - left - width;
const bottom = windowHeight - top - height; const bottom = windowHeight - top - height;
const min = Math.min(left, top, right, bottom); const min = Math.min(left, top, right, bottom);
switch (min) { switch (min) {
case right: case right:
edge = "right"; left = hover ? windowWidth - width : windowWidth - width / 2;
left = windowWidth - width;
break; break;
case left: case left:
edge = "left"; left = hover ? 0 : -width / 2;
left = 0;
break; break;
case bottom: case bottom:
edge = "bottom"; top = hover ? windowHeight - height : windowHeight - height / 2;
top = windowHeight - height;
break; break;
default: default:
edge = "top"; top = hover ? 0 : -height / 2;
top = 0;
} }
left = limitNumber(left, 0, windowWidth - width); return { x: left, y: top };
top = limitNumber(top, 0, windowHeight - height);
return { x: left, y: top, edge, hide: false };
}; };
const getHidePosition = ( function DraggableWrapper({ children, usePaper, ...props }) {
{ x: left, y: top, edge }, if (usePaper) {
windowWidth, return (
windowHeight, <Paper {...props} elevation={4}>
width, {children}
height </Paper>
) => { );
switch (edge) {
case "right":
left = windowWidth - width / 2;
break;
case "left":
left = -width / 2;
break;
case "bottom":
top = windowHeight - height / 2;
break;
default:
top = -height / 2;
} }
return { x: left, y: top, edge, hide: true }; return <div {...props}>{children}</div>;
}; }
export default function Draggable({ export default function Draggable({
windowSize, windowSize: { w: windowWidth, h: windowHeight },
width, width,
height, height,
left, left,
@@ -70,66 +56,38 @@ export default function Draggable({
onMove, onMove,
handler, handler,
children, children,
usePaper,
}) { }) {
const [origin, setOrigin] = useState({ const [hover, setHover] = useState(false);
x: left, const [origin, setOrigin] = useState(null);
y: top, const [position, setPosition] = useState({ x: left, y: top });
px: left, const setFabPosition = useMemo(() => debounce(updateFab, 500), []);
py: top,
});
const [position, setPosition] = useState({
x: left,
y: top,
edge: null,
hide: false,
});
const [edgeTimer, setEdgeTimer] = useState(null);
const goEdge = useCallback((w, h, width, height) => {
setPosition((pre) => getEdgePosition(pre, w, h, width, height));
setEdgeTimer(
setTimeout(() => {
setPosition((pre) => getHidePosition(pre, w, h, width, height));
}, 1500)
);
}, []);
const handlePointerDown = (e) => { const handlePointerDown = (e) => {
!isMobile && e.target.setPointerCapture(e.pointerId); !isMobile && e.target.setPointerCapture(e.pointerId);
onStart && onStart(); onStart && onStart();
edgeTimer && clearTimeout(edgeTimer); const { x, y } = position;
const { clientX, clientY } = isMobile ? e.targetTouches[0] : e; const { clientX, clientY } = isMobile ? e.targetTouches[0] : e;
setOrigin({ setOrigin({ x, y, clientX, clientY });
x: position.x,
y: position.y,
px: clientX,
py: clientY,
});
}; };
const handlePointerMove = (e) => { const handlePointerMove = (e) => {
onMove && onMove(); onMove && onMove();
const { clientX, clientY } = isMobile ? e.targetTouches[0] : e; const { clientX, clientY } = isMobile ? e.targetTouches[0] : e;
if (origin) { if (origin) {
const dx = clientX - origin.px; const dx = clientX - origin.clientX;
const dy = clientY - origin.py; const dy = clientY - origin.clientY;
let x = origin.x + dx; let x = origin.x + dx;
let y = origin.y + dy; let y = origin.y + dy;
const { w, h } = windowSize; x = limitNumber(x, -width / 2, windowWidth - width / 2);
x = limitNumber(x, 0, w - width); y = limitNumber(y, 0, windowHeight - height / 2);
y = limitNumber(y, 0, h - height); setPosition({ x, y });
setPosition({ x, y, edge: null, hide: false });
} }
}; };
const handlePointerUp = (e) => { const handlePointerUp = (e) => {
e.stopPropagation(); e.stopPropagation();
setOrigin(null); setOrigin(null);
if (!snapEdge) {
return;
}
goEdge(windowSize.w, windowSize.h, width, height);
}; };
const handleClick = (e) => { const handleClick = (e) => {
@@ -138,35 +96,48 @@ export default function Draggable({
const handleMouseEnter = (e) => { const handleMouseEnter = (e) => {
e.stopPropagation(); e.stopPropagation();
if (snapEdge && position.hide) { setHover(true);
edgeTimer && clearTimeout(edgeTimer); };
goEdge(windowSize.w, windowSize.h, width, height);
} const handleMouseLeave = (e) => {
e.stopPropagation();
setHover(false);
}; };
useEffect(() => { useEffect(() => {
setOrigin(null); if (!snapEdge || !!origin) {
if (!snapEdge) {
return; return;
} }
goEdge(windowSize.w, windowSize.h, width, height);
}, [snapEdge, goEdge, windowSize.w, windowSize.h, width, height]);
useEffect(() => { setPosition((pre) => {
if (position.hide) { const edgePosition = getEdgePosition({
setFab({ ...pre,
x: position.x, width,
y: position.y, height,
windowWidth,
windowHeight,
hover,
}); });
} setFabPosition(edgePosition);
}, [position.x, position.y, position.hide]); return edgePosition;
});
}, [
origin,
hover,
width,
height,
windowWidth,
windowHeight,
snapEdge,
setFabPosition,
]);
const opacity = useMemo(() => { const opacity = useMemo(() => {
if (snapEdge) { if (snapEdge) {
return position.hide ? 0.2 : 1; return hover || origin ? 1 : 0.2;
} }
return origin ? 0.8 : 1; return origin ? 0.8 : 1;
}, [origin, snapEdge, position.hide]); }, [origin, snapEdge, hover]);
const touchProps = isMobile const touchProps = isMobile
? { ? {
@@ -181,7 +152,8 @@ export default function Draggable({
}; };
return ( return (
<div <DraggableWrapper
usePaper={usePaper}
style={{ style={{
opacity, opacity,
position: "fixed", position: "fixed",
@@ -191,6 +163,7 @@ export default function Draggable({
display: show ? "block" : "none", display: show ? "block" : "none",
}} }}
onMouseEnter={handleMouseEnter} onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onClick={handleClick} onClick={handleClick}
> >
<div <div
@@ -202,6 +175,6 @@ export default function Draggable({
{handler} {handler}
</div> </div>
<div>{children}</div> <div>{children}</div>
</div> </DraggableWrapper>
); );
} }

View File

@@ -1,23 +1,33 @@
import Paper from "@mui/material/Paper";
import Box from "@mui/material/Box";
import Fab from "@mui/material/Fab"; import Fab from "@mui/material/Fab";
import TranslateIcon from "@mui/icons-material/Translate"; import TranslateIcon from "@mui/icons-material/Translate";
import ThemeProvider from "../../hooks/Theme"; import ThemeProvider from "../../hooks/Theme";
import Draggable from "./Draggable"; import Draggable from "./Draggable";
import IconButton from "@mui/material/IconButton";
import CloseIcon from "@mui/icons-material/Close";
import Stack from "@mui/material/Stack";
import { useEffect, useState, useMemo, useCallback } from "react"; import { useEffect, useState, useMemo, useCallback } from "react";
import { SettingProvider } from "../../hooks/Setting"; import { SettingProvider } from "../../hooks/Setting";
import Popup from "../Popup"; import Popup from "../Popup";
import { debounce } from "../../libs/utils"; import { debounce } from "../../libs/utils";
import { isGm } from "../../libs/client";
import Header from "../Popup/Header";
import Box from "@mui/material/Box";
import Divider from "@mui/material/Divider";
import {
DEFAULT_SHORTCUTS,
OPT_SHORTCUT_TRANSLATE,
OPT_SHORTCUT_STYLE,
OPT_SHORTCUT_POPUP,
OPT_SHORTCUT_SETTING,
MSG_TRANS_TOGGLE,
MSG_TRANS_TOGGLE_STYLE,
} from "../../config";
import { shortcutRegister } from "../../libs/shortcut";
import { sendIframeMsg } from "../../libs/iframe";
export default function Action({ translator, fab }) { export default function Action({ translator, fab }) {
const fabWidth = 40; const fabWidth = 40;
const [showPopup, setShowPopup] = useState(false); const [showPopup, setShowPopup] = useState(false);
const [windowSize, setWindowSize] = useState({ const [windowSize, setWindowSize] = useState({
w: document.documentElement.clientWidth, w: window.innerWidth,
h: document.documentElement.clientHeight, h: window.innerHeight,
}); });
const [moved, setMoved] = useState(false); const [moved, setMoved] = useState(false);
@@ -25,8 +35,8 @@ export default function Action({ translator, fab }) {
() => () =>
debounce(() => { debounce(() => {
setWindowSize({ setWindowSize({
w: document.documentElement.clientWidth, w: window.innerWidth,
h: document.documentElement.clientHeight, h: window.innerHeight,
}); });
}), }),
[] []
@@ -44,6 +54,92 @@ export default function Action({ translator, fab }) {
setMoved(true); setMoved(true);
}, []); }, []);
useEffect(() => {
if (!isGm) {
return;
}
// 注册快捷键
const shortcuts = translator.setting.shortcuts || DEFAULT_SHORTCUTS;
const clearShortcuts = [
shortcutRegister(shortcuts[OPT_SHORTCUT_TRANSLATE], () => {
translator.toggle();
sendIframeMsg(MSG_TRANS_TOGGLE);
setShowPopup(false);
}),
shortcutRegister(shortcuts[OPT_SHORTCUT_STYLE], () => {
translator.toggleStyle();
sendIframeMsg(MSG_TRANS_TOGGLE_STYLE);
setShowPopup(false);
}),
shortcutRegister(shortcuts[OPT_SHORTCUT_POPUP], () => {
setShowPopup((pre) => !pre);
}),
shortcutRegister(shortcuts[OPT_SHORTCUT_SETTING], () => {
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank");
}),
];
return () => {
clearShortcuts.forEach((fn) => {
fn();
});
};
}, [translator]);
useEffect(() => {
if (!isGm) {
return;
}
// 注册菜单
try {
const menuCommandIds = [];
menuCommandIds.push(
GM.registerMenuCommand(
"Toggle Translate (Alt+q)",
(event) => {
translator.toggle();
sendIframeMsg(MSG_TRANS_TOGGLE);
setShowPopup(false);
},
"Q"
),
GM.registerMenuCommand(
"Toggle Style (Alt+c)",
(event) => {
translator.toggleStyle();
sendIframeMsg(MSG_TRANS_TOGGLE_STYLE);
setShowPopup(false);
},
"C"
),
GM.registerMenuCommand(
"Open Menu (Alt+k)",
(event) => {
setShowPopup((pre) => !pre);
},
"K"
),
GM.registerMenuCommand(
"Open Setting (Alt+o)",
(event) => {
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank");
},
"O"
)
);
return () => {
menuCommandIds.forEach((id) => {
GM.unregisterMenuCommand(id);
});
};
} catch (err) {
console.log("[registerMenuCommand]", err);
}
}, [translator]);
useEffect(() => { useEffect(() => {
window.addEventListener("resize", handleWindowResize); window.addEventListener("resize", handleWindowResize);
return () => { return () => {
@@ -53,6 +149,7 @@ export default function Action({ translator, fab }) {
useEffect(() => { useEffect(() => {
window.addEventListener("click", handleWindowClick); window.addEventListener("click", handleWindowClick);
return () => { return () => {
window.removeEventListener("click", handleWindowClick); window.removeEventListener("click", handleWindowClick);
}; };
@@ -76,7 +173,7 @@ export default function Action({ translator, fab }) {
windowSize, windowSize,
width: fabWidth, width: fabWidth,
height: fabWidth, height: fabWidth,
left: fab.x ?? 0, left: fab.x ?? -fabWidth,
top: fab.y ?? windowSize.h / 2, top: fab.y ?? windowSize.h / 2,
}; };
@@ -89,39 +186,23 @@ export default function Action({ translator, fab }) {
show={showPopup} show={showPopup}
onStart={handleStart} onStart={handleStart}
onMove={handleMove} onMove={handleMove}
usePaper
handler={ handler={
<Paper style={{ cursor: "move" }} elevation={3}> <Box style={{ cursor: "move" }}>
<Stack <Header setShowPopup={setShowPopup} />
direction="row" <Divider />
justifyContent="space-between" </Box>
alignItems="center"
spacing={2}
>
<Box style={{ marginLeft: 16 }}>
{`${process.env.REACT_APP_NAME} v${process.env.REACT_APP_VERSION}`}
</Box>
<IconButton
onClick={() => {
setShowPopup(false);
}}
>
<CloseIcon />
</IconButton>
</Stack>
</Paper>
} }
> >
<Paper> {showPopup && (
{showPopup && ( <Popup setShowPopup={setShowPopup} translator={translator} />
<Popup setShowPopup={setShowPopup} translator={translator} /> )}
)}
</Paper>
</Draggable> </Draggable>
<Draggable <Draggable
key="fab" key="fab"
snapEdge snapEdge
{...fabProps} {...fabProps}
show={!showPopup} show={fab.isHide ? false : !showPopup}
onStart={handleStart} onStart={handleStart}
onMove={handleMove} onMove={handleMove}
handler={ handler={

View File

@@ -1,44 +1,14 @@
import { DEFAULT_COLOR } from "../../config"; import { loadingSvg } from "../../libs/svg";
export default function LoadingIcon() { export default function LoadingIcon() {
return ( return (
<svg <div
viewBox="0 0 100 100"
style={{ style={{
maxWidth: "1.2em", display: "inline-block",
maxHeight: "1.2em", width: "1.2em",
height: "1em",
}} }}
> dangerouslySetInnerHTML={{ __html: loadingSvg }}
<circle fill={DEFAULT_COLOR} stroke="none" cx="6" cy="50" r="6"> />
<animateTransform
attributeName="transform"
dur="1s"
type="translate"
values="0 15 ; 0 -15; 0 15"
repeatCount="indefinite"
begin="0.1"
/>
</circle>
<circle fill={DEFAULT_COLOR} stroke="none" cx="30" cy="50" r="6">
<animateTransform
attributeName="transform"
dur="1s"
type="translate"
values="0 10 ; 0 -10; 0 10"
repeatCount="indefinite"
begin="0.2"
/>
</circle>
<circle fill={DEFAULT_COLOR} stroke="none" cx="54" cy="50" r="6">
<animateTransform
attributeName="transform"
dur="1s"
type="translate"
values="0 5 ; 0 -5; 0 5"
repeatCount="indefinite"
begin="0.3"
/>
</circle>
</svg>
); );
} }

View File

@@ -1,4 +1,4 @@
import { useMemo, useState, useEffect } from "react"; import { useState, useEffect } from "react";
import LoadingIcon from "./LoadingIcon"; import LoadingIcon from "./LoadingIcon";
import { import {
OPT_STYLE_LINE, OPT_STYLE_LINE,
@@ -6,26 +6,99 @@ import {
OPT_STYLE_DASHLINE, OPT_STYLE_DASHLINE,
OPT_STYLE_WAVYLINE, OPT_STYLE_WAVYLINE,
OPT_STYLE_FUZZY, OPT_STYLE_FUZZY,
OPT_STYLE_HIGHTLIGHT, OPT_STYLE_HIGHLIGHT,
OPT_STYLE_DIY,
DEFAULT_COLOR, DEFAULT_COLOR,
EVENT_KISS,
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";
const LineSpan = styled("span")`
opacity: 0.6;
-webkit-opacity: 0.6;
text-decoration-line: underline;
text-decoration-style: ${(props) => props.$lineStyle};
text-decoration-color: ${(props) => props.$lineColor};
text-decoration-thickness: 2px;
text-underline-offset: 0.3em;
-webkit-text-decoration-line: underline;
-webkit-text-decoration-style: ${(props) => props.$lineStyle};
-webkit-text-decoration-color: ${(props) => props.$lineColor};
-webkit-text-decoration-thickness: 2px;
-webkit-text-underline-offset: 0.3em;
&:hover {
opacity: 1;
-webkit-opacity: 1;
}
`;
const FuzzySpan = styled("span")`
filter: blur(0.2em);
-webkit-filter: blur(0.2em);
&:hover {
filter: none;
-webkit-filter: none;
}
`;
const HighlightSpan = styled("span")`
color: #fff;
background-color: ${(props) => props.$bgColor};
`;
const DiySpan = styled("span")`
${(props) => props.$diyStyle}
`;
function StyledSpan({ textStyle, textDiyStyle, bgColor, children }) {
switch (textStyle) {
case OPT_STYLE_LINE: // 下划线
return (
<LineSpan $lineStyle="solid" $lineColor={bgColor}>
{children}
</LineSpan>
);
case OPT_STYLE_DOTLINE: // 点状线
return (
<LineSpan $lineStyle="dotted" $lineColor={bgColor}>
{children}
</LineSpan>
);
case OPT_STYLE_DASHLINE: // 虚线
return (
<LineSpan $lineStyle="dashed" $lineColor={bgColor}>
{children}
</LineSpan>
);
case OPT_STYLE_WAVYLINE: // 波浪线
return (
<LineSpan $lineStyle="wavy" $lineColor={bgColor}>
{children}
</LineSpan>
);
case OPT_STYLE_FUZZY: // 模糊
return <FuzzySpan>{children}</FuzzySpan>;
case OPT_STYLE_HIGHLIGHT: // 高亮
return (
<HighlightSpan $bgColor={bgColor || DEFAULT_COLOR}>
{children}
</HighlightSpan>
);
case OPT_STYLE_DIY: // 自定义
return <DiySpan $diyStyle={textDiyStyle}>{children}</DiySpan>;
default:
return <span>{children}</span>;
}
}
export default function Content({ q, translator }) { export default function Content({ q, translator }) {
const [rule, setRule] = useState(translator.rule); const [rule, setRule] = useState(translator.rule);
const [hover, setHover] = useState(false);
const { text, sameLang, loading } = useTranslate(q, rule, translator.setting); const { text, sameLang, loading } = useTranslate(q, rule, translator.setting);
const { textStyle, bgColor } = rule; const { textStyle, bgColor = "", textDiyStyle = "" } = rule;
const handleMouseEnter = () => { const { newlineLength = TRANS_NEWLINE_LENGTH } = translator.setting;
setHover(true);
};
const handleMouseLeave = () => {
setHover(false);
};
const handleKissEvent = (e) => { const handleKissEvent = (e) => {
const { action, args } = e.detail; const { action, args } = e.detail;
@@ -34,60 +107,20 @@ export default function Content({ q, translator }) {
setRule(args); setRule(args);
break; break;
default: default:
// console.log(`[popup] kissEvent action skip: ${action}`);
} }
}; };
useEffect(() => { useEffect(() => {
window.addEventListener(EVENT_KISS, handleKissEvent); window.addEventListener(translator.eventName, handleKissEvent);
return () => { return () => {
window.removeEventListener(EVENT_KISS, handleKissEvent); window.removeEventListener(translator.eventName, handleKissEvent);
}; };
}, []); }, [translator.eventName]);
const style = useMemo(() => {
const lineColor = bgColor || "";
const underlineStyle = (st) => ({
opacity: hover ? 1 : 0.6,
textDecorationLine: "underline",
textDecorationColor: lineColor,
textDecorationStyle: st,
textDecorationThickness: "2px",
textUnderlineOffset: "0.3em",
WebkittextDecorationLine: "underline",
WebkittextDecorationColor: lineColor,
WebkittextDecorationStyle: st,
WebkittextDecorationThickness: "2px",
WebkittextTextUnderlineOffset: "0.3em",
});
switch (textStyle) {
case OPT_STYLE_LINE: // 下划线
return underlineStyle("solid");
case OPT_STYLE_DOTLINE: // 点状线
return underlineStyle("dotted");
case OPT_STYLE_DASHLINE: // 虚线
return underlineStyle("dashed");
case OPT_STYLE_WAVYLINE: // 波浪线
return underlineStyle("wavy");
case OPT_STYLE_FUZZY: // 模糊
return {
filter: hover ? "none" : "blur(5px)",
transition: "filter 0.2s ease-in-out",
};
case OPT_STYLE_HIGHTLIGHT: // 高亮
return {
color: "#FFF",
backgroundColor: bgColor || DEFAULT_COLOR,
};
default:
return {};
}
}, [textStyle, hover, bgColor]);
if (loading) { if (loading) {
return ( return (
<> <>
{q.length > 40 ? <br /> : " "} {q.length >= newlineLength ? <br /> : " "}
<LoadingIcon /> <LoadingIcon />
</> </>
); );
@@ -96,14 +129,14 @@ export default function Content({ q, translator }) {
if (text && !sameLang) { if (text && !sameLang) {
return ( return (
<> <>
{q.length > 40 ? <br /> : " "} {q.length >= newlineLength ? <br /> : " "}
<span <StyledSpan
style={style} textStyle={textStyle}
onMouseEnter={handleMouseEnter} textDiyStyle={textDiyStyle}
onMouseLeave={handleMouseLeave} bgColor={bgColor}
> >
{text} {text}
</span> </StyledSpan>
</> </>
); );
} }

185
src/views/Options/Apis.js Normal file
View File

@@ -0,0 +1,185 @@
import Stack from "@mui/material/Stack";
import TextField from "@mui/material/TextField";
import Button from "@mui/material/Button";
import CircularProgress from "@mui/material/CircularProgress";
import {
OPT_TRANS_ALL,
OPT_TRANS_MICROSOFT,
OPT_TRANS_DEEPLFREE,
OPT_TRANS_BAIDU,
OPT_TRANS_TENCENT,
OPT_TRANS_OPENAI,
OPT_TRANS_CUSTOMIZE,
URL_KISS_PROXY,
} from "../../config";
import { 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 { useAlert } from "../../hooks/Alert";
import { useApi } from "../../hooks/Api";
import { apiTranslate } from "../../apis";
import Box from "@mui/material/Box";
import Link from "@mui/material/Link";
function TestButton({ translator, api }) {
const i18n = useI18n();
const alert = useAlert();
const [loading, setLoading] = useState(false);
const handleApiTest = async () => {
try {
setLoading(true);
const [text] = await apiTranslate({
translator,
text: "hello world",
fromLang: "en",
toLang: "zh-CN",
apiSetting: api,
useCache: false,
});
if (!text) {
throw new Error("empty reault");
}
alert.success(i18n("test_success"));
} catch (err) {
alert.error(`${i18n("test_failed")}: ${err.message}`);
} finally {
setLoading(false);
}
};
if (loading) {
return <CircularProgress size={16} />;
}
return (
<Button size="small" variant="contained" onClick={handleApiTest}>
{i18n("click_test")}
</Button>
);
}
function ApiFields({ translator }) {
const i18n = useI18n();
const { api, updateApi, resetApi } = useApi(translator);
const { url = "", key = "", model = "", prompt = "" } = api;
const handleChange = (e) => {
const { name, value } = e.target;
updateApi({
[name]: value,
});
};
const buildinTranslators = [
OPT_TRANS_MICROSOFT,
OPT_TRANS_DEEPLFREE,
OPT_TRANS_BAIDU,
OPT_TRANS_TENCENT,
];
return (
<Stack spacing={3}>
{!buildinTranslators.includes(translator) && (
<>
<TextField
size="small"
label={"URL"}
name="url"
value={url}
onChange={handleChange}
/>
<TextField
size="small"
label={"KEY"}
name="key"
value={key}
onChange={handleChange}
/>
</>
)}
{translator === OPT_TRANS_OPENAI && (
<>
<TextField
size="small"
label={"MODEL"}
name="model"
value={model}
onChange={handleChange}
/>
<TextField
size="small"
label={"PROMPT"}
name="prompt"
value={prompt}
onChange={handleChange}
multiline
/>
</>
)}
<Stack direction="row" spacing={2}>
<TestButton translator={translator} api={api} />
{!buildinTranslators.includes(translator) && (
<Button
size="small"
variant="outlined"
onClick={() => {
resetApi();
}}
>
{i18n("restore_default")}
</Button>
)}
</Stack>
{translator === OPT_TRANS_CUSTOMIZE && (
<pre>{i18n("custom_api_help")}</pre>
)}
</Stack>
);
}
function ApiAccordion({ translator }) {
const [expanded, setExpanded] = useState(false);
const handleChange = (e) => {
setExpanded((pre) => !pre);
};
return (
<Accordion expanded={expanded} onChange={handleChange}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>{translator}</Typography>
</AccordionSummary>
<AccordionDetails>
{expanded && <ApiFields translator={translator} />}
</AccordionDetails>
</Accordion>
);
}
export default function Apis() {
const i18n = useI18n();
return (
<Box>
<Stack spacing={3}>
<Alert severity="info">
<Link href={URL_KISS_PROXY} target="_blank">
{i18n("about_api_proxy")}
</Link>
</Alert>
<Box>
{OPT_TRANS_ALL.map((translator) => (
<ApiAccordion key={translator} translator={translator} />
))}
</Box>
</Stack>
</Box>
);
}

View File

@@ -0,0 +1,13 @@
import IconButton from "@mui/material/IconButton";
import { useDarkMode } from "../../hooks/ColorMode";
import LightModeIcon from "@mui/icons-material/LightMode";
import DarkModeIcon from "@mui/icons-material/DarkMode";
export default function DarkModeButton() {
const { darkMode, toggleDarkMode } = useDarkMode();
return (
<IconButton onClick={toggleDarkMode} color="inherit">
{darkMode ? <LightModeIcon /> : <DarkModeIcon />}
</IconButton>
);
}

View File

@@ -0,0 +1,27 @@
import FileDownloadIcon from "@mui/icons-material/FileDownload";
import Button from "@mui/material/Button";
export default function DownloadButton({ data, text, fileName }) {
const handleClick = (e) => {
e.preventDefault();
if (data) {
const url = window.URL.createObjectURL(new Blob([data]));
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", fileName || `${Date.now()}.json`);
document.body.appendChild(link);
link.click();
link.remove();
}
};
return (
<Button
size="small"
variant="outlined"
onClick={handleClick}
startIcon={<FileDownloadIcon />}
>
{text}
</Button>
);
}

View File

@@ -0,0 +1,150 @@
import Stack from "@mui/material/Stack";
import { OPT_TRANS_BAIDU } from "../../config";
import { useEffect, useState } from "react";
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 CircularProgress from "@mui/material/CircularProgress";
import { useI18n } from "../../hooks/I18n";
import Alert from "@mui/material/Alert";
import { apiTranslate } from "../../apis";
import Box from "@mui/material/Box";
import { useFavWords } from "../../hooks/FavWords";
import DictCont from "../Selection/DictCont";
import DownloadButton from "./DownloadButton";
import UploadButton from "./UploadButton";
import Button from "@mui/material/Button";
import ClearAllIcon from "@mui/icons-material/ClearAll";
import { isValidWord } from "../../libs/utils";
function DictField({ word }) {
const [dictResult, setDictResult] = useState(null);
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 }) {
const [expanded, setExpanded] = useState(false);
const handleChange = (e) => {
setExpanded((pre) => !pre);
};
return (
<Accordion expanded={expanded} onChange={handleChange}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
{/* <Typography>{`[${new Date(
createdAt
).toLocaleString()}] ${word}`}</Typography> */}
<Typography>{`${index + 1}. ${word}`}</Typography>
</AccordionSummary>
<AccordionDetails>
{expanded && <DictField word={word} />}
</AccordionDetails>
</Accordion>
);
}
export default function FavWords() {
const i18n = useI18n();
const { loading, favWords, mergeWords, clearWords } = useFavWords();
const favList = Object.entries(favWords).sort((a, b) =>
a[0].localeCompare(b[0])
);
const downloadList = favList.map(([word]) => word);
const handleImport = async (data) => {
try {
const newWords = data
.split("\n")
.map((line) => line.split(",")[0].trim())
.filter(isValidWord);
await mergeWords(newWords);
} catch (err) {
console.log("[import rules]", err);
}
};
return (
<Box>
<Stack spacing={3}>
<Stack
direction="row"
alignItems="center"
spacing={2}
useFlexGap
flexWrap="wrap"
>
<UploadButton
text={i18n("import")}
handleImport={handleImport}
fileType="text"
fileExts={[".txt", ".csv"]}
/>
<DownloadButton
data={downloadList.join("\n")}
text={i18n("export")}
fileName={`kiss-words_${Date.now()}.txt`}
/>
<Button
size="small"
variant="outlined"
onClick={() => {
clearWords();
}}
startIcon={<ClearAllIcon />}
>
{i18n("clear_all")}
</Button>
</Stack>
<Box>
{loading ? (
<CircularProgress size={24} />
) : (
favList.map(([word, { createdAt }], index) => (
<FavAccordion
key={word}
index={index}
word={word}
createdAt={createdAt}
/>
))
)}
</Box>
</Stack>
</Box>
);
}

View File

@@ -1,19 +1,15 @@
import PropTypes from "prop-types";
import AppBar from "@mui/material/AppBar"; import AppBar from "@mui/material/AppBar";
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
import MenuIcon from "@mui/icons-material/Menu"; import MenuIcon from "@mui/icons-material/Menu";
import Toolbar from "@mui/material/Toolbar"; import Toolbar from "@mui/material/Toolbar";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import { useDarkMode } from "../../hooks/ColorMode";
import LightModeIcon from "@mui/icons-material/LightMode";
import DarkModeIcon from "@mui/icons-material/DarkMode";
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";
function Header(props) { function Header(props) {
const i18n = useI18n(); const i18n = useI18n();
const { onDrawerToggle } = props; const { onDrawerToggle } = props;
const { darkMode, toggleDarkMode } = useDarkMode();
return ( return (
<AppBar <AppBar
@@ -39,18 +35,13 @@ function Header(props) {
underline="none" underline="none"
color="inherit" color="inherit"
href={process.env.REACT_APP_HOMEPAGE} href={process.env.REACT_APP_HOMEPAGE}
target="_blank"
>{`${i18n("app_name")} v${process.env.REACT_APP_VERSION}`}</Link> >{`${i18n("app_name")} v${process.env.REACT_APP_VERSION}`}</Link>
</Box> </Box>
<IconButton onClick={toggleDarkMode} color="inherit"> <DarkModeButton />
{darkMode ? <LightModeIcon /> : <DarkModeIcon />}
</IconButton>
</Toolbar> </Toolbar>
</AppBar> </AppBar>
); );
} }
Header.propTypes = {
onDrawerToggle: PropTypes.func.isRequired,
};
export default Header; export default Header;

View File

@@ -0,0 +1,19 @@
import Button from "@mui/material/Button";
import { useI18n } from "../../hooks/I18n";
import HelpIcon from "@mui/icons-material/Help";
export default function HelpButton({ url }) {
const i18n = useI18n();
return (
<Button
size="small"
variant="outlined"
onClick={() => {
window.open(url, "_blank");
}}
startIcon={<HelpIcon />}
>
{i18n("help")}
</Button>
);
}

View File

@@ -0,0 +1,178 @@
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import TextField from "@mui/material/TextField";
import MenuItem from "@mui/material/MenuItem";
import { useI18n } from "../../hooks/I18n";
import {
OPT_TRANS_ALL,
OPT_LANGS_FROM,
OPT_LANGS_TO,
OPT_INPUT_TRANS_SIGNS,
} from "../../config";
import ShortcutInput from "./ShortcutInput";
import FormControlLabel from "@mui/material/FormControlLabel";
import Switch from "@mui/material/Switch";
import { useInputRule } from "../../hooks/InputRule";
import { useCallback } from "react";
import Grid from "@mui/material/Grid";
import { limitNumber } from "../../libs/utils";
export default function InputSetting() {
const i18n = useI18n();
const { inputRule, updateInputRule } = useInputRule();
const handleChange = (e) => {
e.preventDefault();
let { name, value } = e.target;
switch (name) {
case "triggerTime":
value = limitNumber(value, 10, 1000);
break;
default:
}
updateInputRule({
[name]: value,
});
};
const handleShortcutInput = useCallback(
(val) => {
updateInputRule({ triggerShortcut: val });
},
[updateInputRule]
);
const {
transOpen,
translator,
fromLang,
toLang,
triggerShortcut,
triggerCount,
triggerTime,
transSign,
} = inputRule;
return (
<Box>
<Stack spacing={3}>
<FormControlLabel
control={
<Switch
size="small"
name="transOpen"
checked={transOpen}
onChange={() => {
updateInputRule({ transOpen: !transOpen });
}}
/>
}
label={i18n("use_input_box_translation")}
/>
<TextField
select
size="small"
name="translator"
value={translator}
label={i18n("translate_service")}
onChange={handleChange}
>
{OPT_TRANS_ALL.map((item) => (
<MenuItem key={item} value={item}>
{item}
</MenuItem>
))}
</TextField>
<TextField
select
size="small"
name="fromLang"
value={fromLang}
label={i18n("from_lang")}
onChange={handleChange}
>
{OPT_LANGS_FROM.map(([lang, name]) => (
<MenuItem key={lang} value={lang}>
{name}
</MenuItem>
))}
</TextField>
<TextField
select
size="small"
name="toLang"
value={toLang}
label={i18n("to_lang")}
onChange={handleChange}
>
{OPT_LANGS_TO.map(([lang, name]) => (
<MenuItem key={lang} value={lang}>
{name}
</MenuItem>
))}
</TextField>
<TextField
select
size="small"
name="transSign"
value={transSign}
label={i18n("input_trans_start_sign")}
onChange={handleChange}
helperText={i18n("input_trans_start_sign_help")}
>
<MenuItem value={""}>{i18n("style_none")}</MenuItem>
{OPT_INPUT_TRANS_SIGNS.map((item) => (
<MenuItem key={item} value={item}>
{item}
</MenuItem>
))}
</TextField>
<Box>
<Grid container spacing={2} columns={12}>
<Grid item xs={12} sm={12} md={4} lg={4}>
<ShortcutInput
value={triggerShortcut}
onChange={handleShortcutInput}
label={i18n("trigger_trans_shortcut")}
helperText={i18n("trigger_trans_shortcut_help")}
/>
</Grid>
<Grid item xs={12} sm={12} md={4} lg={4}>
<TextField
select
fullWidth
size="small"
name="triggerCount"
value={triggerCount}
label={i18n("shortcut_press_count")}
onChange={handleChange}
>
{[1, 2, 3, 4, 5].map((val) => (
<MenuItem key={val} value={val}>
{val}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={12} sm={12} md={4} lg={4}>
<TextField
fullWidth
size="small"
label={i18n("combo_timeout")}
type="number"
name="triggerTime"
defaultValue={triggerTime}
onChange={handleChange}
/>
</Grid>
</Grid>
</Box>
</Stack>
</Box>
);
}

View File

@@ -10,6 +10,11 @@ import InfoIcon from "@mui/icons-material/Info";
import DesignServicesIcon from "@mui/icons-material/DesignServices"; 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 SendTimeExtensionIcon from "@mui/icons-material/SendTimeExtension";
import InputIcon from "@mui/icons-material/Input";
import SelectAllIcon from '@mui/icons-material/SelectAll';
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);
@@ -36,12 +41,42 @@ export default function Navigator(props) {
url: "/rules", url: "/rules",
icon: <DesignServicesIcon />, icon: <DesignServicesIcon />,
}, },
{
id: "input_translate",
label: i18n("input_translate"),
url: "/input",
icon: <InputIcon />,
},
{
id: "selection_translate",
label: i18n("selection_translate"),
url: "/tranbox",
icon: <SelectAllIcon />,
},
{
id: "apis_setting",
label: i18n("apis_setting"),
url: "/apis",
icon: <ApiIcon />,
},
{ {
id: "sync", id: "sync",
label: i18n("sync_setting"), label: i18n("sync_setting"),
url: "/sync", url: "/sync",
icon: <SyncIcon />, icon: <SyncIcon />,
}, },
{
id: "webfix",
label: i18n("patch_setting"),
url: "/webfix",
icon: <SendTimeExtensionIcon />,
},
{
id: "words",
label: i18n("favorite_words"),
url: "/words",
icon: <EventNoteIcon />,
},
{ id: "about", label: i18n("about"), url: "/about", icon: <InfoIcon /> }, { id: "about", label: i18n("about"), url: "/about", icon: <InfoIcon /> },
]; ];
return ( return (

View File

@@ -0,0 +1,175 @@
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import TextField from "@mui/material/TextField";
import {
GLOBAL_KEY,
REMAIN_KEY,
OPT_LANGS_FROM,
OPT_LANGS_TO,
OPT_TRANS_ALL,
OPT_STYLE_ALL,
OPT_STYLE_DIY,
OPT_STYLE_USE_COLOR,
} from "../../config";
import { useI18n } from "../../hooks/I18n";
import MenuItem from "@mui/material/MenuItem";
import Grid from "@mui/material/Grid";
import { useOwSubRule } from "../../hooks/SubRules";
export default function OwSubRule() {
const i18n = useI18n();
const { owSubrule, updateOwSubrule } = useOwSubRule();
const handleChange = (e) => {
e.preventDefault();
const { name, value } = e.target;
updateOwSubrule({ [name]: value });
};
const {
translator,
fromLang,
toLang,
textStyle,
transOpen,
bgColor,
textDiyStyle,
} = owSubrule;
const RemainItem = (
<MenuItem key={REMAIN_KEY} value={REMAIN_KEY}>
{i18n("remain_unchanged")}
</MenuItem>
);
const GlobalItem = (
<MenuItem key={GLOBAL_KEY} value={GLOBAL_KEY}>
{GLOBAL_KEY}
</MenuItem>
);
return (
<Stack spacing={2}>
<Box>
<Grid container spacing={2} columns={12}>
<Grid item xs={12} sm={6} md={3} lg={2}>
<TextField
select
size="small"
fullWidth
name="transOpen"
value={transOpen}
label={i18n("translate_switch")}
onChange={handleChange}
>
{RemainItem}
{GlobalItem}
<MenuItem value={"true"}>{i18n("default_enabled")}</MenuItem>
<MenuItem value={"false"}>{i18n("default_disabled")}</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={3} lg={2}>
<TextField
select
size="small"
fullWidth
name="translator"
value={translator}
label={i18n("translate_service")}
onChange={handleChange}
>
{RemainItem}
{GlobalItem}
{OPT_TRANS_ALL.map((item) => (
<MenuItem key={item} value={item}>
{item}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={3} lg={2}>
<TextField
select
size="small"
fullWidth
name="fromLang"
value={fromLang}
label={i18n("from_lang")}
onChange={handleChange}
>
{RemainItem}
{GlobalItem}
{OPT_LANGS_FROM.map(([lang, name]) => (
<MenuItem key={lang} value={lang}>
{name}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={3} lg={2}>
<TextField
select
size="small"
fullWidth
name="toLang"
value={toLang}
label={i18n("to_lang")}
onChange={handleChange}
>
{RemainItem}
{GlobalItem}
{OPT_LANGS_TO.map(([lang, name]) => (
<MenuItem key={lang} value={lang}>
{name}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={3} lg={2}>
<TextField
select
size="small"
fullWidth
name="textStyle"
value={textStyle}
label={i18n("text_style")}
onChange={handleChange}
>
{RemainItem}
{GlobalItem}
{OPT_STYLE_ALL.map((item) => (
<MenuItem key={item} value={item}>
{i18n(item)}
</MenuItem>
))}
</TextField>
</Grid>
{OPT_STYLE_USE_COLOR.includes(textStyle) && (
<Grid item xs={12} sm={6} md={3} lg={2}>
<TextField
size="small"
fullWidth
name="bgColor"
value={bgColor}
label={i18n("bg_color")}
onChange={handleChange}
/>
</Grid>
)}
</Grid>
</Box>
{textStyle === OPT_STYLE_DIY && (
<TextField
size="small"
label={i18n("diy_style")}
helperText={i18n("diy_style_helper")}
name="textDiyStyle"
value={textDiyStyle}
onChange={handleChange}
multiline
/>
)}
</Stack>
);
}

View File

@@ -11,8 +11,12 @@ import {
OPT_LANGS_TO, OPT_LANGS_TO,
OPT_TRANS_ALL, OPT_TRANS_ALL,
OPT_STYLE_ALL, OPT_STYLE_ALL,
OPT_STYLE_DIY,
OPT_STYLE_USE_COLOR,
URL_KISS_RULES_NEW_ISSUE,
OPT_SYNCTYPE_WORKER,
} from "../../config"; } from "../../config";
import { useState, useRef, useEffect, useMemo } from "react"; import { useState, useEffect, useMemo } from "react";
import { useI18n } from "../../hooks/I18n"; import { useI18n } from "../../hooks/I18n";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import Accordion from "@mui/material/Accordion"; import Accordion from "@mui/material/Accordion";
@@ -22,8 +26,6 @@ import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
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";
import FileDownloadIcon from "@mui/icons-material/FileDownload";
import FileUploadIcon from "@mui/icons-material/FileUpload";
import { useSetting } from "../../hooks/Setting"; import { useSetting } from "../../hooks/Setting";
import FormControlLabel from "@mui/material/FormControlLabel"; import FormControlLabel from "@mui/material/FormControlLabel";
import Switch from "@mui/material/Switch"; import Switch from "@mui/material/Switch";
@@ -42,9 +44,18 @@ import { useAlert } from "../../hooks/Alert";
import { syncShareRules } from "../../libs/sync"; import { syncShareRules } from "../../libs/sync";
import { debounce } from "../../libs/utils"; import { debounce } from "../../libs/utils";
import { delSubRules, getSyncWithDefault } from "../../libs/storage"; import { delSubRules, getSyncWithDefault } from "../../libs/storage";
import OwSubRule from "./OwSubRule";
import ClearAllIcon from "@mui/icons-material/ClearAll";
import HelpButton from "./HelpButton";
import { useSyncCaches } from "../../hooks/Sync";
import DownloadButton from "./DownloadButton";
import UploadButton from "./UploadButton";
function RuleFields({ rule, rules, setShow, setKeyword }) { function RuleFields({ rule, rules, setShow, setKeyword }) {
const initFormValues = rule || { ...DEFAULT_RULE, transOpen: "true" }; const initFormValues = rule || {
...DEFAULT_RULE,
transOpen: "true",
};
const editMode = !!rule; const editMode = !!rule;
const i18n = useI18n(); const i18n = useI18n();
@@ -60,6 +71,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
textStyle, textStyle,
transOpen, transOpen,
bgColor, bgColor,
textDiyStyle,
} = formValues; } = formValues;
const hasSamePattern = (str) => { const hasSamePattern = (str) => {
@@ -134,7 +146,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
} }
}; };
const globalItem = rule?.pattern !== "*" && ( const GlobalItem = rule?.pattern !== "*" && (
<MenuItem key={GLOBAL_KEY} value={GLOBAL_KEY}> <MenuItem key={GLOBAL_KEY} value={GLOBAL_KEY}>
{GLOBAL_KEY} {GLOBAL_KEY}
</MenuItem> </MenuItem>
@@ -181,7 +193,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
disabled={disabled} disabled={disabled}
onChange={handleChange} onChange={handleChange}
> >
{globalItem} {GlobalItem}
<MenuItem value={"true"}>{i18n("default_enabled")}</MenuItem> <MenuItem value={"true"}>{i18n("default_enabled")}</MenuItem>
<MenuItem value={"false"}>{i18n("default_disabled")}</MenuItem> <MenuItem value={"false"}>{i18n("default_disabled")}</MenuItem>
</TextField> </TextField>
@@ -197,7 +209,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
disabled={disabled} disabled={disabled}
onChange={handleChange} onChange={handleChange}
> >
{globalItem} {GlobalItem}
{OPT_TRANS_ALL.map((item) => ( {OPT_TRANS_ALL.map((item) => (
<MenuItem key={item} value={item}> <MenuItem key={item} value={item}>
{item} {item}
@@ -216,7 +228,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
disabled={disabled} disabled={disabled}
onChange={handleChange} onChange={handleChange}
> >
{globalItem} {GlobalItem}
{OPT_LANGS_FROM.map(([lang, name]) => ( {OPT_LANGS_FROM.map(([lang, name]) => (
<MenuItem key={lang} value={lang}> <MenuItem key={lang} value={lang}>
{name} {name}
@@ -235,7 +247,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
disabled={disabled} disabled={disabled}
onChange={handleChange} onChange={handleChange}
> >
{globalItem} {GlobalItem}
{OPT_LANGS_TO.map(([lang, name]) => ( {OPT_LANGS_TO.map(([lang, name]) => (
<MenuItem key={lang} value={lang}> <MenuItem key={lang} value={lang}>
{name} {name}
@@ -254,7 +266,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
disabled={disabled} disabled={disabled}
onChange={handleChange} onChange={handleChange}
> >
{globalItem} {GlobalItem}
{OPT_STYLE_ALL.map((item) => ( {OPT_STYLE_ALL.map((item) => (
<MenuItem key={item} value={item}> <MenuItem key={item} value={item}>
{i18n(item)} {i18n(item)}
@@ -262,20 +274,35 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
))} ))}
</TextField> </TextField>
</Grid> </Grid>
<Grid item xs={12} sm={6} md={3} lg={2}> {OPT_STYLE_USE_COLOR.includes(textStyle) && (
<TextField <Grid item xs={12} sm={6} md={3} lg={2}>
size="small" <TextField
fullWidth size="small"
name="bgColor" fullWidth
value={bgColor} name="bgColor"
label={i18n("bg_color")} value={bgColor}
disabled={disabled} label={i18n("bg_color")}
onChange={handleChange} disabled={disabled}
/> onChange={handleChange}
</Grid> />
</Grid>
)}
</Grid> </Grid>
</Box> </Box>
{textStyle === OPT_STYLE_DIY && (
<TextField
size="small"
label={i18n("diy_style")}
helperText={i18n("diy_style_helper")}
name="textDiyStyle"
value={textDiyStyle}
disabled={disabled}
onChange={handleChange}
multiline
/>
)}
{rules && {rules &&
(editMode ? ( (editMode ? (
// 编辑 // 编辑
@@ -337,6 +364,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
} }
function RuleAccordion({ rule, rules }) { function RuleAccordion({ rule, rules }) {
const i18n = useI18n();
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
const handleChange = (e) => { const handleChange = (e) => {
@@ -347,11 +375,14 @@ function RuleAccordion({ rule, rules }) {
<Accordion expanded={expanded} onChange={handleChange}> <Accordion expanded={expanded} onChange={handleChange}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}> <AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography <Typography
style={{ sx={{
opacity: rules ? 1 : 0.5, opacity: rules ? 1 : 0.5,
overflowWrap: "anywhere",
}} }}
> >
{rule.pattern} {rule.pattern === GLOBAL_KEY
? `[${i18n("global_rule")}] ${rule.pattern}`
: rule.pattern}
</Typography> </Typography>
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails>
@@ -361,63 +392,13 @@ function RuleAccordion({ rule, rules }) {
); );
} }
function DownloadButton({ data, text, fileName }) {
const handleClick = (e) => {
e.preventDefault();
if (data) {
const url = window.URL.createObjectURL(new Blob([data]));
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", fileName || `${Date.now()}.json`);
document.body.appendChild(link);
link.click();
link.remove();
}
};
return (
<Button
size="small"
variant="outlined"
onClick={handleClick}
startIcon={<FileDownloadIcon />}
>
{text}
</Button>
);
}
function UploadButton({ onChange, text }) {
const inputRef = useRef(null);
const handleClick = () => {
inputRef.current && inputRef.current.click();
};
return (
<Button
size="small"
variant="outlined"
onClick={handleClick}
startIcon={<FileUploadIcon />}
>
{text}
<input
type="file"
accept=".json"
ref={inputRef}
onChange={onChange}
hidden
/>
</Button>
);
}
function ShareButton({ rules, injectRules, selectedUrl }) { function ShareButton({ rules, injectRules, selectedUrl }) {
const alert = useAlert(); const alert = useAlert();
const i18n = useI18n(); const i18n = useI18n();
const handleClick = async () => { const handleClick = async () => {
try { try {
const { syncUrl, syncKey } = await getSyncWithDefault(); const { syncType, syncUrl, syncKey } = await getSyncWithDefault();
if (!syncUrl || !syncKey) { if (syncType !== OPT_SYNCTYPE_WORKER || !syncUrl || !syncKey) {
alert.warning(i18n("error_sync_setting")); alert.warning(i18n("error_sync_setting"));
return; return;
} }
@@ -448,7 +429,7 @@ function ShareButton({ rules, injectRules, selectedUrl }) {
onClick={handleClick} onClick={handleClick}
startIcon={<ShareIcon />} startIcon={<ShareIcon />}
> >
{"分享"} {i18n("share")}
</Button> </Button>
); );
} }
@@ -463,26 +444,12 @@ function UserRules({ subRules }) {
const injectRules = !!setting?.injectRules; const injectRules = !!setting?.injectRules;
const { selectedUrl, selectedRules } = subRules; const { selectedUrl, selectedRules } = subRules;
const handleImport = (e) => { const handleImport = async (data) => {
const file = e.target.files[0]; try {
if (!file) { await rules.merge(JSON.parse(data));
return; } catch (err) {
console.log("[import rules]", err);
} }
if (!file.type.includes("json")) {
alert(i18n("error_wrong_file_type"));
return;
}
const reader = new FileReader();
reader.onload = async (e) => {
try {
await rules.merge(JSON.parse(e.target.result));
} catch (err) {
console.log("[import rules]", err);
}
};
reader.readAsText(file);
}; };
const handleInject = () => { const handleInject = () => {
@@ -497,6 +464,10 @@ function UserRules({ subRules }) {
} }
}, [showAdd]); }, [showAdd]);
if (!rules.list) {
return;
}
return ( return (
<Stack spacing={3}> <Stack spacing={3}>
<Stack <Stack
@@ -518,10 +489,11 @@ function UserRules({ subRules }) {
{i18n("add")} {i18n("add")}
</Button> </Button>
<UploadButton text={i18n("import")} onChange={handleImport} /> <UploadButton text={i18n("import")} handleImport={handleImport} />
<DownloadButton <DownloadButton
data={JSON.stringify([...rules.list].reverse(), null, "\t")} data={JSON.stringify([...rules.list].reverse(), null, 2)}
text={i18n("export")} text={i18n("export")}
fileName={`kiss-rules_${Date.now()}.json`}
/> />
<ShareButton <ShareButton
@@ -530,6 +502,19 @@ function UserRules({ subRules }) {
selectedUrl={selectedUrl} selectedUrl={selectedUrl}
/> />
<Button
size="small"
variant="outlined"
onClick={() => {
rules.clear();
}}
startIcon={<ClearAllIcon />}
>
{i18n("clear_all")}
</Button>
<HelpButton url={URL_KISS_RULES_NEW_ISSUE} />
<FormControlLabel <FormControlLabel
control={ control={
<Switch <Switch
@@ -577,13 +562,23 @@ function UserRules({ subRules }) {
); );
} }
function SubRulesItem({ index, url, selectedUrl, delSub, setSelectedRules }) { function SubRulesItem({
index,
url,
syncAt,
selectedUrl,
delSub,
setSelectedRules,
updateDataCache,
deleteDataCache,
}) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const handleDel = async () => { const handleDel = async () => {
try { try {
await delSub(url); await delSub(url);
await delSubRules(url); await delSubRules(url);
await deleteDataCache(url);
} catch (err) { } catch (err) {
console.log("[del subrules]", err); console.log("[del subrules]", err);
} }
@@ -596,6 +591,7 @@ function SubRulesItem({ index, url, selectedUrl, delSub, setSelectedRules }) {
if (rules.length > 0 && url === selectedUrl) { if (rules.length > 0 && url === selectedUrl) {
setSelectedRules(rules); setSelectedRules(rules);
} }
await updateDataCache(url);
} catch (err) { } catch (err) {
console.log("[sync sub rules]", err); console.log("[sync sub rules]", err);
} finally { } finally {
@@ -605,7 +601,20 @@ function SubRulesItem({ index, url, selectedUrl, delSub, setSelectedRules }) {
return ( return (
<Stack direction="row" alignItems="center" spacing={2}> <Stack direction="row" alignItems="center" spacing={2}>
<FormControlLabel value={url} control={<Radio />} label={url} /> <FormControlLabel
value={url}
control={<Radio />}
sx={{
overflowWrap: "anywhere",
}}
label={url}
/>
{syncAt && (
<span style={{ marginLeft: "0.5em", opacity: 0.5 }}>
[{new Date(syncAt).toLocaleString()}]
</span>
)}
{loading ? ( {loading ? (
<CircularProgress size={16} /> <CircularProgress size={16} />
@@ -624,7 +633,7 @@ function SubRulesItem({ index, url, selectedUrl, delSub, setSelectedRules }) {
); );
} }
function SubRulesEdit({ subList, addSub }) { function SubRulesEdit({ subList, addSub, updateDataCache }) {
const i18n = useI18n(); const i18n = useI18n();
const [inputText, setInputText] = useState(""); const [inputText, setInputText] = useState("");
const [inputError, setInputError] = useState(""); const [inputError, setInputError] = useState("");
@@ -659,6 +668,7 @@ function SubRulesEdit({ subList, addSub }) {
throw new Error("empty rules"); throw new Error("empty rules");
} }
await addSub(url); await addSub(url);
await updateDataCache(url);
setShowInput(false); setShowInput(false);
setInputText(""); setInputText("");
} catch (err) { } catch (err) {
@@ -693,6 +703,7 @@ function SubRulesEdit({ subList, addSub }) {
> >
{i18n("add")} {i18n("add")}
</Button> </Button>
<HelpButton url={URL_KISS_RULES_NEW_ISSUE} />
</Stack> </Stack>
{showInput && ( {showInput && (
@@ -737,25 +748,38 @@ function SubRules({ subRules }) {
setSelectedRules, setSelectedRules,
loading, loading,
} = subRules; } = subRules;
const { dataCaches, updateDataCache, deleteDataCache, reloadSync } =
useSyncCaches();
const handleSelect = (e) => { const handleSelect = (e) => {
const url = e.target.value; const url = e.target.value;
selectSub(url); selectSub(url);
}; };
useEffect(() => {
reloadSync();
}, [selectedRules, reloadSync]);
return ( return (
<Stack spacing={3}> <Stack spacing={3}>
<SubRulesEdit subList={subList} addSub={addSub} /> <SubRulesEdit
subList={subList}
addSub={addSub}
updateDataCache={updateDataCache}
/>
<RadioGroup value={selectedUrl} onChange={handleSelect}> <RadioGroup value={selectedUrl} onChange={handleSelect}>
{subList.map((item, index) => ( {subList.map((item, index) => (
<SubRulesItem <SubRulesItem
key={item.url} key={item.url}
url={item.url} url={item.url}
syncAt={dataCaches[item.url]}
index={index} index={index}
selectedUrl={selectedUrl} selectedUrl={selectedUrl}
delSub={delSub} delSub={delSub}
setSelectedRules={setSelectedRules} setSelectedRules={setSelectedRules}
updateDataCache={updateDataCache}
deleteDataCache={deleteDataCache}
/> />
))} ))}
</RadioGroup> </RadioGroup>
@@ -797,6 +821,7 @@ export default function Rules() {
<Tabs value={activeTab} onChange={handleTabChange}> <Tabs value={activeTab} onChange={handleTabChange}>
<Tab label={i18n("personal_rules")} /> <Tab label={i18n("personal_rules")} />
<Tab label={i18n("subscribe_rules")} /> <Tab label={i18n("subscribe_rules")} />
<Tab label={i18n("overwrite_subscribe_rules")} />
</Tabs> </Tabs>
</Box> </Box>
<div hidden={activeTab !== 0}> <div hidden={activeTab !== 0}>
@@ -805,6 +830,7 @@ export default function Rules() {
<div hidden={activeTab !== 1}> <div hidden={activeTab !== 1}>
{activeTab === 1 && <SubRules subRules={subRules} />} {activeTab === 1 && <SubRules subRules={subRules} />}
</div> </div>
<div hidden={activeTab !== 2}>{activeTab === 2 && <OwSubRule />}</div>
</Stack> </Stack>
</Box> </Box>
); );

View File

@@ -5,14 +5,41 @@ import TextField from "@mui/material/TextField";
import MenuItem from "@mui/material/MenuItem"; import MenuItem from "@mui/material/MenuItem";
import FormControl from "@mui/material/FormControl"; import FormControl from "@mui/material/FormControl";
import Select from "@mui/material/Select"; import Select from "@mui/material/Select";
import Link from "@mui/material/Link";
import FormHelperText from "@mui/material/FormHelperText";
import { useSetting } from "../../hooks/Setting"; import { useSetting } from "../../hooks/Setting";
import { limitNumber } from "../../libs/utils"; import { limitNumber } from "../../libs/utils";
import { useI18n } from "../../hooks/I18n"; import { useI18n } from "../../hooks/I18n";
import { UI_LANGS } from "../../config"; import { useAlert } from "../../hooks/Alert";
import { isExt } from "../../libs/client";
import Grid from "@mui/material/Grid";
import {
UI_LANGS,
TRANS_NEWLINE_LENGTH,
CACHE_NAME,
OPT_MOUSEKEY_ALL,
OPT_MOUSEKEY_DISABLE,
OPT_SHORTCUT_TRANSLATE,
OPT_SHORTCUT_STYLE,
OPT_SHORTCUT_POPUP,
OPT_SHORTCUT_SETTING,
} from "../../config";
import { useShortcut } from "../../hooks/Shortcut";
import ShortcutInput from "./ShortcutInput";
import { useFab } from "../../hooks/Fab";
function ShortcutItem({ action, label }) {
const { shortcut, setShortcut } = useShortcut(action);
return (
<ShortcutInput value={shortcut} onChange={setShortcut} label={label} />
);
}
export default function Settings() { export default function Settings() {
const i18n = useI18n(); const i18n = useI18n();
const { setting, updateSetting } = useSetting(); const { setting, updateSetting } = useSetting();
const alert = useAlert();
const { fab, updateFab } = useFab();
const handleChange = (e) => { const handleChange = (e) => {
e.preventDefault(); e.preventDefault();
@@ -30,6 +57,9 @@ export default function Settings() {
case "maxLength": case "maxLength":
value = limitNumber(value, 100, 10000); value = limitNumber(value, 100, 10000);
break; break;
case "newlineLength":
value = limitNumber(value, 1, 1000);
break;
default: default:
} }
updateSetting({ updateSetting({
@@ -37,19 +67,27 @@ export default function Settings() {
}); });
}; };
const handleClearCache = () => {
try {
caches.delete(CACHE_NAME);
alert.success(i18n("clear_success"));
} catch (err) {
console.log("[clear cache]", err);
}
};
const { const {
uiLang, uiLang,
googleUrl,
fetchLimit, fetchLimit,
fetchInterval, fetchInterval,
minLength, minLength,
maxLength, maxLength,
openaiUrl,
openaiKey,
openaiModel,
openaiPrompt,
clearCache, clearCache,
newlineLength = TRANS_NEWLINE_LENGTH,
mouseKey = OPT_MOUSEKEY_DISABLE,
detectRemote = false,
} = setting; } = setting;
const { isHide = false } = fab || {};
return ( return (
<Box> <Box>
@@ -75,7 +113,7 @@ export default function Settings() {
label={i18n("fetch_limit")} label={i18n("fetch_limit")}
type="number" type="number"
name="fetchLimit" name="fetchLimit"
value={fetchLimit} defaultValue={fetchLimit}
onChange={handleChange} onChange={handleChange}
/> />
@@ -84,7 +122,7 @@ export default function Settings() {
label={i18n("fetch_interval")} label={i18n("fetch_interval")}
type="number" type="number"
name="fetchInterval" name="fetchInterval"
value={fetchInterval} defaultValue={fetchInterval}
onChange={handleChange} onChange={handleChange}
/> />
@@ -93,7 +131,7 @@ export default function Settings() {
label={i18n("min_translate_length")} label={i18n("min_translate_length")}
type="number" type="number"
name="minLength" name="minLength"
value={minLength} defaultValue={minLength}
onChange={handleChange} onChange={handleChange}
/> />
@@ -102,64 +140,114 @@ export default function Settings() {
label={i18n("max_translate_length")} label={i18n("max_translate_length")}
type="number" type="number"
name="maxLength" name="maxLength"
value={maxLength} defaultValue={maxLength}
onChange={handleChange}
/>
<TextField
size="small"
label={i18n("num_of_newline_characters")}
type="number"
name="newlineLength"
defaultValue={newlineLength}
onChange={handleChange} onChange={handleChange}
/> />
<FormControl size="small"> <FormControl size="small">
<InputLabel>{i18n("clear_cache")}</InputLabel> <InputLabel>{i18n("mouseover_translation")}</InputLabel>
<Select <Select
name="clearCache" name="mouseKey"
value={clearCache} value={mouseKey}
label={i18n("clear_cache")} label={i18n("mouseover_translation")}
onChange={handleChange} onChange={handleChange}
> >
<MenuItem value={false}>{i18n("clear_cache_never")}</MenuItem> {OPT_MOUSEKEY_ALL.map((item) => (
<MenuItem value={true}>{i18n("clear_cache_restart")}</MenuItem> <MenuItem key={item} value={item}>
{i18n(item)}
</MenuItem>
))}
</Select> </Select>
</FormControl> </FormControl>
<TextField <FormControl size="small">
size="small" <InputLabel>{i18n("hide_fab_button")}</InputLabel>
label={i18n("google_api")} <Select
name="googleUrl" name="isHide"
value={googleUrl} value={isHide}
onChange={handleChange} label={i18n("hide_fab_button")}
/> onChange={(e) => {
updateFab({ isHide: e.target.value });
}}
>
<MenuItem value={false}>{i18n("show")}</MenuItem>
<MenuItem value={true}>{i18n("hide")}</MenuItem>
</Select>
</FormControl>
<TextField <FormControl size="small">
size="small" <InputLabel>{i18n("detect_lang_remote")}</InputLabel>
label={i18n("openai_api")} <Select
name="openaiUrl" name="detectRemote"
value={openaiUrl} value={detectRemote}
onChange={handleChange} label={i18n("detect_lang_remote")}
/> onChange={handleChange}
>
<MenuItem value={false}>{i18n("disable")}</MenuItem>
<MenuItem value={true}>{i18n("enable")}</MenuItem>
</Select>
<FormHelperText>{i18n("detect_lang_remote_help")}</FormHelperText>
</FormControl>
<TextField {isExt ? (
size="small" <FormControl size="small">
type="password" <InputLabel>{i18n("if_clear_cache")}</InputLabel>
label={i18n("openai_key")} <Select
name="openaiKey" name="clearCache"
value={openaiKey} value={clearCache}
onChange={handleChange} label={i18n("if_clear_cache")}
/> onChange={handleChange}
>
<TextField <MenuItem value={false}>{i18n("clear_cache_never")}</MenuItem>
size="small" <MenuItem value={true}>{i18n("clear_cache_restart")}</MenuItem>
label={i18n("openai_model")} </Select>
name="openaiModel" <FormHelperText>
value={openaiModel} <Link component="button" onClick={handleClearCache}>
onChange={handleChange} {i18n("clear_all_cache_now")}
/> </Link>
</FormHelperText>
<TextField </FormControl>
size="small" ) : (
label={i18n("openai_prompt")} <>
name="openaiPrompt" <Box>
value={openaiPrompt} <Grid container spacing={2} columns={12}>
onChange={handleChange} <Grid item xs={12} sm={12} md={3} lg={3}>
multiline <ShortcutItem
/> action={OPT_SHORTCUT_TRANSLATE}
label={i18n("toggle_translate_shortcut")}
/>
</Grid>
<Grid item xs={12} sm={12} md={3} lg={3}>
<ShortcutItem
action={OPT_SHORTCUT_STYLE}
label={i18n("toggle_style_shortcut")}
/>
</Grid>
<Grid item xs={12} sm={12} md={3} lg={3}>
<ShortcutItem
action={OPT_SHORTCUT_POPUP}
label={i18n("toggle_popup_shortcut")}
/>
</Grid>
<Grid item xs={12} sm={12} md={3} lg={3}>
<ShortcutItem
action={OPT_SHORTCUT_SETTING}
label={i18n("open_setting_shortcut")}
/>
</Grid>
</Grid>
</Box>
</>
)}
</Stack> </Stack>
</Box> </Box>
); );

View File

@@ -0,0 +1,56 @@
import Stack from "@mui/material/Stack";
import TextField from "@mui/material/TextField";
import IconButton from "@mui/material/IconButton";
import EditIcon from "@mui/icons-material/Edit";
import { useEffect, useState, useRef } from "react";
import { shortcutListener } from "../../libs/shortcut";
export default function ShortcutInput({ value, onChange, label, helperText }) {
const [disabled, setDisabled] = useState(true);
const inputRef = useRef(null);
useEffect(() => {
if (disabled) {
return;
}
inputRef.current.focus();
onChange([]);
const clearShortcut = shortcutListener((curkeys, allkeys) => {
onChange(allkeys);
if (curkeys.length === 0) {
setDisabled(true);
}
}, inputRef.current);
return () => {
clearShortcut();
};
}, [disabled, onChange]);
return (
<Stack direction="row" alignItems="flex-start">
<TextField
size="small"
label={label}
name={label}
value={value.map((item) => (item === " " ? "Space" : item)).join(" + ")}
fullWidth
inputRef={inputRef}
disabled={disabled}
onBlur={() => {
setDisabled(true);
}}
helperText={helperText}
/>
<IconButton
onClick={() => {
setDisabled(false);
}}
>
{<EditIcon />}
</IconButton>
</Stack>
);
}

View File

@@ -5,19 +5,27 @@ import { useI18n } from "../../hooks/I18n";
import { useSync } from "../../hooks/Sync"; 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 { URL_KISS_WORKER } from "../../config"; import MenuItem from "@mui/material/MenuItem";
import {
URL_KISS_WORKER,
OPT_SYNCTYPE_ALL,
OPT_SYNCTYPE_WORKER,
OPT_SYNCTYPE_WEBDAV,
} 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 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 CircularProgress from "@mui/material/CircularProgress";
import { useSetting } from "../../hooks/Setting";
export default function SyncSetting() { export default function SyncSetting() {
const i18n = useI18n(); const i18n = useI18n();
const { sync, updateSync } = useSync(); const { sync, updateSync } = useSync();
const alert = useAlert(); const alert = useAlert();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { reloadSetting } = useSetting();
const handleChange = async (e) => { const handleChange = async (e) => {
e.preventDefault(); e.preventDefault();
@@ -32,22 +40,47 @@ export default function SyncSetting() {
try { try {
setLoading(true); setLoading(true);
await syncSettingAndRules(); await syncSettingAndRules();
alert.success(i18n("data_sync_success")); await reloadSetting();
alert.success(i18n("sync_success"));
} catch (err) { } catch (err) {
console.log("[sync all]", err); console.log("[sync all]", err);
alert.error(i18n("data_sync_error")); alert.error(i18n("sync_failed"));
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
const { syncUrl, syncKey } = sync; if (!sync) {
return;
}
const {
syncType = OPT_SYNCTYPE_WORKER,
syncUrl = "",
syncUser = "",
syncKey = "",
} = sync;
return ( return (
<Box> <Box>
<Stack spacing={3}> <Stack spacing={3}>
<Alert severity="warning">{i18n("sync_warn")}</Alert> <Alert severity="warning">{i18n("sync_warn")}</Alert>
<TextField
select
size="small"
name="syncType"
value={syncType}
label={i18n("data_sync_type")}
onChange={handleChange}
>
{OPT_SYNCTYPE_ALL.map((item) => (
<MenuItem key={item} value={item}>
{item}
</MenuItem>
))}
</TextField>
<TextField <TextField
size="small" size="small"
label={i18n("data_sync_url")} label={i18n("data_sync_url")}
@@ -55,10 +88,24 @@ export default function SyncSetting() {
value={syncUrl} value={syncUrl}
onChange={handleChange} onChange={handleChange}
helperText={ helperText={
<Link href={URL_KISS_WORKER}>{i18n("about_sync_api")}</Link> syncType === OPT_SYNCTYPE_WORKER && (
<Link href={URL_KISS_WORKER} target="_blank">
{i18n("about_sync_api")}
</Link>
)
} }
/> />
{syncType === OPT_SYNCTYPE_WEBDAV && (
<TextField
size="small"
label={i18n("data_sync_user")}
name="syncUser"
value={syncUser}
onChange={handleChange}
/>
)}
<TextField <TextField
size="small" size="small"
type="password" type="password"
@@ -82,7 +129,7 @@ export default function SyncSetting() {
onClick={handleSyncTest} onClick={handleSyncTest}
startIcon={<SyncIcon />} startIcon={<SyncIcon />}
> >
{i18n("data_sync_test")} {i18n("sync_now")}
</Button> </Button>
{loading && <CircularProgress size={16} />} {loading && <CircularProgress size={16} />}
</Stack> </Stack>

View File

@@ -0,0 +1,140 @@
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import TextField from "@mui/material/TextField";
import MenuItem from "@mui/material/MenuItem";
import { useI18n } from "../../hooks/I18n";
import { OPT_TRANS_ALL, OPT_LANGS_FROM, OPT_LANGS_TO } from "../../config";
import ShortcutInput from "./ShortcutInput";
import FormControlLabel from "@mui/material/FormControlLabel";
import Switch from "@mui/material/Switch";
import { useCallback } from "react";
import { limitNumber } from "../../libs/utils";
import { useTranbox } from "../../hooks/Tranbox";
export default function Tranbox() {
const i18n = useI18n();
const { tranboxSetting, updateTranbox } = useTranbox();
const handleChange = (e) => {
e.preventDefault();
let { name, value } = e.target;
switch (name) {
case "btnOffsetX":
value = limitNumber(value, 0, 100);
break;
case "btnOffsetY":
value = limitNumber(value, 0, 100);
break;
default:
}
updateTranbox({
[name]: value,
});
};
const handleShortcutInput = useCallback(
(val) => {
updateTranbox({ tranboxShortcut: val });
},
[updateTranbox]
);
const {
transOpen,
translator,
fromLang,
toLang,
tranboxShortcut,
btnOffsetX,
btnOffsetY,
} = tranboxSetting;
return (
<Box>
<Stack spacing={3}>
<FormControlLabel
control={
<Switch
size="small"
name="transOpen"
checked={transOpen}
onChange={() => {
updateTranbox({ transOpen: !transOpen });
}}
/>
}
label={i18n("toggle_selection_translate")}
/>
<TextField
select
size="small"
name="translator"
value={translator}
label={i18n("translate_service")}
onChange={handleChange}
>
{OPT_TRANS_ALL.map((item) => (
<MenuItem key={item} value={item}>
{item}
</MenuItem>
))}
</TextField>
<TextField
select
size="small"
name="fromLang"
value={fromLang}
label={i18n("from_lang")}
onChange={handleChange}
>
{OPT_LANGS_FROM.map(([lang, name]) => (
<MenuItem key={lang} value={lang}>
{name}
</MenuItem>
))}
</TextField>
<TextField
select
size="small"
name="toLang"
value={toLang}
label={i18n("to_lang")}
onChange={handleChange}
>
{OPT_LANGS_TO.map(([lang, name]) => (
<MenuItem key={lang} value={lang}>
{name}
</MenuItem>
))}
</TextField>
<TextField
size="small"
label={i18n("tranbtn_offset_x")}
type="number"
name="btnOffsetX"
defaultValue={btnOffsetX}
onChange={handleChange}
/>
<TextField
size="small"
label={i18n("tranbtn_offset_y")}
type="number"
name="btnOffsetY"
defaultValue={btnOffsetY}
onChange={handleChange}
/>
<ShortcutInput
value={tranboxShortcut}
onChange={handleShortcutInput}
label={i18n("trigger_tranbox_shortcut")}
/>
</Stack>
</Box>
);
}

View File

@@ -0,0 +1,55 @@
import { useRef } from "react";
import FileUploadIcon from "@mui/icons-material/FileUpload";
import { useI18n } from "../../hooks/I18n";
import Button from "@mui/material/Button";
export default function UploadButton({
handleImport,
text,
fileType = "json",
fileExts = [".json"],
}) {
const i18n = useI18n();
const inputRef = useRef(null);
const handleClick = () => {
if (inputRef.current) {
inputRef.current.click();
inputRef.current.value = null;
}
};
const onChange = (e) => {
const file = e.target.files[0];
if (!file) {
return;
}
if (!file.type.includes(fileType)) {
alert(i18n("error_wrong_file_type"));
return;
}
const reader = new FileReader();
reader.onload = async (e) => {
handleImport(e.target.result);
};
reader.readAsText(file);
};
return (
<Button
size="small"
variant="outlined"
onClick={handleClick}
startIcon={<FileUploadIcon />}
>
{text}
<input
type="file"
accept={fileExts.join(", ")}
ref={inputRef}
onChange={onChange}
hidden
/>
</Button>
);
}

165
src/views/Options/Webfix.js Normal file
View File

@@ -0,0 +1,165 @@
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>
);
}

View File

@@ -15,9 +15,16 @@ import { AlertProvider } from "../../hooks/Alert";
import Link from "@mui/material/Link"; import Link from "@mui/material/Link";
import Divider from "@mui/material/Divider"; import Divider from "@mui/material/Divider";
import Stack from "@mui/material/Stack"; import Stack from "@mui/material/Stack";
import { adaptScript } from "../../libs/gm";
import Alert from "@mui/material/Alert";
import Apis from "./Apis";
import Webfix from "./Webfix";
import InputSetting from "./InputSetting";
import Tranbox from "./Tranbox";
import FavWords from "./FavWords";
export default function Options() { export default function Options() {
const [error, setError] = useState(false); const [error, setError] = useState("");
const [ready, setReady] = useState(false); const [ready, setReady] = useState(false);
useEffect(() => { useEffect(() => {
@@ -26,7 +33,22 @@ export default function Options() {
// 等待GM注入 // 等待GM注入
let i = 0; let i = 0;
for (;;) { for (;;) {
if (window.APP_NAME === process.env.REACT_APP_NAME) { if (window?.APP_INFO?.name === process.env.REACT_APP_NAME) {
const { version, eventName } = window.APP_INFO;
// 检查版本是否一致
if (version !== process.env.REACT_APP_VERSION) {
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})。`
);
break;
}
if (eventName) {
// 注入GM接口
adaptScript(eventName);
}
// 同步数据 // 同步数据
await trySyncSettingAndRules(); await trySyncSettingAndRules();
setReady(true); setReady(true);
@@ -34,7 +56,9 @@ export default function Options() {
} }
if (++i > 8) { if (++i > 8) {
setError(true); setError(
"Time out. Please confirm whether to install or enable KISS Translator GreaseMonkey script? 连接超时,请检查是否安装或启用简约翻译油猴脚本。"
);
break; break;
} }
@@ -56,29 +80,20 @@ export default function Options() {
href={process.env.REACT_APP_HOMEPAGE} href={process.env.REACT_APP_HOMEPAGE}
>{`KISS Translator v${process.env.REACT_APP_VERSION}`}</Link> >{`KISS Translator v${process.env.REACT_APP_VERSION}`}</Link>
</Divider> </Divider>
<h2> <Alert severity="error">{error}</Alert>
Please confirm whether to install or enable KISS Translator
GreaseMonkey script?
</h2>
<Stack spacing={2}> <Stack spacing={2}>
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}> <Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}>
Install Userscript 1 Install/Update Userscript for Tampermonkey/Violentmonkey
</Link>
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL2}>
Install Userscript 2
</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 Userscript Safari 1 Install/Update Userscript for iOS Safari
</Link>
<Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL2}>
Install Userscript Safari 2
</Link>
<Link href={process.env.REACT_APP_OPTIONSPAGE}>
Open Options Page 1
</Link>
<Link href={process.env.REACT_APP_OPTIONSPAGE2}>
Open Options Page 2
</Link> </Link>
{/* <Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL2}>
Install/Update Userscript for iOS Safari 2
</Link> */}
</Stack> </Stack>
</center> </center>
); );
@@ -106,7 +121,12 @@ export default function Options() {
<Route path="/" element={<Layout />}> <Route path="/" element={<Layout />}>
<Route index element={<Setting />} /> <Route index element={<Setting />} />
<Route path="rules" element={<Rules />} /> <Route path="rules" element={<Rules />} />
<Route path="input" element={<InputSetting />} />
<Route path="tranbox" element={<Tranbox />} />
<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="about" element={<About />} /> <Route path="about" element={<About />} />
</Route> </Route>
</Routes> </Routes>

47
src/views/Popup/Header.js Normal file
View File

@@ -0,0 +1,47 @@
import Box from "@mui/material/Box";
import IconButton from "@mui/material/IconButton";
import CloseIcon from "@mui/icons-material/Close";
import HomeIcon from "@mui/icons-material/Home";
import Stack from "@mui/material/Stack";
import DarkModeButton from "../Options/DarkModeButton";
export default function Header({ setShowPopup }) {
const handleHomepage = () => {
window.open(process.env.REACT_APP_HOMEPAGE, "_blank");
};
return (
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
spacing={2}
>
<Stack direction="row" justifyContent="flex-start" alignItems="center">
<IconButton onClick={handleHomepage}>
<HomeIcon />
</IconButton>
<Box
sx={{
userSelect: "none",
WebkitUserSelect: "none",
}}
>
{`${process.env.REACT_APP_NAME} v${process.env.REACT_APP_VERSION}`}
</Box>
</Stack>
{setShowPopup ? (
<IconButton
onClick={() => {
setShowPopup(false);
}}
>
<CloseIcon />
</IconButton>
) : (
<DarkModeButton />
)}
</Stack>
);
}

View File

@@ -5,29 +5,38 @@ 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 { sendTabMsg } from "../../libs/msg"; import { sendBgMsg, sendTabMsg, getTabInfo } 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";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import Divider from "@mui/material/Divider";
import Header from "./Header";
import { import {
MSG_TRANS_TOGGLE, MSG_TRANS_TOGGLE,
MSG_TRANS_GETRULE, MSG_TRANS_GETRULE,
MSG_TRANS_PUTRULE, MSG_TRANS_PUTRULE,
MSG_OPEN_OPTIONS,
MSG_SAVE_RULE,
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,
} from "../../config"; } from "../../config";
import { sendIframeMsg } from "../../libs/iframe"; import { sendIframeMsg } from "../../libs/iframe";
import { saveRule } from "../../libs/rules";
import { tryClearCaches } from "../../libs";
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 handleOpenSetting = () => { const handleOpenSetting = () => {
if (isExt) { if (!tran) {
browser?.runtime.openOptionsPage(); browser?.runtime.openOptionsPage();
} else if (isExt) {
sendBgMsg(MSG_OPEN_OPTIONS);
} else { } else {
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank"); window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank");
} }
@@ -38,7 +47,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
try { try {
setRule({ ...rule, transOpen: e.target.checked ? "true" : "false" }); setRule({ ...rule, transOpen: e.target.checked ? "true" : "false" });
if (isExt) { if (!tran) {
await sendTabMsg(MSG_TRANS_TOGGLE); await sendTabMsg(MSG_TRANS_TOGGLE);
} else { } else {
tran.toggle(); tran.toggle();
@@ -54,7 +63,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
const { name, value } = e.target; const { name, value } = e.target;
setRule((pre) => ({ ...pre, [name]: value })); setRule((pre) => ({ ...pre, [name]: value }));
if (isExt) { if (!tran) {
await sendTabMsg(MSG_TRANS_PUTRULE, { [name]: value }); await sendTabMsg(MSG_TRANS_PUTRULE, { [name]: value });
} else { } else {
tran.updateRule({ [name]: value }); tran.updateRule({ [name]: value });
@@ -65,8 +74,30 @@ export default function Popup({ setShowPopup, translator: tran }) {
} }
}; };
const handleClearCache = () => {
tryClearCaches();
};
const handleSaveRule = async () => {
try {
let href = window.location.href;
if (!tran) {
const tab = await getTabInfo();
href = tab.url;
}
const newRule = { ...rule, pattern: href };
if (isExt && tran) {
sendBgMsg(MSG_SAVE_RULE, newRule);
} else {
saveRule(newRule);
}
} catch (err) {
console.log("[save rule]", err);
}
};
useEffect(() => { useEffect(() => {
if (!isExt) { if (tran) {
return; return;
} }
(async () => { (async () => {
@@ -79,12 +110,18 @@ export default function Popup({ setShowPopup, translator: tran }) {
console.log("[query rule]", err); console.log("[query rule]", err);
} }
})(); })();
}, []); }, [tran]);
if (!rule) { if (!rule) {
return ( return (
<Box minWidth={300} sx={{ p: 2 }}> <Box minWidth={300}>
<Stack spacing={3}> {!tran && (
<>
<Header />
<Divider />
</>
)}
<Stack sx={{ p: 2 }} spacing={3}>
<Button variant="text" onClick={handleOpenSetting}> <Button variant="text" onClick={handleOpenSetting}>
{i18n("setting")} {i18n("setting")}
</Button> </Button>
@@ -96,17 +133,35 @@ export default function Popup({ setShowPopup, translator: tran }) {
const { transOpen, translator, fromLang, toLang, textStyle, bgColor } = rule; const { transOpen, translator, fromLang, toLang, textStyle, bgColor } = rule;
return ( return (
<Box minWidth={300} sx={{ p: 2 }}> <Box minWidth={300}>
<Stack spacing={2}> {!tran && (
<FormControlLabel <>
control={ <Header />
<Switch <Divider />
checked={transOpen === "true"} </>
onChange={handleTransToggle} )}
/> <Stack sx={{ p: 2 }} spacing={2}>
} <Stack
label={i18n("translate_alt")} direction="row"
/> justifyContent="space-between"
alignItems="center"
spacing={2}
>
<FormControlLabel
control={
<Switch
checked={transOpen === "true"}
onChange={handleTransToggle}
/>
}
label={i18n("translate_alt")}
/>
{!isExt && (
<Button variant="text" onClick={handleClearCache}>
{i18n("clear_cache")}
</Button>
)}
</Stack>
<TextField <TextField
select select
@@ -172,17 +227,29 @@ export default function Popup({ setShowPopup, translator: tran }) {
))} ))}
</TextField> </TextField>
<TextField {OPT_STYLE_USE_COLOR.includes(textStyle) && (
size="small" <TextField
name="bgColor" size="small"
value={bgColor} name="bgColor"
label={i18n("bg_color")} value={bgColor}
onChange={handleChange} label={i18n("bg_color")}
/> onChange={handleChange}
/>
)}
<Button variant="text" onClick={handleOpenSetting}> <Stack
{i18n("setting")} direction="row"
</Button> justifyContent="space-between"
alignItems="center"
spacing={2}
>
<Button variant="text" onClick={handleSaveRule}>
{i18n("save_rule")}
</Button>
<Button variant="text" onClick={handleOpenSetting}>
{i18n("setting")}
</Button>
</Stack>
</Stack> </Stack>
</Box> </Box>
); );

View File

@@ -0,0 +1,35 @@
import IconButton from "@mui/material/IconButton";
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
import LibraryAddCheckIcon from "@mui/icons-material/LibraryAddCheck";
import { useState } from "react";
export default function CopyBtn({ text }) {
const [copied, setCopied] = useState(false);
const handleClick = (e) => {
e.stopPropagation();
navigator.clipboard.writeText(text);
setCopied(true);
const timer = setTimeout(() => {
clearTimeout(timer);
setCopied(false);
}, 500);
};
return (
<IconButton
size="small"
sx={{
opacity: 0.5,
"&:hover": {
opacity: 1,
},
}}
onClick={handleClick}
>
{copied ? (
<LibraryAddCheckIcon fontSize="inherit" />
) : (
<ContentCopyIcon fontSize="inherit" />
)}
</IconButton>
);
}

View File

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

View File

@@ -0,0 +1,258 @@
import { useEffect, useState } from "react";
import Paper from "@mui/material/Paper";
import Box from "@mui/material/Box";
function Pointer({
direction,
size,
setSize,
position,
setPosition,
children,
minSize,
maxSize,
...props
}) {
const [origin, setOrigin] = useState(null);
function handlePointerDown(e) {
e.target.setPointerCapture(e.pointerId);
setOrigin({
x: position.x,
y: position.y,
w: size.w,
h: size.h,
clientX: e.clientX,
clientY: e.clientY,
});
}
function handlePointerMove(e) {
if (origin) {
const dx = e.clientX - origin.clientX;
const dy = e.clientY - origin.clientY;
let x = position.x;
let y = position.y;
let w = size.w;
let h = size.h;
switch (direction) {
case "Header":
x = origin.x + dx;
y = origin.y + dy;
break;
case "TopLeft":
x = origin.x + dx;
y = origin.y + dy;
w = origin.w - dx;
h = origin.h - dy;
break;
case "Top":
y = origin.y + dy;
h = origin.h - dy;
break;
case "TopRight":
y = origin.y + dy;
w = origin.w + dx;
h = origin.h - dy;
break;
case "Left":
x = origin.x + dx;
w = origin.w - dx;
break;
case "Right":
w = origin.w + dx;
break;
case "BottomLeft":
x = origin.x + dx;
w = origin.w - dx;
h = origin.h + dy;
break;
case "Bottom":
h = origin.h + dy;
break;
case "BottomRight":
w = origin.w + dx;
h = origin.h + dy;
break;
default:
}
if (w < minSize.w) {
w = minSize.w;
x = position.x;
}
if (w > maxSize.w) {
w = maxSize.w;
x = position.x;
}
if (h < minSize.h) {
h = minSize.h;
y = position.y;
}
if (h > maxSize.h) {
h = maxSize.h;
y = position.y;
}
setPosition({ x, y });
setSize({ w, h });
}
}
function handlePointerUp(e) {
setOrigin(null);
}
return (
<div
{...props}
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
>
{children}
</div>
);
}
export default function DraggableResizable({
header,
children,
defaultPosition = {
x: 0,
y: 0,
},
defaultSize = {
w: 600,
h: 400,
},
minSize = {
w: 300,
h: 200,
},
maxSize = {
w: 1200,
h: 1200,
},
onChangeSize,
onChangePosition,
}) {
const lineWidth = 4;
const [position, setPosition] = useState(defaultPosition);
const [size, setSize] = useState(defaultSize);
const opts = {
size,
setSize,
position,
setPosition,
minSize,
maxSize,
};
useEffect(() => {
onChangeSize && onChangeSize(size);
}, [size, onChangeSize]);
useEffect(() => {
onChangePosition && onChangePosition(position);
}, [position, onChangePosition]);
return (
<Box
style={{
position: "fixed",
left: position.x,
top: position.y,
display: "grid",
gridTemplateColumns: `${lineWidth * 2}px auto ${lineWidth * 2}px`,
gridTemplateRows: `${lineWidth * 2}px auto ${lineWidth * 2}px`,
zIndex: 2147483647,
}}
>
<Pointer
direction="TopLeft"
style={{
transform: `translate(${lineWidth}px, ${lineWidth}px)`,
cursor: "nw-resize",
}}
{...opts}
/>
<Pointer
direction="Top"
style={{
margin: `0 ${lineWidth}px`,
transform: `translate(0px, ${lineWidth}px)`,
cursor: "row-resize",
}}
{...opts}
/>
<Pointer
direction="TopRight"
style={{
transform: `translate(-${lineWidth}px, ${lineWidth}px)`,
cursor: "ne-resize",
}}
{...opts}
/>
<Pointer
direction="Left"
style={{
margin: `${lineWidth}px 0`,
transform: `translate(${lineWidth}px, 0px)`,
cursor: "col-resize",
}}
{...opts}
/>
<Paper elevation={4}>
<Pointer direction="Header" style={{ cursor: "move" }} {...opts}>
{header}
</Pointer>
<div
style={{
width: size.w,
height: size.h,
overflow: "hidden auto",
}}
>
{children}
</div>
</Paper>
<Pointer
direction="Right"
style={{
margin: `${lineWidth}px 0`,
transform: `translate(-${lineWidth}px, 0px)`,
cursor: "col-resize",
}}
{...opts}
/>
<Pointer
direction="BottomLeft"
style={{
transform: `translate(${lineWidth}px, -${lineWidth}px)`,
cursor: "ne-resize",
}}
{...opts}
/>
<Pointer
direction="Bottom"
style={{
margin: `0 ${lineWidth}px`,
transform: `translate(0px, -${lineWidth}px)`,
cursor: "row-resize",
}}
{...opts}
/>
<Pointer
direction="BottomRight"
style={{
transform: `translate(-${lineWidth}px, -${lineWidth}px)`,
cursor: "nw-resize",
}}
{...opts}
/>
</Box>
);
}

View File

@@ -0,0 +1,31 @@
import IconButton from "@mui/material/IconButton";
import FavoriteIcon from "@mui/icons-material/Favorite";
import FavoriteBorderIcon from "@mui/icons-material/FavoriteBorder";
import { useState } from "react";
import { useFavWords } from "../../hooks/FavWords";
export default function FavBtn({ word }) {
const { favWords, toggleFav } = useFavWords();
const [loading, setLoading] = useState(false);
const handleClick = async () => {
try {
setLoading(true);
await toggleFav(word);
} catch (err) {
console.log("[set fav]", err);
} finally {
setLoading(false);
}
};
return (
<IconButton disabled={loading} size="small" onClick={handleClick}>
{favWords[word] ? (
<FavoriteIcon fontSize="inherit" />
) : (
<FavoriteBorderIcon fontSize="inherit" />
)}
</IconButton>
);
}

View File

@@ -0,0 +1,190 @@
import { SettingProvider } from "../../hooks/Setting";
import ThemeProvider from "../../hooks/Theme";
import DraggableResizable from "./DraggableResizable";
import Header from "../Popup/Header";
import Stack from "@mui/material/Stack";
import TextField from "@mui/material/TextField";
import MenuItem from "@mui/material/MenuItem";
import Grid from "@mui/material/Grid";
import Box from "@mui/material/Box";
import Divider from "@mui/material/Divider";
import IconButton from "@mui/material/IconButton";
import DoneIcon from "@mui/icons-material/Done";
import { useI18n } from "../../hooks/I18n";
import { OPT_TRANS_ALL, OPT_LANGS_FROM, OPT_LANGS_TO } from "../../config";
import { useState, useRef } from "react";
import TranCont from "./TranCont";
import CopyBtn from "./CopyBtn";
function TranForm({ text, setText, tranboxSetting, transApis }) {
const i18n = useI18n();
const [editMode, setEditMode] = useState(false);
const [editText, setEditText] = useState("");
const [translator, setTranslator] = useState(tranboxSetting.translator);
const [fromLang, setFromLang] = useState(tranboxSetting.fromLang);
const [toLang, setToLang] = useState(tranboxSetting.toLang);
const inputRef = useRef(null);
return (
<Stack sx={{ p: 2 }} spacing={2}>
<Box>
<Grid container spacing={2} columns={12}>
<Grid item xs={4} sm={4} md={4} lg={4}>
<TextField
select
SelectProps={{ MenuProps: { disablePortal: true } }}
fullWidth
size="small"
name="fromLang"
value={fromLang}
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>
<TextField
label={i18n("original_text")}
inputRef={inputRef}
fullWidth
multiline
value={editMode ? editText : text}
disabled={!editMode}
onChange={(e) => {
setEditText(e.target.value);
}}
onClick={() => {
setEditMode(true);
setEditText(text);
const timer = setTimeout(() => {
clearTimeout(timer);
inputRef.current?.focus();
}, 100);
}}
onBlur={() => {
setEditMode(false);
setText(editText.trim());
}}
InputProps={{
endAdornment: (
<Stack
direction="row"
sx={{
position: "absolute",
right: 0,
top: 0,
}}
>
{editMode ? (
<IconButton
size="small"
onClick={(e) => {
e.stopPropagation();
}}
>
<DoneIcon fontSize="inherit" />
</IconButton>
) : (
<CopyBtn text={text} />
)}
</Stack>
),
}}
/>
</Box>
<TranCont
text={text}
translator={translator}
fromLang={fromLang}
toLang={toLang}
transApis={transApis}
/>
</Stack>
);
}
export default function TranBox({
text,
setText,
setShowBox,
tranboxSetting,
transApis,
boxSize,
setBoxSize,
boxPosition,
setBoxPosition,
}) {
return (
<SettingProvider>
<ThemeProvider>
<DraggableResizable
defaultPosition={boxPosition}
defaultSize={boxSize}
header={<Header setShowPopup={setShowBox} />}
onChangeSize={setBoxSize}
onChangePosition={setBoxPosition}
>
<Divider />
<TranForm
text={text}
setText={setText}
tranboxSetting={tranboxSetting}
transApis={transApis}
/>
</DraggableResizable>
</ThemeProvider>
</SettingProvider>
);
}

View File

@@ -0,0 +1,36 @@
export default function TranBtn({ onClick, position, tranboxSetting }) {
return (
<div
style={{
cursor: "pointer",
position: "fixed",
left: position.x + tranboxSetting.btnOffsetX,
top: position.y + tranboxSetting.btnOffsetY,
zIndex: 2147483647,
}}
onClick={onClick}
onMouseUp={(e) => {
e.stopPropagation();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 32 32"
version="1.1"
>
<path
d="M0 0 C10.56 0 21.12 0 32 0 C32 10.56 32 21.12 32 32 C21.44 32 10.88 32 0 32 C0 21.44 0 10.88 0 0 Z "
fill="#209CEE"
transform="translate(0,0)"
/>
<path
d="M0 0 C0.66 0 1.32 0 2 0 C2 2.97 2 5.94 2 9 C2.969375 8.2575 3.93875 7.515 4.9375 6.75 C5.48277344 6.33234375 6.02804688 5.9146875 6.58984375 5.484375 C8.39053593 3.83283924 8.39053593 3.83283924 9 0 C13.95 0 18.9 0 24 0 C24 0.99 24 1.98 24 3 C22.68 3 21.36 3 20 3 C20 9.27 20 15.54 20 22 C19.01 22 18.02 22 17 22 C17 15.73 17 9.46 17 3 C15.35 3 13.7 3 12 3 C11.731875 3.598125 11.46375 4.19625 11.1875 4.8125 C10.01506533 6.97224808 8.80630718 8.35790256 7 10 C8.01790655 12.27071461 8.77442829 13.80784632 10.6875 15.4375 C11.120625 15.953125 11.55375 16.46875 12 17 C11.6875 19.6875 11.6875 19.6875 11 22 C10.34 22 9.68 22 9 22 C8.773125 21.236875 8.54625 20.47375 8.3125 19.6875 C6.73268318 16.45263699 5.16717283 15.58358642 2 14 C2 16.64 2 19.28 2 22 C1.34 22 0.68 22 0 22 C0 14.74 0 7.48 0 0 Z "
fill="#E9F5FD"
transform="translate(4,5)"
/>
</svg>
</div>
);
}

View File

@@ -0,0 +1,99 @@
import TextField from "@mui/material/TextField";
import Box from "@mui/material/Box";
import Alert from "@mui/material/Alert";
import CircularProgress from "@mui/material/CircularProgress";
import Stack from "@mui/material/Stack";
import { useI18n } from "../../hooks/I18n";
import { DEFAULT_TRANS_APIS, OPT_TRANS_BAIDU } from "../../config";
import { useEffect, useState } from "react";
import { apiTranslate } from "../../apis";
import { isValidWord } from "../../libs/utils";
import CopyBtn from "./CopyBtn";
import DictCont from "./DictCont";
export default function TranCont({
text,
translator,
fromLang,
toLang,
transApis,
}) {
const i18n = useI18n();
const [trText, setTrText] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [dictResult, setDictResult] = useState(null);
useEffect(() => {
(async () => {
try {
setLoading(true);
setTrText("");
setError("");
setDictResult(null);
const apiSetting =
transApis[translator] || DEFAULT_TRANS_APIS[translator];
const tranRes = await apiTranslate({
text,
translator,
fromLang,
toLang,
apiSetting,
});
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",
});
setDictResult(dictRes[2].dict_result);
}
}
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
})();
}, [text, translator, fromLang, toLang, transApis]);
return (
<>
<Box>
<TextField
label={i18n("translated_text")}
// disabled
fullWidth
multiline
value={trText}
InputProps={{
endAdornment: (
<Stack
direction="row"
sx={{
position: "absolute",
right: 0,
top: 0,
}}
>
<CopyBtn text={trText} />
</Stack>
),
}}
/>
</Box>
{loading && <CircularProgress size={24} />}
{error && <Alert severity="error">{error}</Alert>}
{dictResult && <DictCont dictResult={dictResult} />}
</>
);
}

View File

@@ -0,0 +1,85 @@
import { useState, useEffect } from "react";
import TranBtn from "./TranBtn";
import TranBox from "./TranBox";
import { shortcutRegister } from "../../libs/shortcut";
export default function Slection({ tranboxSetting, transApis }) {
const [showBox, setShowBox] = useState(false);
const [showBtn, setShowBtn] = useState(false);
const [selectedText, setSelText] = useState("");
const [text, setText] = useState("");
const [position, setPosition] = useState({ x: 0, y: 0 });
const [boxSize, setBoxSize] = useState({ w: 600, h: 400 });
const [boxPosition, setBoxPosition] = useState({
x: (window.innerWidth - 600) / 2,
y: (window.innerHeight - 400) / 2,
});
function handleMouseup(e) {
const selectedText = window.getSelection()?.toString()?.trim() || "";
if (!selectedText) {
setShowBtn(false);
return;
}
setSelText(selectedText);
setShowBtn(true);
setPosition({ x: e.clientX, y: e.clientY });
}
const handleClick = (e) => {
e.stopPropagation();
setShowBtn(false);
setText(selectedText);
if (!showBox) {
setShowBox(true);
}
};
useEffect(() => {
window.addEventListener("mouseup", handleMouseup);
return () => {
window.removeEventListener("mouseup", handleMouseup);
};
}, []);
useEffect(() => {
const clearShortcut = shortcutRegister(
tranboxSetting.tranboxShortcut,
() => {
setShowBox((pre) => !pre);
}
);
return () => {
clearShortcut();
};
}, [tranboxSetting.tranboxShortcut, setShowBox]);
return (
<>
{showBox && (
<TranBox
text={text}
setText={setText}
boxSize={boxSize}
setBoxSize={setBoxSize}
boxPosition={boxPosition}
setBoxPosition={setBoxPosition}
tranboxSetting={tranboxSetting}
transApis={transApis}
setShowBox={setShowBox}
/>
)}
{showBtn && (
<TranBtn
position={position}
tranboxSetting={tranboxSetting}
onClick={handleClick}
/>
)}
</>
);
}

15242
yarn.lock

File diff suppressed because it is too large Load Diff