Compare commits

..

349 Commits

Author SHA1 Message Date
Gabe Yuan
72ccfc8aec v1.8.11 2024-05-23 20:06:48 +08:00
Gabe Yuan
d117c5dc10 feat: baidu dict can be disabled 2024-05-23 00:08:10 +08:00
Gabe Yuan
9312783f44 feat: lang detector can be selected 2024-05-22 23:33:30 +08:00
Gabe Yuan
e5b16ebfd3 Merge remote-tracking branch 'origin/master' into dev 2024-05-22 10:19:04 +08:00
Gabe Yuan
5d1d65c2d3 feat: the temperature and maxTokens of the openai can be configured 2024-05-21 23:15:46 +08:00
Gabe
9ca1309cec Merge pull request #126 from kebyn/master
fix: option translation
2024-05-21 20:02:53 +08:00
kebyn
a03afc05f5 fix: option translation 2024-05-21 08:56:09 +00:00
Gabe Yuan
0198963584 fix: deeplx: replace auto to blank string 2024-05-21 11:55:17 +08:00
Gabe Yuan
58e745d967 v1.8.10 2024-05-17 10:38:11 +08:00
Gabe Yuan
377e347d68 feat: support translate hooks 2024-05-15 11:07:13 +08:00
Gabe Yuan
bac0704d3d feat: download and upload settings 2024-05-12 20:24:40 +08:00
Gabe Yuan
d2ff46edf6 fix: show full gemini url 2024-05-12 16:25:20 +08:00
Gabe Yuan
f908372b4e feat: support hook for custom api 2024-05-12 16:10:11 +08:00
Gabe Yuan
5d44ff4913 v1.8.9 2024-04-28 22:23:53 +08:00
Gabe Yuan
4c9aa66048 feat: support ollama api 2024-04-28 21:45:20 +08:00
Gabe Yuan
b6a09b99ab feat: support ollama api 2024-04-28 21:43:20 +08:00
Gabe Yuan
3a0dcb1a52 feat: add more openai translator 2024-04-28 16:58:09 +08:00
Gabe Yuan
5015503b4c feat: hide transbox header when mouseleave 2024-04-28 14:56:49 +08:00
Gabe Yuan
16423feea4 fix: update readme 2024-04-21 22:21:29 +08:00
Gabe Yuan
9703514698 v1.8.8 2024-04-21 19:25:00 +08:00
Gabe Yuan
de7a97fb76 feat: tranbox offset 2024-04-21 19:19:06 +08:00
Gabe Yuan
319aaf8132 fix: move taskpool from background to content 2024-04-21 13:16:44 +08:00
Gabe Yuan
74bc58ba91 feat: transbox follow selection 2024-04-20 18:07:16 +08:00
Gabe Yuan
d622db0d7c fix: i18n menu command for userscript 2024-04-20 15:54:41 +08:00
Gabe Yuan
de1ddf2362 fix: stopPropagation when close tranbox 2024-04-20 15:12:25 +08:00
Gabe Yuan
32c0fc860b fix: update readme 2024-04-20 14:11:32 +08:00
Gabe Yuan
1938f432dd feat: support multi url for DEEPLX 2024-04-20 14:01:34 +08:00
Gabe Yuan
a5cfb0ca1d fix: fetch pool retry 2024-04-20 11:52:16 +08:00
Gabe Yuan
a172234fb0 fix: niutrans i18n text 2024-04-18 12:31:16 +08:00
Gabe Yuan
63f989b31a v1.8.7 2024-04-18 10:07:53 +08:00
Gabe Yuan
2ae5d01d5c fix: custom option 2024-04-18 09:48:07 +08:00
Gabe Yuan
130f1deed1 fix: remove encodeURIComponent 2024-04-18 00:31:36 +08:00
Gabe Yuan
5880d85b48 fix: encodeURIComponent text 2024-04-17 23:44:53 +08:00
Gabe Yuan
9455670e80 feat: custom request 2024-04-17 22:35:12 +08:00
Gabe Yuan
e369321c66 feat: custom request 2024-04-17 17:38:54 +08:00
Gabe Yuan
efc51b0d46 feat: extend styles for transbox 2024-04-17 15:35:44 +08:00
Gabe Yuan
d6f3b23b88 fix: tranbox ui 2024-04-17 10:31:37 +08:00
Gabe Yuan
0a4fa7b9f8 fix: tranbox ui 2024-04-17 10:03:56 +08:00
Gabe Yuan
2b3e4a8d25 fix: reaplce loading button 2024-04-16 16:39:11 +08:00
Gabe Yuan
bf3a16f96d feat: export word & translation 2024-04-16 16:29:59 +08:00
Gabe Yuan
b416e72820 fix: transbox ui 2024-04-16 15:22:27 +08:00
Gabe Yuan
ca84bdb227 feat: tranbox hover trigger 2024-04-16 12:47:55 +08:00
Gabe Yuan
148a4e97a6 fix: tranbox ui 2024-04-16 11:25:04 +08:00
Gabe Yuan
a13493ebc2 feat: simple style tranbox 2024-04-16 00:54:37 +08:00
Gabe Yuan
ce4ac79e5f fix: optimization tranbox 2024-04-15 18:04:35 +08:00
Gabe Yuan
8f76ea49e7 fix: loading icon 2024-04-13 21:23:58 +08:00
Gabe Yuan
923d3293cd fix: limit selection btn in window & click to hide 2024-04-12 22:28:40 +08:00
Gabe Yuan
7379ff8d15 v1.8.6 2024-04-12 14:39:17 +08:00
Gabe Yuan
18ebec350d fix: clean env 2024-04-12 14:33:29 +08:00
Gabe Yuan
3b0cbc53aa fix: response err data: url 2024-04-12 11:47:22 +08:00
Gabe Yuan
f00e8ffa4d feat: add niutrans api 2024-04-12 11:31:01 +08:00
Gabe Yuan
d6f7aad1c3 fix: utils func 2024-04-11 10:44:25 +08:00
Gabe Yuan
092ea6e836 fix: custom api 2024-04-10 13:37:16 +08:00
Gabe Yuan
d565e2464a feat: tranbox: mobile support 2024-04-07 16:55:54 +08:00
Gabe Yuan
2f5d875c47 v1.8.5 2024-04-02 17:01:34 +08:00
Gabe Yuan
fdb2ddc5f7 fix: rules 2024-04-01 12:35:54 +08:00
Gabe Yuan
7a12c5315a feat: close tranbox when click away 2024-04-01 12:25:59 +08:00
Gabe Yuan
60d788288d feat: add more custom apis 2024-04-01 11:50:29 +08:00
Gabe Yuan
dc3c510d57 fix: update observer callback 2024-03-27 14:24:41 +08:00
Gabe Yuan
ec6a49f01e fix: update readme 2024-03-26 17:50:56 +08:00
Gabe Yuan
2b9bfbc20d feat: csp list 2024-03-26 12:42:39 +08:00
Gabe Yuan
06a51df834 feat: csp list 2024-03-26 12:05:35 +08:00
Gabe Yuan
6fa183dc56 feat: csp list 2024-03-26 12:00:09 +08:00
Gabe Yuan
b3cb4049ed fix: tranbox input onfocus 2024-03-25 22:46:02 +08:00
Gabe Yuan
602b51b1f5 fix: dict audio 2024-03-25 21:00:39 +08:00
Gabe Yuan
a83039577c feat: word pronunciation supported 2024-03-25 18:14:12 +08:00
Gabe Yuan
1c77a289a6 fix: upgrade dependencies 2024-03-21 23:19:15 +08:00
Gabe Yuan
6278b9124d v1.8.4 2024-03-21 16:15:45 +08:00
Gabe Yuan
f94cafdbcc fix: i18n text 2024-03-21 16:12:38 +08:00
Gabe Yuan
e13da2caba fix: i18n text 2024-03-21 16:07:56 +08:00
Gabe Yuan
d833fa8dfd fix: build cmd 2024-03-21 15:48:54 +08:00
Gabe Yuan
c921cc59b9 feat: add pack cmd 2024-03-21 15:38:28 +08:00
Gabe Yuan
7a2c594324 fix: setting ui 2024-03-21 15:21:38 +08:00
Gabe Yuan
0eeb9c2fbf feat: move fetch setting to apis 2024-03-21 15:07:50 +08:00
Gabe Yuan
ac921cd5a0 feat: move fixer to rules 2024-03-21 11:40:47 +08:00
Gabe Yuan
3ea14c1687 fix: webfix 2024-03-21 10:15:23 +08:00
Gabe Yuan
6e927473b9 feat: add log function 2024-03-19 18:07:18 +08:00
Gabe Yuan
1d9e9c1b7d fix: change runtime.onMessage to async fn 2024-03-19 17:28:07 +08:00
Gabe Yuan
54a6189b0c fix: sendResponse ok 2024-03-19 15:37:23 +08:00
Gabe Yuan
85aa9f39c2 fix: add timeout for fetch 2024-03-19 14:27:26 +08:00
Gabe Yuan
bda83ce76e fix: add timeout for fetch 2024-03-19 14:25:37 +08:00
Gabe Yuan
9ee4c20250 fix: sync rules 2024-03-19 14:08:23 +08:00
Gabe Yuan
fbc70e43e3 feat: add baidu suggest 2024-03-19 11:48:30 +08:00
Gabe Yuan
bc4b4a2171 fix: i118n text 2024-03-19 10:52:32 +08:00
Gabe Yuan
96f9bf6f6f fix: replace default transtag to span 2024-03-19 10:00:00 +08:00
Gabe Yuan
9f0986536a feat: wrap trems by <i> tag 2024-03-19 09:55:56 +08:00
Gabe Yuan
f668aa7acd fix: hide transbox 2024-03-18 17:55:32 +08:00
Gabe Yuan
fc50f4784a fix: hide transbox 2024-03-18 17:52:13 +08:00
Gabe Yuan
1fa58cad31 fix: rules 2024-03-18 11:02:33 +08:00
Gabe Yuan
469e62557c fix: icon & text 2024-03-18 10:48:38 +08:00
Gabe Yuan
75830aaea7 feat: add translate interval setting 2024-03-17 12:09:32 +08:00
Gabe Yuan
61ef5df559 fix: rules 2024-03-17 11:35:43 +08:00
Gabe Yuan
14b5ba9c4c feat: move settings to rule 2024-03-16 23:37:27 +08:00
Gabe Yuan
9e9c56a3b4 fix: inject js/css 2024-03-15 17:38:24 +08:00
Gabe Yuan
3a79f55614 fix: update rules 2024-03-15 16:59:53 +08:00
Gabe Yuan
af25ee5c11 fix: showMore 2024-03-15 16:19:08 +08:00
Gabe Yuan
6dd581d5e2 fix: default trans tag 2024-03-15 16:00:20 +08:00
Gabe Yuan
746ec019c4 fix: inject js/css 2024-03-15 15:47:57 +08:00
Gabe Yuan
2b70f28b0b fix: help text 2024-03-15 10:47:35 +08:00
Gabe Yuan
45127646e8 fix: inject js/css 2024-03-15 10:35:30 +08:00
Gabe Yuan
83e9c1dd97 feat: inject user js/css 2024-03-14 18:08:02 +08:00
Gabe Yuan
2eabb7d5ac feat: inject user js/css 2024-03-14 18:06:28 +08:00
Gabe Yuan
9d4c596b4b fix: getCurTab 2024-03-14 16:28:32 +08:00
Gabe Yuan
cc38ab6c45 fix: styledSpan & transOnly 2024-03-14 16:26:17 +08:00
Gabe Yuan
586fa09c7b fix: clean content.html 2024-03-14 11:54:34 +08:00
Gabe Yuan
0c45bc5ea8 fix: clean comment code 2024-03-14 11:50:31 +08:00
Gabe Yuan
9d9c0633f0 feat: transTag && transOnly 2024-03-13 16:35:40 +08:00
Gabe Yuan
47f9635b10 v1.8.3 2024-02-26 16:42:36 +08:00
Gabe Yuan
68088f5e17 fix: remove excess line breaks 2024-02-26 16:35:29 +08:00
Gabe Yuan
77a37cc6df fix: remove excess line breaks 2024-02-26 16:34:53 +08:00
Gabe Yuan
420e59bf81 fix: translate text 2024-02-22 22:57:34 +08:00
Gabe Yuan
dbc5135301 fix: update default blacklist 2024-02-22 17:44:17 +08:00
Gabe Yuan
8c7d6bb552 fix: remove postMessage from iframe 2024-02-22 17:26:22 +08:00
Gabe Yuan
2b5c1952c0 fix: update default blacklist 2024-02-22 11:45:41 +08:00
Gabe Yuan
85a82618e5 fix: contextMenus text 2024-02-21 17:01:11 +08:00
Gabe Yuan
0280ac34c3 fix: dict voice not exist 2024-02-21 15:47:14 +08:00
Gabe Yuan
439900154b fix: dict voice not exist 2024-02-21 15:41:27 +08:00
Gabe Yuan
4a6e902684 fix: update readme 2024-02-06 10:22:56 +08:00
Gabe Yuan
71bbd2e54a v1.8.1 2024-02-06 10:09:33 +08:00
Gabe Yuan
3083d8e147 feat: context menu type 2024-02-05 11:28:34 +08:00
Gabe Yuan
e74883e9c2 fix: contextMenus: duplicate id err 2024-02-05 10:51:42 +08:00
Gabe Yuan
0816a9d167 fix: add BLOCKQUOTE to webfix 2024-02-05 10:02:30 +08:00
Gabe Yuan
4b3e91fa84 v1.8.1 2024-02-02 15:45:33 +08:00
Gabe Yuan
0973a0b60e fix: some js syntax 2024-02-02 15:44:44 +08:00
Gabe Yuan
de5f61126d fix: terms hepler text 2024-02-02 12:35:56 +08:00
Gabe Yuan
0c20ca761f fix: update toggle_translate CN text 2024-02-02 12:22:08 +08:00
Gabe Yuan
4bce56207e fix: optimize terms function 2024-02-02 12:10:27 +08:00
Gabe Yuan
dca54e0033 feat: setting: translate page title 2024-02-02 11:20:39 +08:00
Gabe Yuan
309646bf1d feat: setting: translate page title 2024-02-02 11:13:41 +08:00
Gabe Yuan
18b9961b39 fix: try add context menux on startup 2024-02-02 10:49:15 +08:00
Gabe Yuan
1e51ff17f2 v1.8.0 2024-01-22 13:19:37 +08:00
Gabe Yuan
63b5f707e2 fix: update ui when shortcut changed 2024-01-22 13:11:02 +08:00
Gabe Yuan
30efb6ee7a fix: title translate 2024-01-19 21:03:51 +08:00
Gabe Yuan
61b017618a feat: supported translation all when page opened 2024-01-19 17:55:18 +08:00
Gabe Yuan
1e0397adc9 feat: translate page title 2024-01-19 17:18:05 +08:00
Gabe Yuan
48b34bf95f fix: save new rule with hostname 2024-01-19 16:13:46 +08:00
Gabe Yuan
d5fc69e210 feat: support custom terms 2024-01-19 16:02:53 +08:00
Gabe Yuan
59f9dd697f fix: update baidu translate api 2024-01-18 15:26:37 +08:00
Gabe Yuan
c9d72323f1 keep selector support for sub-element 2024-01-12 16:04:34 +08:00
Gabe Yuan
e87f7f3abe fix: help text 2024-01-12 09:42:49 +08:00
Gabe Yuan
82ebbcb6d6 v1.7.16 2024-01-04 15:55:28 +08:00
Gabe Yuan
2db11070c5 fix: move clear_cache button to bottom of popup 2024-01-04 15:41:20 +08:00
Gabe Yuan
5efd2517e7 fix: tranbox shortcut in usserscript 2024-01-04 12:18:36 +08:00
Gabe Yuan
c0ba654678 fix: remove bgcolor input from popup 2024-01-04 10:49:44 +08:00
Gabe Yuan
546a5a549b fix: comment text 2024-01-04 10:39:40 +08:00
Gabe Yuan
cbf02c34e3 fix: remove position limit for tranbtn 2024-01-04 10:34:12 +08:00
Gabe Yuan
74a7258f10 fix: optimize key pick 2024-01-04 09:40:03 +08:00
Gabe Yuan
1006c044bc fix: update readme 2024-01-03 15:48:34 +08:00
Gabe
ef4ea719f3 fix: Update README.md 2024-01-03 15:47:24 +08:00
Gabe
8b34afe69f fix: Update README.md 2024-01-03 15:25:40 +08:00
Gabe Yuan
01292af298 feat: move open_tranbox shortcurt to browser commands 2024-01-03 13:10:02 +08:00
Gabe Yuan
cff8b2fe39 feat: move open_tranbox shortcurt to browser commands 2024-01-03 11:59:41 +08:00
Gabe Yuan
2cb20b5cc0 fix: update rules 2024-01-03 10:43:02 +08:00
Gabe Yuan
8f2aed18fe fix: contextMenus created on page and selection 2024-01-03 10:32:11 +08:00
Gabe Yuan
d85831cc9a fix: keep the translated image size unchanged 2024-01-03 10:10:54 +08:00
Gabe Yuan
55dc3a5556 feat: keep unchanged elements 2024-01-02 17:57:04 +08:00
Gabe Yuan
591afe08bd feat: keep unchanged elements 2024-01-02 17:55:59 +08:00
Gabe Yuan
748f2002ab fix: run webfix before translate 2023-12-27 15:44:02 +08:00
Gabe Yuan
d2d18a2384 fix: instagram input translate: addEventListener keyup 2023-12-27 11:25:53 +08:00
Gabe Yuan
35f4fa6aa7 fix: register menu command when hide fab button 2023-12-26 10:08:36 +08:00
Gabe Yuan
66fc2d22ed feat: toto: selection translation on mobile support 2023-12-25 17:25:00 +08:00
Gabe Yuan
16cf9ee1ed feat: toto: selection translation on mobile support 2023-12-25 17:21:59 +08:00
Gabe Yuan
d9d97bf14c fix: selection button position 2023-12-25 14:42:13 +08:00
Gabe Yuan
dc811bd3c7 feat: selection translation on mobile support 2023-12-25 11:50:30 +08:00
Gabe Yuan
b939d1849a feat: multi key calling support 2023-12-22 11:35:46 +08:00
Gabe Yuan
beca31f55d v1.7.15 2023-12-21 14:24:08 +08:00
Gabe Yuan
c7df103950 feat: add gemini translator 2023-12-21 14:15:14 +08:00
Gabe Yuan
4bf7972ad5 fix: fab button fontsize 2023-12-19 15:34:40 +08:00
Gabe Yuan
534eaed1ed refactor: input translate 2023-12-18 11:46:37 +08:00
Gabe Yuan
7e014e7385 fix: contextMenus setting 2023-12-15 11:43:01 +08:00
Gabe Yuan
34adb2660b fix: grant GM.unregisterMenuCommand 2023-12-15 10:58:49 +08:00
Gabe Yuan
b6bc165cf0 fix: context munus 2023-12-11 17:26:49 +08:00
Gabe Yuan
bdd5ed7fc7 feat: mutual translation effect with the target language 2023-12-11 15:54:54 +08:00
Gabe Yuan
95d19417c3 fix: touch tap limit 2023-12-11 11:40:20 +08:00
Gabe Yuan
30ebebdd71 fix: tranbtn position: absolute 2023-12-11 11:38:47 +08:00
Gabe Yuan
e9c557776d feat: context menus setting 2023-12-11 11:25:02 +08:00
Gabe Yuan
535a43b698 release: v1.7.14 2023-11-30 16:44:56 +08:00
Gabe Yuan
59752ed4aa fix: set FormControl small size 2023-11-30 15:11:06 +08:00
Gabe Yuan
b3e7b8f3f1 fix: readme 2023-11-28 15:15:02 +08:00
Gabe Yuan
c4e9365512 fix: clipboard.writeText run with async 2023-11-28 14:59:31 +08:00
Gabe Yuan
7d3972d3a8 perf: merge Translate Popup/Selected shortcut 2023-11-28 13:36:40 +08:00
Gabe Yuan
52ca4306fd feat: blockquote style 2023-11-28 11:41:45 +08:00
Gabe Yuan
da368ee612 feat: disable languages 2023-11-28 11:11:59 +08:00
Gabe Yuan
22c50e7765 feat: translate blacklist 2023-11-24 17:07:29 +08:00
Gabe Yuan
7bc39dd1bc fix: default shortcut: open setting page 2023-11-23 17:47:50 +08:00
Gabe Yuan
c80ead6116 v1.7.13 2023-11-22 15:24:54 +08:00
Gabe Yuan
67e76e4009 update readme 2023-11-22 15:08:26 +08:00
Gabe Yuan
b213218a30 update readme 2023-11-22 15:04:52 +08:00
Gabe Yuan
c629a1252c GM registerMenuCommand: open transbox, translate selected 2023-11-22 14:45:01 +08:00
Gabe Yuan
64d2481e93 update rules 2023-11-22 13:38:49 +08:00
Gabe Yuan
e7d6a6add8 update readme 2023-11-22 12:27:38 +08:00
Gabe Yuan
edc25f7da4 add touch option: four finger tap 2023-11-22 11:27:41 +08:00
Gabe Yuan
5bff84ace1 add context menus: open tranbox 2023-11-22 11:02:48 +08:00
Gabe Yuan
f8bfcba317 fix html fontsize 2023-11-22 10:23:14 +08:00
Gabe Yuan
013a05201b add context menus 2023-11-21 11:36:46 +08:00
Gabe Yuan
433e811821 add context menus 2023-11-21 11:20:05 +08:00
Gabe Yuan
df4cfc0fbc update readme 2023-11-17 11:38:42 +08:00
Gabe Yuan
1bfb465fd6 v1.7.12 2023-11-14 22:30:53 +08:00
Gabe Yuan
d5d5ec3fef fix text 2023-11-14 16:55:44 +08:00
Gabe Yuan
0a32f94d32 update webfix 2023-11-14 15:15:19 +08:00
Gabe Yuan
8067f34ce6 update readme 2023-11-14 10:50:48 +08:00
Gabe Yuan
214c189a7c fix text 2023-11-14 10:41:44 +08:00
Gabe Yuan
1f67afc8d8 fix error: addNode.querySelectorAll 2023-11-14 10:26:07 +08:00
Gabe Yuan
7d4af27919 user webfix rules 2023-11-14 10:19:23 +08:00
Gabe Yuan
2d651abfdd user webfix rules... 2023-11-13 18:03:38 +08:00
Gabe Yuan
6e06fe79cd bug fix: repace mouseover to mouseenter 2023-11-13 11:02:25 +08:00
Gabe Yuan
6093577591 sleep mouseup 2023-11-11 17:22:43 +08:00
Gabe Yuan
4b23ee733f touch operation 2023-11-11 16:59:38 +08:00
Gabe Yuan
46428b7c7f touch operation... 2023-11-10 18:00:34 +08:00
Gabe Yuan
6805340a9a remove wrangler script 2023-11-10 10:56:17 +08:00
Gabe Yuan
df36ca8d8b fix bug 2023-11-07 17:57:17 +08:00
Gabe Yuan
fe13de7c30 trans api return early 2023-11-07 17:52:33 +08:00
Gabe Yuan
b00f636b72 fix bug 2023-11-07 17:47:42 +08:00
Gabe Yuan
8d074e63e1 throw fetch error 2023-11-06 15:42:21 +08:00
Gabe Yuan
37989b0089 fix text 2023-11-06 12:06:10 +08:00
Gabe Yuan
477361eb40 replace getElementsByTagName to querySelectorAll 2023-11-05 16:20:44 +08:00
Gabe Yuan
94288b5dc3 show detail error to api test 2023-11-02 23:35:36 +08:00
Gabe Yuan
84de1e0f12 replace getElementsByName to getElementsByTagName 2023-11-02 10:59:26 +08:00
Gabe Yuan
06f93c1c10 repace querySelectorAll to getElementsByName 2023-11-02 10:56:46 +08:00
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
93 changed files with 18010 additions and 17276 deletions

12
.env
View File

@@ -2,28 +2,18 @@ GENERATE_SOURCEMAP=false
REACT_APP_NAME=KISS Translator
REACT_APP_NAME_CN=简约翻译
REACT_APP_VERSION=1.6.10
REACT_APP_VERSION=1.8.11
REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator
REACT_APP_OPTIONSPAGE=https://fishjar.github.io/kiss-translator/options.html
REACT_APP_OPTIONSPAGE2=https://kiss-translator.rayjar.com/options
REACT_APP_OPTIONSPAGE_DEV=http://localhost:3000/options.html
REACT_APP_LOGOURL=https://fishjar.github.io/kiss-translator/images/logo192.png
REACT_APP_LOGOURL2=https://kiss-translator.rayjar.com/images/logo192.png
REACT_APP_RULESURL=https://fishjar.github.io/kiss-rules/kiss-rules.json
REACT_APP_RULESURL_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_VERSIONFILE2=https://kiss-translator.rayjar.com/version.txt
REACT_APP_USERSCRIPT_DOWNLOADURL=https://fishjar.github.io/kiss-translator/kiss-translator.user.js
REACT_APP_USERSCRIPT_DOWNLOADURL2=https://kiss-translator.rayjar.com/kiss-translator.user.js
REACT_APP_USERSCRIPT_IOS_DOWNLOADURL=https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js
REACT_APP_USERSCRIPT_IOS_DOWNLOADURL2=https://kiss-translator.rayjar.com/kiss-translator-ios-safari.user.js

View File

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

View File

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

View File

@@ -1,24 +1,63 @@
# KISS Translator
A minimalist [bilingual translation Extension & Greasemonkey Script](https://github.com/fishjar/kiss-translator).
English | [简体中文](README.md)
A simple, open source [bilingual translation extension & Greasemonkey script](https://github.com/fishjar/kiss-translator).
[kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f)
## Inspiration
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.
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
- [x] Keep it simple, smart
- [x] Open source
- [x] Adapt to common browsers
- [x] Chrome/Edge/Firefox/Kiwi/Orion
- [ ] Safari
- [x] Supports multiple translation services
- [x] Google/Microsoft/DeepL/NiuTrans/OpenAI/Gemini/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] Customized terminology
- [x] Custom translation style
- [x] Custom shortcut keys
- `Alt+Q` Toggle Translation
- `Alt+C` Toggle Styles
- `Alt+K` Open Setting Popup
- `Alt+S` Open Translate Popup / Translate Selected Text
- `Alt+O` Open Options Page
- `Alt+I` Input Box Translation
## Associated ProjectS
## Install
> Note: For the following reasons, it is recommended to use browser extensions first
>
> - Browser extensions have more complete functions (local language recognition, context menu, etc.)
> - Grease Monkey script will encounter more usage problems (cross domain issues, script conflicts, etc.)
- [x] Browser extension
- [x] Chrome [Installation address](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof?hl=zh-CN)
- [x] Kiwi (Android)
- [x] Orion (iOS)
- [x] Edge [Installation address](https://microsoftedge.microsoft.com/addons/detail/%E7%AE%80%E7%BA%A6%E7%BF%BB%E8%AF%91/jemckldkclkinpjighnoilpbldbdmmlh?hl=zh-CN)
- [x] 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](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)
- [Greasy Fork](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator)
- [x] iOS Safari ([Userscripts Safari](https://github.com/quoid/userscripts)) [Installation link](https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js)
## Associated Projects
- Data synchronization service: [https://github.com/fishjar/kiss-worker](https://github.com/fishjar/kiss-worker)
- Data synchronization service available for this project.
@@ -27,9 +66,6 @@ If you also like a little more simplicity, welcome to pick it up.
- 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.
@@ -38,50 +74,19 @@ If you also like a little more simplicity, welcome to pick it up.
- Supports query of English words, sentences and Chinese characters.
- Supports history records and word collections.
## Description
### Support shortcut keys
- `Alt+Q` Toggle Translation
- `Alt+C` Toggle Styles
- `Alt+K` Open Menu
## 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] DeepL
- [x] OpenAI
- [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] Data Synchronization Function
- [x] Greasemonkey Script ([Setting Page 1](https://fishjar.github.io/kiss-translator/options.html)、[Setting Page 2](https://kiss-translator.rayjar.com/options))
- [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)
- [x] [Violentmonkey](https://violentmonkey.github.io/) (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)
- [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)
## Guide
## Development Guidelines
```sh
git clone https://github.com/fishjar/kiss-translator.git
cd kiss-translator
yarn install
yarn build
pnpm install
pnpm build
```
## Discussion
- Join [Telegram Group](https://t.me/+RRCu_4oNwrM2NmFl)
## Appreciate
![appreciate](https://github.com/fishjar/kiss-translator/assets/1157624/ebaecabe-2934-4172-8085-af236f5ee399)

109
README.md
View File

@@ -1,22 +1,61 @@
# 简约翻译
一个简约的 [双语网页翻译扩展 & 油猴脚本](https://github.com/fishjar/kiss-translator)。
[English](README.en.md) | 简体中文
一个简约、开源的 [双语对照翻译扩展 & 油猴脚本](https://github.com/fishjar/kiss-translator)。
[kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f)
## 缘由
## 特性
本项目灵感来源于 [Immersive Translate](https://github.com/immersive-translate/immersive-translate),在试用了后,发现搭配本人早前开发的 [网页划词翻译扩展](https://github.com/fishjar/kiss-dictionary) 一起使用,刚好形成很好补充。
- [x] 保持简约
- [x] 开放源代码
- [x] 适配常见浏览器
- [x] Chrome/Edge/Firefox/Kiwi/Orion
- [ ] Safari
- [x] 支持多种翻译服务
- [x] Google/Microsoft/DeepL/NiuTrans/OpenAI/Gemini/CloudflareAI/Baidu/Tencent
- [x] 自定义翻译接口
- [x] 覆盖常见翻译场景
- [x] 网页双语对照翻译
- [x] 输入框翻译
- [x] 划词翻译
- [x] 收藏词汇
- [x] 鼠标悬停翻译
- [x] YouTube 字幕翻译
- [x] 跨客户端数据同步
- [x] KISS-Workercloudflare/docker
- [x] WebDAV
- [x] 自定义翻译规则
- [x] 规则订阅/规则分享
- [x] 自定义专业术语
- [x] 自定义译文样式
- [x] 自定义快捷键
- `Alt+Q` 开启翻译
- `Alt+C` 切换样式
- `Alt+K` 打开设置弹窗
- `Alt+S` 打开翻译弹窗/翻译选中文字
- `Alt+O` 打开设置页面
- `Alt+I` 输入框翻译
但该扩展的功能对我来说有些繁杂了,而且只提供编译混淆后的安装包,没有提供源代码,无法满足我的一些个性化定制需求。
## 安装
恰巧本人对翻译类工具有些执念,本着`“自用为主,能用就行”`的理念,于是动手撸了一个,目前初版完成,基本达到个人使用需求。
> 注:基于以下原因,建议优先使用浏览器扩展
>
> - 浏览器扩展的功能更完整(本地语言识别、右键菜单等)
> - 油猴脚本会遇到更多使用上的问题(跨域问题、脚本冲突等)
如果你也喜欢简约一点的,欢迎自取。
## 特点
- 保持简约
- [x] 浏览器扩展
- [x] Chrome [安装地址](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof?hl=zh-CN)
- [x] Kiwi (Android)
- [x] Orion (iOS)
- [x] Edge [安装地址](https://microsoftedge.microsoft.com/addons/detail/%E7%AE%80%E7%BA%A6%E7%BF%BB%E8%AF%91/jemckldkclkinpjighnoilpbldbdmmlh?hl=zh-CN)
- [x] Firefox [安装地址](https://addons.mozilla.org/zh-CN/firefox/addon/kiss-translator/)
- [ ] Safari
- [x] 油猴脚本
- [x] Chrome/Edge/Firefox ([Tampermonkey](https://www.tampermonkey.net/)/[Violentmonkey](https://violentmonkey.github.io/)) [安装链接](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)
- [Greasy Fork](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator)
- [x] iOS Safari ([Userscripts Safari](https://github.com/quoid/userscripts)) [安装链接](https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js)
## 关联项目
@@ -27,61 +66,27 @@
- 社区订阅规则: [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)
- 搭配本项目一起使用的划词翻译插件。
- 支持英文单词、句子、汉字的查询。
- 支持历史记录、单词收藏。
## 简要说明
### 支持快捷键
- `Alt+Q` 开启翻译
- `Alt+C` 切换样式
- `Alt+K` 打开菜单
## 进度
- [x] 提供试用安装包
- [x] 适配浏览器
- [x] Chrome
- [x] Edge
- [x] Firefox
- [ ] Safari
- [x] Kiwi
- [x] 支持翻译服务
- [x] Google
- [x] Microsoft
- [x] DeepL
- [x] OpenAI
- [x] 上架应用市场
- [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] Firefox [安装地址](https://addons.mozilla.org/zh-CN/firefox/addon/kiss-translator/)
- [ ] Safari
- [x] Greasy Fork [安装地址](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator)
- [x] 开放源代码
- [x] 数据同步功能
- [x] 油猴脚本 ([设置页面 1](https://fishjar.github.io/kiss-translator/options.html)、[设置页面 2](https://kiss-translator.rayjar.com/options))
- [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] [Violentmonkey](https://violentmonkey.github.io/) (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)
## 指引
## 开发指引
```sh
git clone https://github.com/fishjar/kiss-translator.git
cd kiss-translator
yarn install
yarn build
pnpm install
pnpm build
```
## 交流
- 加入 [Telegram 群](https://t.me/+RRCu_4oNwrM2NmFl)
## 赞赏
![appreciate](https://github.com/fishjar/kiss-translator/assets/1157624/ebaecabe-2934-4172-8085-af236f5ee399)

View File

@@ -75,7 +75,7 @@ const userscriptWebpack = (config, env) => {
// @name ${process.env.REACT_APP_NAME}
// @namespace ${process.env.REACT_APP_HOMEPAGE}
// @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>
// @homepageURL ${process.env.REACT_APP_HOMEPAGE}
// @license GPL-3.0
@@ -85,6 +85,7 @@ const userscriptWebpack = (config, env) => {
// @updateURL ${process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}
// @grant GM.xmlHttpRequest
// @grant GM.registerMenuCommand
// @grant GM.unregisterMenuCommand
// @grant GM.setValue
// @grant GM.getValue
// @grant GM.deleteValue
@@ -95,14 +96,24 @@ const userscriptWebpack = (config, env) => {
// @connect edge.microsoft.com
// @connect api-free.deepl.com
// @connect api.deepl.com
// @connect www2.deepl.com
// @connect api.openai.com
// @connect generativelanguage.googleapis.com
// @connect openai.azure.com
// @connect workers.dev
// @connect github.io
// @connect githubusercontent.com
// @connect kiss-translator.rayjar.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
// @connect localhost:11434
// @connect 127.0.0.1:11434
// @run-at document-end
// ==/UserScript==

View File

@@ -1,34 +1,38 @@
{
"name": "kiss-translator",
"description": "A minimalist bilingual translation Extension & Greasemonkey Script",
"version": "1.6.10",
"version": "1.8.11",
"author": "Gabe<yugang2002@gmail.com>",
"private": true,
"dependencies": {
"@emotion/cache": "^11.11.0",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.10.8",
"@mui/icons-material": "^5.11.11",
"@mui/material": "^5.11.12",
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.15.15",
"@mui/lab": "5.0.0-alpha.170",
"@mui/material": "^5.15.15",
"query-string": "^8.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^8.0.7",
"react-router-dom": "^6.10.0",
"react-router-dom": "^6.16.0",
"react-scripts": "5.0.1",
"sval": "^0.5.2",
"webdav": "^5.3.0",
"webextension-polyfill": "^0.10.0"
},
"scripts": {
"start": "REACT_APP_CLIENT=web react-app-rewired start",
"start:userscript": "REACT_APP_CLIENT=userscript react-app-rewired start",
"build:chrome": "rm -rf build/chrome && BUILD_PATH=./build/chrome REACT_APP_CLIENT=chrome react-app-rewired build",
"build:chrome": "rm -rf build/chrome && BUILD_PATH=./build/chrome REACT_APP_CLIENT=chrome react-app-rewired build && rm build/chrome/content.html",
"build:edge": "rm -rf build/edge && cp -r build/chrome build/edge",
"build:firefox": "rm -rf build/firefox && cp -r build/chrome build/firefox && cat ./build/firefox/manifest.firefox.json > ./build/firefox/manifest.json",
"build:firefox": "rm -rf build/firefox && cp -r build/chrome build/firefox && cat ./build/firefox/manifest.firefox.json > ./build/firefox/manifest.json && rm build/*/manifest.firefox.json",
"build:web": "rm -rf build/web && BUILD_PATH=./build/web REACT_APP_CLIENT=userscript react-app-rewired build",
"build:userscript-ios": "file1=build/web/kiss-translator.user.js file2=build/web/kiss-translator-ios-safari.user.js && cp $file1 $file2 && sed -i 's|// @grant unsafeWindow|// @inject-into content|g' $file2",
"build:userscript": "rm -rf build/userscript && mkdir build/userscript && cp build/web/*.user.js build/userscript/",
"build:rules": "babel-node src/rules.js",
"build": "yarn build:chrome && yarn build:edge && yarn build:firefox && yarn build:web && yarn build:userscript-ios && yarn build:userscript && yarn build:rules",
"deploy:web": "wrangler pages deploy ./build/web --project-name kiss-translator",
"build": "pnpm build:chrome && pnpm build:edge && pnpm build:firefox && pnpm build:web && pnpm build:userscript-ios && pnpm build:userscript && pnpm build:rules",
"zip": "cd build && zip -r chrome.zip chrome && zip -r edge.zip edge && cd firefox && zip -r ../firefox.zip .",
"test": "react-app-rewired test",
"eject": "react-scripts eject"
},
@@ -39,7 +43,8 @@
],
"globals": {
"GM": true,
"unsafeWindow": true
"unsafeWindow": true,
"globalThis": true
}
},
"browserslist": {
@@ -55,11 +60,10 @@
]
},
"devDependencies": {
"@babel/core": "^7.22.10",
"@babel/node": "^7.22.10",
"@babel/core": "^7.22.20",
"@babel/node": "^7.22.19",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-env": "^7.22.10",
"react-app-rewired": "^2.2.1",
"wrangler": "^3.4.0"
"@babel/preset-env": "^7.22.20",
"react-app-rewired": "^2.2.1"
}
}

10399
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -64,6 +64,45 @@
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root">
<p>You need to enable <code>JavaScript</code> to run <span>this app.</span></p>
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<div id="content">
<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
@@ -84,6 +123,11 @@
>
</p>
</h2>
<hr />
<input id="input1" style="width: 80%;" />
<hr />
<textarea id="textarea1" style="width: 80%;">test</textarea>
<hr />
<div id="addtitle"></div>
<h2>Shadow 1</h2>
<div id="shadow1"></div>

View File

@@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "__MSG_app_name__",
"description": "__MSG_app_description__",
"version": "1.6.10",
"version": "1.8.11",
"default_locale": "en",
"author": "Gabe<yugang2002@gmail.com>",
"homepage_url": "https://github.com/fishjar/kiss-translator",
@@ -28,14 +28,29 @@
},
"description": "__MSG_toggle_translate__"
},
"openTranbox": {
"suggested_key": {
"default": "Alt+S"
},
"description": "__MSG_open_tranbox__"
},
"toggleStyle": {
"suggested_key": {
"default": "Alt+C"
},
"description": "__MSG_toggle_style__"
},
"openOptions": {
"description": "__MSG_open_options__"
}
},
"permissions": ["<all_urls>", "storage"],
"permissions": [
"<all_urls>",
"storage",
"contextMenus",
"scripting",
"declarativeNetRequest"
],
"icons": {
"16": "images/logo16.png",
"32": "images/logo32.png",

View File

@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "__MSG_app_name__",
"description": "__MSG_app_description__",
"version": "1.6.10",
"version": "1.8.11",
"default_locale": "en",
"author": "Gabe<yugang2002@gmail.com>",
"homepage_url": "https://github.com/fishjar/kiss-translator",
@@ -29,14 +29,28 @@
},
"description": "__MSG_toggle_translate__"
},
"openTranbox": {
"suggested_key": {
"default": "Alt+S"
},
"description": "__MSG_open_tranbox__"
},
"toggleStyle": {
"suggested_key": {
"default": "Alt+C"
},
"description": "__MSG_toggle_style__"
},
"openOptions": {
"description": "__MSG_open_options__"
}
},
"permissions": ["storage"],
"permissions": [
"storage",
"contextMenus",
"scripting",
"declarativeNetRequest"
],
"host_permissions": ["<all_urls>"],
"icons": {
"16": "images/logo16.png",

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

@@ -0,0 +1,23 @@
import queryString from "query-string";
import { URL_BAIDU_TRANSAPI, DEFAULT_USER_AGENT } from "../config";
export const genBaidu = async ({ text, from, to }) => {
const data = {
from,
to,
query: text,
source: "txt",
};
const init = {
headers: {
// Origin: "https://fanyi.baidu.com",
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
"User-Agent": DEFAULT_USER_AGENT,
},
method: "POST",
body: queryString.stringify(data),
};
return [URL_BAIDU_TRANSAPI, 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

@@ -1,18 +1,43 @@
import queryString from "query-string";
import { fetchPolyfill } from "../libs/fetch";
import { fetchData } from "../libs/fetch";
import {
OPT_TRANS_GOOGLE,
OPT_TRANS_MICROSOFT,
OPT_TRANS_DEEPL,
OPT_TRANS_DEEPLFREE,
OPT_TRANS_DEEPLX,
OPT_TRANS_NIUTRANS,
OPT_TRANS_BAIDU,
OPT_TRANS_TENCENT,
OPT_TRANS_OPENAI,
OPT_TRANS_OPENAI_2,
OPT_TRANS_OPENAI_3,
OPT_TRANS_GEMINI,
OPT_TRANS_CLOUDFLAREAI,
OPT_TRANS_OLLAMA,
OPT_TRANS_OLLAMA_2,
OPT_TRANS_OLLAMA_3,
OPT_TRANS_CUSTOMIZE,
OPT_LANGS_SPECIAL,
PROMPT_PLACE_FROM,
PROMPT_PLACE_TO,
OPT_TRANS_CUSTOMIZE_2,
OPT_TRANS_CUSTOMIZE_3,
OPT_TRANS_CUSTOMIZE_4,
OPT_TRANS_CUSTOMIZE_5,
URL_CACHE_TRAN,
KV_SALT_SYNC,
URL_GOOGLE_TRAN,
URL_MICROSOFT_LANGDETECT,
URL_BAIDU_LANGDETECT,
URL_BAIDU_SUGGEST,
URL_BAIDU_TTS,
OPT_LANGS_BAIDU,
URL_TENCENT_TRANSMART,
OPT_LANGS_TENCENT,
OPT_LANGS_SPECIAL,
OPT_LANGS_MICROSOFT,
} from "../config";
import { tryDetectLang } from "../libs";
import { sha256 } from "../libs/utils";
import interpreter from "../libs/interpreter";
import { msAuth } from "../libs/auth";
/**
* 同步数据
@@ -21,222 +46,154 @@ import { sha256 } from "../libs/utils";
* @param {*} data
* @returns
*/
export const apiSyncData = async (url, key, data, isBg = false) =>
fetchPolyfill(url, {
export const apiSyncData = async (url, key, data) =>
fetchData(url, {
headers: {
"Content-type": "application/json",
Authorization: `Bearer ${await sha256(key, KV_SALT_SYNC)}`,
},
method: "POST",
body: JSON.stringify(data),
isBg,
});
/**
* 下载数据
* @param {*} url
* @param {*} isBg
* @returns
*/
export const apiFetch = (url, isBg = false) => fetchPolyfill(url, { isBg });
export const apiFetch = (url) => fetchData(url);
/**
* 谷歌翻译
* Google语言识别
* @param {*} text
* @param {*} to
* @param {*} from
* @returns
*/
const apiGoogleTranslate = async (
translator,
text,
to,
from,
{ url, key, useCache = true }
) => {
export const apiGoogleLangdetect = async (text) => {
const params = {
client: "gtx",
dt: "t",
dj: 1,
ie: "UTF-8",
sl: from,
tl: to,
sl: "auto",
tl: "zh-CN",
q: text,
};
const input = `${url}?${queryString.stringify(params)}`;
const res = await fetchPolyfill(input, {
const input = `${URL_GOOGLE_TRAN}?${queryString.stringify(params)}`;
const res = await fetchData(input, {
headers: {
"Content-type": "application/json",
},
useCache,
usePool: true,
translator,
token: key,
useCache: true,
});
const trText = res.sentences.map((item) => item.trans).join(" ");
const isSame = to === res.src;
return [trText, isSame];
return res.src;
};
/**
* 微软翻译
* Microsoft语言识别
* @param {*} text
* @param {*} to
* @param {*} from
* @returns
*/
const apiMicrosoftTranslate = async (
translator,
text,
to,
from,
{ url, useCache = true }
) => {
const params = {
from,
to,
"api-version": "3.0",
};
const input = `${url}?${queryString.stringify(params)}`;
const res = await fetchPolyfill(input, {
export const apiMicrosoftLangdetect = async (text) => {
const [token] = await msAuth();
const res = await fetchData(URL_MICROSOFT_LANGDETECT, {
headers: {
"Content-type": "application/json",
Authorization: `Bearer ${token}`,
},
method: "POST",
body: JSON.stringify([{ Text: text }]),
useCache,
usePool: true,
translator,
useCache: true,
});
const trText = res[0].translations[0].text;
const isSame = to === res[0].detectedLanguage?.language;
return [trText, isSame];
return OPT_LANGS_MICROSOFT.get(res[0].language) ?? res[0].language;
};
/**
* DeepL翻译
* 百度语言识别
* @param {*} text
* @param {*} to
* @param {*} from
* @returns
*/
const apiDeepLTranslate = async (
translator,
text,
to,
from,
{ url, key, useCache = true }
) => {
const data = {
text: [text],
target_lang: to,
split_sentences: "0",
};
if (from) {
data.source_lang = from;
export const apiBaiduLangdetect = async (text) => {
const res = await fetchData(URL_BAIDU_LANGDETECT, {
headers: {
"Content-type": "application/json",
},
method: "POST",
body: JSON.stringify({
query: text,
}),
useCache: true,
});
if (res.error === 0) {
return OPT_LANGS_BAIDU.get(res.lan) ?? res.lan;
}
const res = await fetchPolyfill(url, {
headers: {
"Content-type": "application/json",
},
method: "POST",
body: JSON.stringify(data),
useCache,
usePool: true,
translator,
token: key,
});
const trText = res.translations.map((item) => item.text).join(" ");
const isSame = to === res.translations[0].detected_source_language;
return [trText, isSame];
return "";
};
/**
* OpenAI 翻译
* 百度翻译建议
* @param {*} text
* @param {*} to
* @param {*} from
* @returns
*/
const apiOpenaiTranslate = async (
translator,
text,
to,
from,
{ url, key, model, prompt, useCache = true }
) => {
prompt = prompt
.replaceAll(PROMPT_PLACE_FROM, from)
.replaceAll(PROMPT_PLACE_TO, to);
const res = await fetchPolyfill(url, {
export const apiBaiduSuggest = async (text) => {
const res = await fetchData(URL_BAIDU_SUGGEST, {
headers: {
"Content-type": "application/json",
},
method: "POST",
body: JSON.stringify({
model: model,
messages: [
{
role: "system",
content: prompt,
},
{
role: "user",
content: text,
},
],
temperature: 0,
max_tokens: 256,
kw: text,
}),
useCache,
usePool: true,
translator,
token: key,
useCache: true,
});
const trText = res?.choices?.[0].message.content;
const sLang = await tryDetectLang(text);
const tLang = await tryDetectLang(trText);
const isSame = text === trText || (sLang && tLang && sLang === tLang);
return [trText, isSame];
if (res.errno === 0) {
return res.data;
}
return [];
};
/**
* 自定义接口 翻译
* 百度语音
* @param {*} text
* @param {*} to
* @param {*} from
* @param {*} lan
* @param {*} spd
* @returns
*/
const apiCustomTranslate = async (
translator,
text,
to,
from,
{ url, key, useCache = true }
) => {
const res = await fetchPolyfill(url, {
export const apiBaiduTTS = (text, lan = "uk", spd = 3) => {
const url = `${URL_BAIDU_TTS}?${queryString.stringify({ lan, text, spd })}`;
return fetchData(url, {
useCache: false, // 为避免缓存过快增长,禁用缓存语音数据
});
};
/**
* 腾讯语言识别
* @param {*} text
* @returns
*/
export const apiTencentLangdetect = async (text) => {
const body = JSON.stringify({
header: {
fn: "text_analysis",
},
text,
});
const res = await fetchData(URL_TENCENT_TRANSMART, {
headers: {
"Content-type": "application/json",
},
method: "POST",
body: JSON.stringify({
text,
from,
to,
}),
useCache,
usePool: true,
translator,
token: key,
body,
useCache: true,
});
const trText = res.text;
const isSame = to === res.from;
return [trText, isSame];
return OPT_LANGS_TENCENT.get(res.language) ?? res.language;
};
/**
@@ -244,29 +201,142 @@ const apiCustomTranslate = async (
* @param {*} param0
* @returns
*/
export const apiTranslate = ({
export const apiTranslate = async ({
translator,
text,
fromLang,
toLang,
apiSetting,
apiSetting = {},
useCache = true,
usePool = true,
}) => {
const from = OPT_LANGS_SPECIAL[translator]?.get(fromLang) ?? fromLang;
const to = OPT_LANGS_SPECIAL[translator]?.get(toLang) ?? toLang;
const callApi = (api) => api(translator, text, to, from, apiSetting);
let trText = "";
let isSame = false;
if (!text) {
return [trText, true];
}
const from =
OPT_LANGS_SPECIAL[translator].get(fromLang) ??
OPT_LANGS_SPECIAL[translator].get("auto");
const to = OPT_LANGS_SPECIAL[translator].get(toLang);
if (!to) {
console.log(`[trans] target lang: ${toLang} not support`);
return [trText, isSame];
}
// 版本号一/二位升级,旧缓存失效
const [v1, v2] = process.env.REACT_APP_VERSION.split(".");
const cacheOpts = {
translator,
text,
fromLang,
toLang,
version: [v1, v2].join("."),
};
const transOpts = {
translator,
text,
from,
to,
};
const res = await fetchData(
`${URL_CACHE_TRAN}?${queryString.stringify(cacheOpts)}`,
{
useCache,
usePool,
transOpts,
apiSetting,
}
);
switch (translator) {
case OPT_TRANS_GOOGLE:
return callApi(apiGoogleTranslate);
trText = res.sentences.map((item) => item.trans).join(" ");
isSame = to === res.src;
break;
case OPT_TRANS_MICROSOFT:
return callApi(apiMicrosoftTranslate);
trText = res
.map((item) => item.translations.map((item) => item.text).join(" "))
.join(" ");
isSame = text === trText;
break;
case OPT_TRANS_DEEPL:
return callApi(apiDeepLTranslate);
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_NIUTRANS:
const json = JSON.parse(res);
if (json.error_msg) {
throw new Error(json.error_msg);
}
trText = json.tgt_text;
isSame = to === json.from;
break;
case OPT_TRANS_BAIDU:
// trText = res.trans_result?.data.map((item) => item.dst).join(" ");
// isSame = res.trans_result?.to === res.trans_result?.from;
if (res.type === 1) {
trText = Object.keys(JSON.parse(res.result).content[0].mean[0].cont)[0];
isSame = to === res.from;
} else if (res.type === 2) {
trText = res.data.map((item) => item.dst).join(" ");
isSame = to === res.from;
}
break;
case OPT_TRANS_TENCENT:
trText = res.auto_translation;
isSame = text === trText;
break;
case OPT_TRANS_OPENAI:
return callApi(apiOpenaiTranslate);
case OPT_TRANS_OPENAI_2:
case OPT_TRANS_OPENAI_3:
trText = res?.choices?.map((item) => item.message.content).join(" ");
isSame = text === trText;
break;
case OPT_TRANS_GEMINI:
trText = res?.candidates
?.map((item) => item.content?.parts.map((item) => item.text).join(" "))
.join(" ");
isSame = text === trText;
break;
case OPT_TRANS_CLOUDFLAREAI:
trText = res?.result?.translated_text;
isSame = text === trText;
break;
case OPT_TRANS_OLLAMA:
case OPT_TRANS_OLLAMA_2:
case OPT_TRANS_OLLAMA_3:
trText = res?.response;
isSame = text === trText;
break;
case OPT_TRANS_CUSTOMIZE:
return callApi(apiCustomTranslate);
case OPT_TRANS_CUSTOMIZE_2:
case OPT_TRANS_CUSTOMIZE_3:
case OPT_TRANS_CUSTOMIZE_4:
case OPT_TRANS_CUSTOMIZE_5:
const { resHook } = apiSetting;
if (resHook?.trim()) {
interpreter.run(`exports.resHook = ${resHook}`);
[trText, isSame] = interpreter.exports.resHook(res, text, from, to);
} else {
trText = res.text;
isSame = to === res.from;
}
break;
default:
return ["", false];
}
return [trText, isSame, res];
};

409
src/apis/trans.js Normal file
View File

@@ -0,0 +1,409 @@
import queryString from "query-string";
import {
OPT_TRANS_GOOGLE,
OPT_TRANS_MICROSOFT,
OPT_TRANS_DEEPL,
OPT_TRANS_DEEPLFREE,
OPT_TRANS_DEEPLX,
OPT_TRANS_NIUTRANS,
OPT_TRANS_BAIDU,
OPT_TRANS_TENCENT,
OPT_TRANS_OPENAI,
OPT_TRANS_OPENAI_2,
OPT_TRANS_OPENAI_3,
OPT_TRANS_GEMINI,
OPT_TRANS_CLOUDFLAREAI,
OPT_TRANS_OLLAMA,
OPT_TRANS_OLLAMA_2,
OPT_TRANS_OLLAMA_3,
OPT_TRANS_CUSTOMIZE,
OPT_TRANS_CUSTOMIZE_2,
OPT_TRANS_CUSTOMIZE_3,
OPT_TRANS_CUSTOMIZE_4,
OPT_TRANS_CUSTOMIZE_5,
URL_MICROSOFT_TRAN,
URL_TENCENT_TRANSMART,
INPUT_PLACE_URL,
INPUT_PLACE_FROM,
INPUT_PLACE_TO,
INPUT_PLACE_TEXT,
INPUT_PLACE_KEY,
INPUT_PLACE_MODEL,
} from "../config";
import { msAuth } from "../libs/auth";
import { genDeeplFree } from "./deepl";
import { genBaidu } from "./baidu";
import interpreter from "../libs/interpreter";
const keyMap = new Map();
const urlMap = new Map();
// 轮询key/url
const keyPick = (translator, key = "", cacheMap) => {
const keys = key
.split(/\n|,/)
.map((item) => item.trim())
.filter(Boolean);
if (keys.length === 0) {
return "";
}
const preIndex = cacheMap.get(translator) ?? -1;
const curIndex = (preIndex + 1) % keys.length;
cacheMap.set(translator, curIndex);
return keys[curIndex];
};
const genGoogle = ({ text, from, to, url, key }) => {
const params = {
client: "gtx",
dt: "t",
dj: 1,
ie: "UTF-8",
sl: from,
tl: to,
q: text,
};
const input = `${url}?${queryString.stringify(params)}`;
const init = {
headers: {
"Content-type": "application/json",
},
};
if (key) {
init.headers.Authorization = `Bearer ${key}`;
}
return [input, init];
};
const genMicrosoft = async ({ text, from, to }) => {
const [token] = await msAuth();
const params = {
from,
to,
"api-version": "3.0",
};
const input = `${URL_MICROSOFT_TRAN}?${queryString.stringify(params)}`;
const init = {
headers: {
"Content-type": "application/json",
Authorization: `Bearer ${token}`,
},
method: "POST",
body: JSON.stringify([{ Text: text }]),
};
return [input, init];
};
const genDeepl = ({ text, from, to, url, key }) => {
const data = {
text: [text],
target_lang: to,
source_lang: from,
// split_sentences: "0",
};
const init = {
headers: {
"Content-type": "application/json",
Authorization: `DeepL-Auth-Key ${key}`,
},
method: "POST",
body: JSON.stringify(data),
};
return [url, init];
};
const genDeeplX = ({ text, from, to, url, key }) => {
const data = {
text,
target_lang: to,
source_lang: from,
};
const init = {
headers: {
"Content-type": "application/json",
},
method: "POST",
body: JSON.stringify(data),
};
if (key) {
init.headers.Authorization = `Bearer ${key}`;
}
return [url, init];
};
const genNiuTrans = ({ text, from, to, url, key, dictNo, memoryNo }) => {
const data = {
from,
to,
apikey: key,
src_text: text,
dictNo,
memoryNo,
};
const init = {
headers: {
"Content-type": "application/json",
},
method: "POST",
body: JSON.stringify(data),
};
return [url, init];
};
const genTencent = ({ text, from, to }) => {
const data = {
header: {
fn: "auto_translation_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,
temperature,
maxTokens,
}) => {
prompt = prompt
.replaceAll(INPUT_PLACE_FROM, from)
.replaceAll(INPUT_PLACE_TO, to);
const data = {
model,
messages: [
{
role: "system",
content: prompt,
},
{
role: "user",
content: text,
},
],
temperature,
max_tokens: maxTokens,
};
const init = {
headers: {
"Content-type": "application/json",
Authorization: `Bearer ${key}`, // OpenAI
"api-key": key, // Azure OpenAI
},
method: "POST",
body: JSON.stringify(data),
};
return [url, init];
};
const genGemini = ({ text, from, to, url, key, prompt, model }) => {
url = url
.replaceAll(INPUT_PLACE_MODEL, model)
.replaceAll(INPUT_PLACE_KEY, key);
prompt = prompt
.replaceAll(INPUT_PLACE_FROM, from)
.replaceAll(INPUT_PLACE_TO, to)
.replaceAll(INPUT_PLACE_TEXT, text);
const data = {
contents: [
{
// role: "user",
parts: [
{
text: prompt,
},
],
},
],
};
const init = {
headers: {
"Content-type": "application/json",
},
method: "POST",
body: JSON.stringify(data),
};
return [url, init];
};
const genOllama = ({ text, from, to, url, key, prompt, model }) => {
prompt = prompt
.replaceAll(INPUT_PLACE_FROM, from)
.replaceAll(INPUT_PLACE_TO, to)
.replaceAll(INPUT_PLACE_TEXT, text);
const data = {
model,
prompt,
stream: false,
};
const init = {
headers: {
"Content-type": "application/json",
},
method: "POST",
body: JSON.stringify(data),
};
if (key) {
init.headers.Authorization = `Bearer ${key}`;
}
return [url, init];
};
const genCloudflareAI = ({ text, from, to, url, key }) => {
const data = {
text,
source_lang: from,
target_lang: to,
};
const init = {
headers: {
"Content-type": "application/json",
Authorization: `Bearer ${key}`,
},
method: "POST",
body: JSON.stringify(data),
};
return [url, init];
};
const genCustom = ({ text, from, to, url, key, reqHook }) => {
url = url
.replaceAll(INPUT_PLACE_URL, url)
.replaceAll(INPUT_PLACE_FROM, from)
.replaceAll(INPUT_PLACE_TO, to)
.replaceAll(INPUT_PLACE_TEXT, text)
.replaceAll(INPUT_PLACE_KEY, key);
let init = {};
if (reqHook?.trim()) {
interpreter.run(`exports.reqHook = ${reqHook}`);
[url, init] = interpreter.exports.reqHook(text, from, to, url, key);
return [url, init];
}
const data = {
text,
from,
to,
};
init = {
headers: {
"Content-type": "application/json",
},
method: "POST",
body: JSON.stringify(data),
};
if (key) {
init.headers.Authorization = `Bearer ${key}`;
}
return [url, init];
};
/**
* 构造翻译接口请求参数
* @param {*}
* @returns
*/
export const genTransReq = ({ translator, text, from, to }, apiSetting) => {
const args = { text, from, to, ...apiSetting };
switch (translator) {
case OPT_TRANS_DEEPL:
case OPT_TRANS_OPENAI:
case OPT_TRANS_OPENAI_2:
case OPT_TRANS_OPENAI_3:
case OPT_TRANS_GEMINI:
case OPT_TRANS_CLOUDFLAREAI:
case OPT_TRANS_OLLAMA:
case OPT_TRANS_OLLAMA_2:
case OPT_TRANS_OLLAMA_3:
case OPT_TRANS_NIUTRANS:
args.key = keyPick(translator, args.key, keyMap);
break;
case OPT_TRANS_DEEPLX:
args.url = keyPick(translator, args.url, urlMap);
break;
default:
}
switch (translator) {
case OPT_TRANS_GOOGLE:
return genGoogle(args);
case OPT_TRANS_MICROSOFT:
return genMicrosoft(args);
case OPT_TRANS_DEEPL:
return genDeepl(args);
case OPT_TRANS_DEEPLFREE:
return genDeeplFree(args);
case OPT_TRANS_DEEPLX:
return genDeeplX(args);
case OPT_TRANS_NIUTRANS:
return genNiuTrans(args);
case OPT_TRANS_BAIDU:
return genBaidu(args);
case OPT_TRANS_TENCENT:
return genTencent(args);
case OPT_TRANS_OPENAI:
case OPT_TRANS_OPENAI_2:
case OPT_TRANS_OPENAI_3:
return genOpenAI(args);
case OPT_TRANS_GEMINI:
return genGemini(args);
case OPT_TRANS_CLOUDFLAREAI:
return genCloudflareAI(args);
case OPT_TRANS_OLLAMA:
case OPT_TRANS_OLLAMA_2:
case OPT_TRANS_OLLAMA_3:
return genOllama(args);
case OPT_TRANS_CUSTOMIZE:
case OPT_TRANS_CUSTOMIZE_2:
case OPT_TRANS_CUSTOMIZE_3:
case OPT_TRANS_CUSTOMIZE_4:
case OPT_TRANS_CUSTOMIZE_5:
return genCustom(args);
default:
throw new Error(`[trans] translator: ${translator} not support`);
}
};

View File

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

268
src/common.js Normal file
View File

@@ -0,0 +1,268 @@
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,
MSG_OPEN_TRANBOX,
APP_LCNAME,
DEFAULT_TRANBOX_SETTING,
} from "./config";
import { getFabWithDefault, getSettingWithDefault } from "./libs/storage";
import { Translator } from "./libs/translator";
import { isIframe, sendIframeMsg } from "./libs/iframe";
import Slection from "./views/Selection";
import { touchTapListener } from "./libs/touch";
import { debounce, genEventName } from "./libs/utils";
import { handlePing, injectScript } from "./libs/gm";
import { browser } from "./libs/browser";
import { matchRule } from "./libs/rules";
import { trySyncAllSubRules } from "./libs/subRules";
import { isInBlacklist } from "./libs/blacklist";
import inputTranslate from "./libs/inputTranslate";
/**
* 油猴脚本设置页面
*/
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);
}
}
/**
* 插件监听后端事件
* @param {*} translator
*/
function runtimeListener(translator) {
browser?.runtime.onMessage.addListener(async ({ action, args }) => {
switch (action) {
case MSG_TRANS_TOGGLE:
translator.toggle();
sendIframeMsg(MSG_TRANS_TOGGLE);
break;
case MSG_TRANS_TOGGLE_STYLE:
translator.toggleStyle();
sendIframeMsg(MSG_TRANS_TOGGLE_STYLE);
break;
case MSG_TRANS_GETRULE:
break;
case MSG_TRANS_PUTRULE:
translator.updateRule(args);
sendIframeMsg(MSG_TRANS_PUTRULE, args);
break;
case MSG_OPEN_TRANBOX:
window.dispatchEvent(new CustomEvent(MSG_OPEN_TRANBOX));
break;
default:
return { error: `message action is unavailable: ${action}` };
}
return { data: translator.rule };
});
}
/**
* iframe 页面执行
* @param {*} translator
*/
function runIframe(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:
translator.updateRule(args || {});
break;
default:
}
});
}
/**
* 悬浮按钮
* @param {*} translator
* @returns
*/
async function showFab(translator) {
const fab = await getFabWithDefault();
const $action = document.createElement("div");
$action.setAttribute("id", APP_LCNAME);
$action.style.fontSize = "0";
$action.style.width = "0";
$action.style.height = "0";
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>
);
}
/**
* 划词翻译
* @param {*} param0
* @returns
*/
function showTransbox({
contextMenuType,
tranboxSetting = DEFAULT_TRANBOX_SETTING,
transApis,
darkMode,
uiLang,
langDetector,
}) {
if (!tranboxSetting?.transOpen) {
return;
}
const $tranbox = document.createElement("div");
$tranbox.setAttribute("id", "kiss-transbox");
$tranbox.style.fontSize = "0";
$tranbox.style.width = "0";
$tranbox.style.height = "0";
document.body.parentElement.appendChild($tranbox);
const shadowContainer = $tranbox.attachShadow({ mode: "closed" });
const emotionRoot = document.createElement("style");
const shadowRootElement = document.createElement("div");
shadowRootElement.classList.add(`KT-transbox`);
shadowRootElement.classList.add(`KT-transbox_${darkMode ? "dark" : "light"}`);
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
contextMenuType={contextMenuType}
tranboxSetting={tranboxSetting}
transApis={transApis}
uiLang={uiLang}
langDetector={langDetector}
/>
</CacheProvider>
</React.StrictMode>
);
}
/**
* 显示错误信息到页面顶部
* @param {*} message
*/
function showErr(message) {
const $err = document.createElement("div");
$err.innerText = `KISS-Translator: ${message}`;
$err.style.cssText = "background:red; color:#fff;";
document.body.prepend($err);
}
/**
* 监听触屏操作
* @param {*} translator
* @returns
*/
function touchOperation(translator) {
const { touchTranslate = 2 } = translator.setting;
if (touchTranslate === 0) {
return;
}
const handleTap = debounce(() => {
translator.toggle();
sendIframeMsg(MSG_TRANS_TOGGLE);
});
touchTapListener(handleTap, touchTranslate);
}
/**
* 入口函数
*/
export async function run(isUserscript = false) {
try {
const href = document.location.href;
// 设置页面
if (
isUserscript &&
(href.includes(process.env.REACT_APP_OPTIONSPAGE_DEV) ||
href.includes(process.env.REACT_APP_OPTIONSPAGE))
) {
runSettingPage();
return;
}
// 读取设置信息
const setting = await getSettingWithDefault();
// 黑名单
if (isInBlacklist(href, setting)) {
return;
}
// 翻译网页
const rule = await matchRule(href, setting);
const translator = new Translator(rule, setting);
// 适配iframe
if (isIframe) {
runIframe(translator);
return;
}
// 监听消息
!isUserscript && runtimeListener(translator);
// 输入框翻译
inputTranslate(setting);
// 划词翻译
showTransbox(setting);
// 浮球按钮
await showFab(translator);
// 触屏操作
touchOperation(translator);
// 同步订阅规则
isUserscript && (await trySyncAllSubRules(setting));
} catch (err) {
console.error("[KISS-Translator]", err);
showErr(err.message);
}
}

View File

@@ -42,56 +42,81 @@ const customApiLangs = `["en", "English - English"],
["vi", "Vietnamese - Tiếng Việt"],
`;
const customApiHelpZH = `/// 自定义翻译源接口说明
// 请求Request数据将按下面规范发送
{
url: {{YOUR_URL}},
method: "POST",
const hookExample = `// URL
https://translate.googleapis.com/translate_a/single?client=gtx&dj=1&dt=t&ie=UTF-8&q={{text}}&sl=en&tl=zh-CN
// Request Hook
(text, from, to, url, key) => [url, {
headers: {
"Content-type": "application/json",
"Authorization" = "Bearer {{YOUR_KEY}}"
"Content-type": "application/json",
},
method: "GET",
body: null,
}]
// Response Hook
(res, text, from, to) => [res.sentences.map((item) => item.trans).join(" "), to === res.src]`;
const customApiHelpZH = `// 请求数据默认格式
{
"url": "{{url}}",
"method": "POST",
"headers": {
"Content-type": "application/json",
"Authorization": "Bearer {{key}}"
},
"body": {
"text": "{{text}}", // 待翻译文字
"from": "{{from}}", // 文字的语言(可能为空)
"to": "{{to}}", // 目标语言
},
body: {
text, // 需要翻译的文字
from, // 源语言,可能为空,表示需要接口自动识别语言
to, // 目标语言
}
}
// 返回Response数据需符合下面的JSON规范
// 返回数据默认格式
{
text, // 翻译后的文字
from, // 识别的源语言
to, // 目标语言(可选)
text: "", // 翻译后的文字
from: "", // 识别的源语言
to: "", // 目标语言(可选)
}
// Hook 范例
${hookExample}
// 支持的语言代码如下
${customApiLangs}
`;
const customApiHelpEN = `/// Custom translation source interface description
// Request data will be sent according to the following specifications
const customApiHelpEN = `// Default request
{
url: {{YOUR_URL}},
method: "POST",
headers: {
"url": "{{url}}",
"method": "POST",
"headers": {
"Content-type": "application/json",
"Authorization" = "Bearer {{YOUR_KEY}}"
"Authorization": "Bearer {{key}}"
},
"body": {
"text": "{{text}}", // Text to be translated
"from": "{{from}}", // The language of the text (may be empty)
"to": "{{to}}", // Target language
},
body: {
text, // text to be translated
from, // Source language, may be empty
to, // Target language
}
}
// The returned data must conform to the following JSON specification
// Default response
{
text, // translated text
from, // Recognized source language
to, // Target language (optional)
text: "", // translated text
from: "", // Recognized source language
to: "", // Target language (optional)
}
/// Hook Example
${hookExample}
// The supported language codes are as follows
${customApiLangs}
`;
@@ -110,8 +135,8 @@ export const I18N = {
en: customApiHelpEN,
},
translate_alt: {
zh: `翻译 (Alt+Q)`,
en: `Translate (Alt+Q)`,
zh: `翻译`,
en: `Translate`,
},
basic_setting: {
zh: `基本设置`,
@@ -158,20 +183,24 @@ export const I18N = {
en: `Interface Language`,
},
fetch_limit: {
zh: `最大请求数量 (1-100)`,
en: `Maximum Number Of Request (1-100)`,
zh: `最大并发请求数量 (1-100)`,
en: `Maximum Number Of Concurrent Requests (1-100)`,
},
fetch_interval: {
zh: `请求间隔时间 (0-5000ms)`,
en: `Request Interval (0-5000ms)`,
zh: `每次请求间隔时间 (0-5000ms)`,
en: `Time Between Requests (0-5000ms)`,
},
translate_interval: {
zh: `重新翻译间隔时间 (100-5000ms)`,
en: `Retranslation Interval (100-5000ms)`,
},
min_translate_length: {
zh: `最小翻译长度 (1-100)`,
en: `Min Translate Length (1-100)`,
zh: `最小翻译字符数 (1-100)`,
en: `Minimum number Of Translated Characters (1-100)`,
},
max_translate_length: {
zh: `最大翻译长度 (100-10000)`,
en: `Max Translate Length (100-10000)`,
zh: `最大翻译字符数 (100-10000)`,
en: `Maximum number Of Translated Characters (100-10000)`,
},
num_of_newline_characters: {
zh: `换行字符数 (1-1000)`,
@@ -181,16 +210,20 @@ export const I18N = {
zh: `翻译服务`,
en: `Translate Service`,
},
mouseover_translation: {
zh: `鼠标悬停翻译`,
en: `Mouseover translation`,
translate_timing: {
zh: `翻译时机`,
en: `Translate Timing`,
},
mk_disable: {
zh: `禁用`,
en: `Disable`,
mk_pagescroll: {
zh: `滚动加载翻译(推荐)`,
en: `Rolling Loading (Suggested)`,
},
mk_pageopen: {
zh: `页面打开全部翻译`,
en: `Page Open`,
},
mk_mouseover: {
zh: `鼠标悬停`,
zh: `鼠标悬停翻译`,
en: `Mouseover`,
},
mk_ctrlKey: {
@@ -213,13 +246,21 @@ export const I18N = {
zh: `目标语言`,
en: `Target Language`,
},
to_lang2: {
zh: `第二目标语言`,
en: `Target Language 2`,
},
to_lang2_helper: {
zh: `设定后,与目标语言产生互译效果,但依赖远程语言识别。`,
en: `After setting, it will produce mutual translation effect with the target language, but it relies on remote language recognition.`,
},
text_style: {
zh: `样式`,
zh: `文样式`,
en: `Text Style`,
},
text_style_alt: {
zh: `文字样式 (Alt+C)`,
en: `Text Style (Alt+C)`,
zh: `译文样式`,
en: `Text Style`,
},
bg_color: {
zh: `样式颜色`,
@@ -293,6 +334,10 @@ export const I18N = {
zh: `2、“订阅规则”的注入位置是倒数第二的位置因此除全局规则(*)外,“个人规则”优先级比“订阅规则”高,“个人规则”填写同样的网址会覆盖”订阅规则“的条目。`,
en: `2. The injection position of "Subscription Rules" is the penultimate position. Therefore, except for the global rules (*), the priority of "Personal Rules" is higher than that of "Subscription Rules". Filling in the same url in "Personal Rules" will overwrite "Subscription Rules" entry.`,
},
rules_warn_3: {
zh: `3、关于规则填写输入框留空或下拉框选“*”表示采用全局规则。`,
en: `3. Regarding filling in the rules: Leave the input box blank or select "*" in the drop-down box to use global rule.`,
},
sync_warn: {
zh: `如果服务器存在其他客户端同步的数据,第一次同步将直接覆盖本地配置,后面则根据修改时间,新的覆盖旧的。`,
en: `If the server has data synchronized by other clients, the first synchronization will directly overwrite the local configuration, and later, according to the modification time, the new one will overwrite the old one.`,
@@ -333,6 +378,10 @@ export const I18N = {
zh: `高亮`,
en: `Highlight`,
},
blockquote: {
zh: `引用`,
en: `Blockquote`,
},
diy_style: {
zh: `自定义样式`,
en: `Custom Style`,
@@ -350,12 +399,12 @@ export const I18N = {
en: `URL pattern`,
},
pattern_helper: {
zh: `1、支持星号(*)通配符。2、多个URL支持英文逗号“,”分隔。`,
en: `1. The asterisk (*) wildcard is supported. 2. Multiple URLs can be separated by English commas ",".`,
zh: `1、支持星号(*)通配符。2、多个URL用换行或英文逗号“,”分隔。`,
en: `1. Supports the asterisk (*) wildcard character. 2. Separate multiple URLs with newlines or English commas ",".`,
},
selector_helper: {
zh: `1、遵循CSS选择器语法。2、留空表示采用全局设置。3、多个CSS选择器之间用“;”隔开。4、“shadow root”选择器和内部选择器用“>>>”隔开。`,
en: `1. Follow CSS selector syntax. 2. Leave blank to adopt the global setting. 3. Separate multiple CSS selectors with ";". 4. The "shadow root" selector and the internal selector are separated by ">>>".`,
zh: `1、遵循CSS选择器语法。2、多个CSS选择器之间用“;”隔开。3、“shadow root”选择器和内部选择器用“>>>”隔开。`,
en: `1. Follow CSS selector syntax. 2. Separate multiple CSS selectors with ";". 3. The "shadow root" selector and the internal selector are separated by ">>>".`,
},
translate_switch: {
zh: `开启翻译`,
@@ -373,6 +422,62 @@ export const I18N = {
zh: `选择器`,
en: `Selector`,
},
keep_selector: {
zh: `保留元素选择器`,
en: `Keep unchanged selector`,
},
keep_selector_helper: {
zh: `1、遵循CSS选择器语法。2、子元素选择器用“>>>”隔开。`,
en: `1. Follow CSS selector syntax. 2. Sub-element selectors are separated by ">>>".`,
},
terms: {
zh: `专业术语`,
en: `Terms`,
},
terms_helper: {
zh: `1、支持正则表达式匹配无需斜杆不支持修饰符。2、多条术语用换行或分号“;”隔开。3、术语和译文用英文逗号“,”隔开。4、没有译文视为不翻译术语。`,
en: `1. Supports regular expression matching, no slash required, and no modifiers are supported. 2. Separate multiple terms with newlines or semicolons ";". 3. Terms and translations are separated by English commas ",". 4. If there is no translation, the term will be deemed not to be translated.`,
},
selector_style: {
zh: `选择器节点样式`,
en: `Selector Style`,
},
selector_style_helper: {
zh: `开启翻译时注入,关闭翻译时不会移除。`,
en: `It is injected when translation is turned on and will not be removed when translation is turned off.`,
},
selector_parent_style: {
zh: `选择器父节点样式`,
en: `Selector Parent Style`,
},
inject_js: {
zh: `注入JS`,
en: `Inject JS`,
},
inject_js_helper: {
zh: `1、开启翻译时注入运行关闭翻译时移除。2、随着页面变化可能会多次注入运行。`,
en: `1. Inject and run when translation is turned on, and removed when translation is turned off. 2. As the page changes, it may be injected and run multiple times.`,
},
inject_css: {
zh: `注入CSS`,
en: `Inject CSS`,
},
inject_css_helper: {
zh: `开启翻译时注入,关闭翻译时将移除。`,
en: `Injected when translation is enabled and removed when translation is disabled.`,
},
root_selector: {
zh: `根选择器`,
en: `Root Selector`,
},
fixer_function: {
zh: `修复函数`,
en: `Fixer Function`,
},
fixer_function_helper: {
zh: `1、br是将<br>换行替换成<p "kiss-p">。2、bn是将\\n换行替换成<p "kiss-p">。3、brToDiv和bnToDiv是替换成<div class="kiss-p">。`,
en: `1. br replaces <br> line breaks with <p "kiss-p">. 2. bn replaces \\n newline with <p "kiss-p">. 3. brToDiv and bnToDiv are replaced with <div class="kiss-p">.`,
},
import: {
zh: `导入`,
en: `Import`,
@@ -381,6 +486,10 @@ export const I18N = {
zh: `导出`,
en: `Export`,
},
export_translation: {
zh: `导出释义`,
en: `Export Translation`,
},
error_cant_be_blank: {
zh: `不能为空`,
en: `Can not be blank`,
@@ -433,10 +542,18 @@ export const I18N = {
zh: `重启浏览器时清除缓存`,
en: `Clear cache when restarting browser`,
},
data_sync_type: {
zh: `数据同步方式`,
en: `Data Sync Type`,
},
data_sync_url: {
zh: `数据同步接口`,
en: `Data Sync API`,
},
data_sync_user: {
zh: `数据同步账户`,
en: `Data Sync User`,
},
data_sync_key: {
zh: `数据同步密钥`,
en: `Data Sync Key`,
@@ -458,8 +575,8 @@ export const I18N = {
en: `Sorry, something went wrong!`,
},
error_sync_setting: {
zh: `您的同步设置未填写,无法在线分享。`,
en: `Your sync settings are missing and cannot be shared online.`,
zh: `您的同步类型必须为“KISS-Worker”且需填写完整`,
en: `Your sync type must be "KISS-Worker" and must be filled in completely`,
},
click_test: {
zh: `点击测试`,
@@ -527,14 +644,294 @@ export const I18N = {
},
hide_fab_button: {
zh: `隐藏悬浮按钮`,
en: `"Hide Fab Button`,
en: `Hide Fab Button`,
},
hide_tran_button: {
zh: `隐藏翻译按钮`,
en: `Hide Translate Button`,
},
hide_click_away: {
zh: `点击外部关闭弹窗`,
en: `Click outside to close the pop-up window`,
},
use_simple_style: {
zh: `使用简洁界面`,
en: `Use a simple interface`,
},
show: {
zh: `显示`,
en: `"Show`,
en: `Show`,
},
hide: {
zh: `隐藏`,
en: `"Hide`,
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: `Open Translate Popup/Translate Selected Shortcut`,
},
tranbtn_offset_x: {
zh: `翻译按钮偏移X±200`,
en: `Translate Button Offset X (±200)`,
},
tranbtn_offset_y: {
zh: `翻译按钮偏移Y±200`,
en: `Translate Button Offset Y (±200)`,
},
tranbox_offset_x: {
zh: `翻译框偏移X±200`,
en: `Translate Box Offset X (±200)`,
},
tranbox_offset_y: {
zh: `翻译框偏移Y±200`,
en: `Translate Box Offset Y (±200)`,
},
translated_text: {
zh: `译文`,
en: `Translated Text`,
},
original_text: {
zh: `原文`,
en: `Original Text`,
},
favorite_words: {
zh: `收藏词汇`,
en: `Favorite Words`,
},
touch_setting: {
zh: `触屏设置`,
en: `Touch Setting`,
},
touch_translate_shortcut: {
zh: `触屏翻译快捷方式`,
en: `Touch Translate Shortcut`,
},
touch_tap_0: {
zh: `禁用`,
en: `Disable`,
},
touch_tap_2: {
zh: `双指轻触`,
en: `Two finger tap`,
},
touch_tap_3: {
zh: `三指轻触`,
en: `Three finger tap`,
},
touch_tap_4: {
zh: `四指轻触`,
en: `Four finger tap`,
},
translate_blacklist: {
zh: `禁用翻译名单`,
en: `Translate Blacklist`,
},
disabled_csplist: {
zh: `禁用CSP名单`,
en: `Disabled CSP List`,
},
disabled_csplist_helper: {
zh: `3、通过调整CSP策略使得某些页面能够注入JS/CSS/Media请谨慎使用除非您已知晓相关风险。`,
en: `3. By adjusting the CSP policy, some pages can inject JS/CSS/Media. Please use it with caution unless you are aware of the related risks.`,
},
skip_langs: {
zh: `不翻译的语言`,
en: `Disable Languages`,
},
skip_langs_helper: {
zh: `此功能依赖准确的语言检测,建议启用远程语言检测。`,
en: `This feature relies on accurate language detection. It is recommended to enable remote language detection.`,
},
context_menus: {
zh: `右键菜单`,
en: `Context Menus`,
},
hide_context_menus: {
zh: `隐藏右键菜单`,
en: `Hide Context Menus`,
},
simple_context_menus: {
zh: `简单右键菜单`,
en: `Simple_context_menus Context Menus`,
},
secondary_context_menus: {
zh: `二级右键菜单`,
en: `Secondary Context Menus`,
},
mulkeys_help: {
zh: `支持用换行或英文逗号“,”分隔,轮询调用。`,
en: `Supports polling calls separated by newlines or English commas ",".`,
},
translation_element_tag: {
zh: `译文元素标签`,
en: `Translation Element Tag`,
},
show_only_translations: {
zh: `仅显示译文`,
en: `Show Only Translations`,
},
show_only_translations_help: {
zh: `非完美实现,某些页面可能有样式等问题。`,
en: `It is not a perfect implementation and some pages may have style issues.`,
},
translate_page_title: {
zh: `是否翻译页面标题`,
en: `Translate Page Title`,
},
more: {
zh: `更多`,
en: `More`,
},
fixer_selector: {
zh: `网页修复选择器`,
en: `Fixer Selector`,
},
reg_niutrans: {
zh: `获取小牛翻译密钥【简约翻译专属新用户注册赠送300万字符】`,
en: `Get NiuTrans APIKey [KISS Translator Exclusive New User Registration Free 3 Million Characters]`,
},
trigger_mode: {
zh: `触发方式`,
en: `Trigger Mode`,
},
trigger_click: {
zh: `点击触发`,
en: `Click Trigger`,
},
trigger_hover: {
zh: `鼠标悬停触发`,
en: `Hover Trigger`,
},
trigger_select: {
zh: `选中触发`,
en: `Select Trigger`,
},
extend_styles: {
zh: `附加样式`,
en: `Extend Styles`,
},
custom_option: {
zh: `自定义选项`,
en: `Custom Option`,
},
translate_selected_text: {
zh: `翻译选中文字`,
en: `Translate Selected Text`,
},
toggle_style: {
zh: `切换样式`,
en: `Toggle Style`,
},
open_menu: {
zh: `打开弹窗菜单`,
en: `Open Popup Menu`,
},
open_setting: {
zh: `打开设置`,
en: `Open Setting`,
},
follow_selection: {
zh: `翻译框跟随选中文本`,
en: `Transbox Follow Selection`,
},
translate_start_hook: {
zh: `翻译开始钩子函数`,
en: `Translate Start Hook`,
},
translate_start_hook_helper: {
zh: `翻译开始时运行,入参为: 翻译节点,原文文本。`,
en: `Run when translation starts, the input parameters are: translation node, original text.`,
},
translate_end_hook: {
zh: `翻译完成钩子函数`,
en: `Translate End Hook`,
},
translate_end_hook_helper: {
zh: `翻译完成时运行,入参为: 翻译节点,原文文本,译文文本,保留元素。`,
en: `Run when the translation is completed, the input parameters are: translation node, original text, translation text, retained elements.`,
},
translate_remove_hook: {
zh: `翻译移除钩子函数`,
en: `Translate Removed Hook`,
},
translate_remove_hook_helper: {
zh: `翻译移除时运行,入参为: 翻译节点。`,
en: `Run when translation is removed, the input parameters are: translation node.`,
},
english_dict: {
zh: `英文词典`,
en: `English Dictionary`,
},
};

View File

@@ -1,5 +1,6 @@
import {
DEFAULT_SELECTOR,
DEFAULT_KEEP_SELECTOR,
GLOBAL_KEY,
REMAIN_KEY,
SHADOW_KEY,
@@ -20,15 +21,18 @@ export {
};
export const STOKEY_MSAUTH = `${APP_NAME}_msauth`;
export const STOKEY_BDAUTH = `${APP_NAME}_bdauth`;
export const STOKEY_SETTING = `${APP_NAME}_setting`;
export const STOKEY_RULES = `${APP_NAME}_rules`;
export const STOKEY_WORDS = `${APP_NAME}_words`;
export const STOKEY_SYNC = `${APP_NAME}_sync`;
export const STOKEY_FAB = `${APP_NAME}_fab`;
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_STYLE = "toggleStyle";
export const CMD_OPEN_OPTIONS = "openOptions";
export const CMD_OPEN_TRANBOX = "openTranbox";
export const CLIENT_WEB = "web";
export const CLIENT_CHROME = "chrome";
@@ -37,22 +41,30 @@ export const CLIENT_FIREFOX = "firefox";
export const CLIENT_USERSCRIPT = "userscript";
export const CLIENT_EXTS = [CLIENT_CHROME, CLIENT_EDGE, CLIENT_FIREFOX];
export const KV_RULES_KEY = "KT_RULES";
export const KV_RULES_SHARE_KEY = "KT_RULES_SHARE";
export const KV_SETTING_KEY = "KT_SETTING";
export const KV_RULES_KEY = "kiss-rules.json";
export const KV_WORDS_KEY = "kiss-words.json";
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_SHARE = "KISS-Translator-SHARE";
export const CACHE_NAME = `${APP_NAME}_cache`;
export const MSG_FETCH = "fetch";
export const MSG_FETCH_LIMIT = "fetch_limit";
export const MSG_FETCH_CLEAR = "fetch_clear";
export const MSG_GET_HTTPCACHE = "get_httpcache";
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_STYLE = "trans_toggle_style";
export const MSG_OPEN_TRANBOX = "open_tranbox";
export const MSG_TRANS_GETRULE = "trans_getrule";
export const MSG_TRANS_PUTRULE = "trans_putrule";
export const MSG_TRANS_CURRULE = "trans_currule";
export const MSG_CONTEXT_MENUS = "context_menus";
export const MSG_COMMAND_SHORTCUTS = "command_shortcuts";
export const MSG_INJECT_JS = "inject_js";
export const MSG_INJECT_CSS = "inject_css";
export const MSG_UPDATE_CSP = "update_csp";
export const THEME_LIGHT = "light";
export const THEME_DARK = "dark";
@@ -64,19 +76,84 @@ export const URL_KISS_RULES_NEW_ISSUE =
"https://github.com/fishjar/kiss-rules/issues/new";
export const URL_RAW_PREFIX =
"https://raw.githubusercontent.com/fishjar/kiss-translator/master";
export const URL_CACHE_TRAN = `https://${APP_LCNAME}/translate`;
// api.cognitive.microsofttranslator.com
export const URL_MICROSOFT_TRAN =
"https://api-edge.cognitive.microsofttranslator.com/translate";
export const URL_MICROSOFT_AUTH = "https://edge.microsoft.com/translate/auth";
export const URL_MICROSOFT_LANGDETECT =
"https://api-edge.cognitive.microsofttranslator.com/detect?api-version=3.0";
export const URL_GOOGLE_TRAN =
"https://translate.googleapis.com/translate_a/single";
export const URL_BAIDU_LANGDETECT = "https://fanyi.baidu.com/langdetect";
export const URL_BAIDU_SUGGEST = "https://fanyi.baidu.com/sug";
export const URL_BAIDU_TTS = "https://fanyi.baidu.com/gettts";
export const URL_BAIDU_WEB = "https://fanyi.baidu.com/";
export const URL_BAIDU_TRANSAPI = "https://fanyi.baidu.com/transapi";
export const URL_BAIDU_TRANSAPI_V2 = "https://fanyi.baidu.com/v2transapi";
export const URL_DEEPLFREE_TRAN = "https://www2.deepl.com/jsonrpc";
export const URL_TENCENT_TRANSMART = "https://transmart.qq.com/api/imt";
export const URL_NIUTRANS_REG =
"https://niutrans.com/login?active=3&userSource=kiss-translator";
export const DEFAULT_USER_AGENT =
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36";
export const OPT_DICT_BAIDU = "Baidu";
export const OPT_TRANS_GOOGLE = "Google";
export const OPT_TRANS_MICROSOFT = "Microsoft";
export const OPT_TRANS_DEEPL = "DeepL";
export const OPT_TRANS_DEEPLX = "DeepLX";
export const OPT_TRANS_DEEPLFREE = "DeepLFree";
export const OPT_TRANS_NIUTRANS = "NiuTrans";
export const OPT_TRANS_BAIDU = "Baidu";
export const OPT_TRANS_TENCENT = "Tencent";
export const OPT_TRANS_OPENAI = "OpenAI";
export const OPT_TRANS_OPENAI_2 = "OpenAI2";
export const OPT_TRANS_OPENAI_3 = "OpenAI3";
export const OPT_TRANS_GEMINI = "Gemini";
export const OPT_TRANS_CLOUDFLAREAI = "CloudflareAI";
export const OPT_TRANS_OLLAMA = "Ollama";
export const OPT_TRANS_OLLAMA_2 = "Ollama2";
export const OPT_TRANS_OLLAMA_3 = "Ollama3";
export const OPT_TRANS_CUSTOMIZE = "Custom";
export const OPT_TRANS_CUSTOMIZE_2 = "Custom2";
export const OPT_TRANS_CUSTOMIZE_3 = "Custom3";
export const OPT_TRANS_CUSTOMIZE_4 = "Custom4";
export const OPT_TRANS_CUSTOMIZE_5 = "Custom5";
export const OPT_TRANS_ALL = [
OPT_TRANS_GOOGLE,
OPT_TRANS_MICROSOFT,
OPT_TRANS_BAIDU,
OPT_TRANS_TENCENT,
OPT_TRANS_DEEPL,
OPT_TRANS_DEEPLFREE,
OPT_TRANS_DEEPLX,
OPT_TRANS_NIUTRANS,
OPT_TRANS_OPENAI,
OPT_TRANS_OPENAI_2,
OPT_TRANS_OPENAI_3,
OPT_TRANS_GEMINI,
OPT_TRANS_CLOUDFLAREAI,
OPT_TRANS_OLLAMA,
OPT_TRANS_OLLAMA_2,
OPT_TRANS_OLLAMA_3,
OPT_TRANS_CUSTOMIZE,
OPT_TRANS_CUSTOMIZE_2,
OPT_TRANS_CUSTOMIZE_3,
OPT_TRANS_CUSTOMIZE_4,
OPT_TRANS_CUSTOMIZE_5,
];
export const OPT_LANGDETECTOR_ALL = [
OPT_TRANS_GOOGLE,
OPT_TRANS_MICROSOFT,
OPT_TRANS_BAIDU,
OPT_TRANS_TENCENT,
];
export const OPT_LANGS_TO = [
@@ -120,7 +197,9 @@ export const OPT_LANGS_TO = [
];
export const OPT_LANGS_FROM = [["auto", "Auto-detect"], ...OPT_LANGS_TO];
export const OPT_LANGS_SPECIAL = {
[OPT_TRANS_GOOGLE]: new Map(OPT_LANGS_FROM.map(([key]) => [key, key])),
[OPT_TRANS_MICROSOFT]: new Map([
...OPT_LANGS_FROM.map(([key]) => [key, key]),
["auto", ""],
["zh-CN", "zh-Hans"],
["zh-TW", "zh-Hant"],
@@ -131,11 +210,149 @@ export const OPT_LANGS_SPECIAL = {
["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", "auto"],
["zh-CN", "ZH"],
["zh-TW", "ZH"],
]),
[OPT_TRANS_NIUTRANS]: new Map([
...OPT_LANGS_FROM.map(([key]) => [key, key]),
["auto", "auto"],
["zh-CN", "zh"],
["zh-TW", "cht"],
]),
[OPT_TRANS_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_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
),
[OPT_TRANS_CUSTOMIZE]: new Map([["auto", ""]]),
[OPT_TRANS_OPENAI_2]: new Map(
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
),
[OPT_TRANS_OPENAI_3]: new Map(
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
),
[OPT_TRANS_GEMINI]: new Map(
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
),
[OPT_TRANS_OLLAMA]: new Map(
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
),
[OPT_TRANS_OLLAMA_2]: new Map(
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
),
[OPT_TRANS_OLLAMA_3]: new Map(
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
),
[OPT_TRANS_CLOUDFLAREAI]: new Map([
["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", ""],
]),
[OPT_TRANS_CUSTOMIZE_2]: new Map([
...OPT_LANGS_FROM.map(([key]) => [key, key]),
["auto", ""],
]),
[OPT_TRANS_CUSTOMIZE_3]: new Map([
...OPT_LANGS_FROM.map(([key]) => [key, key]),
["auto", ""],
]),
[OPT_TRANS_CUSTOMIZE_4]: new Map([
...OPT_LANGS_FROM.map(([key]) => [key, key]),
["auto", ""],
]),
[OPT_TRANS_CUSTOMIZE_5]: new Map([
...OPT_LANGS_FROM.map(([key]) => [key, key]),
["auto", ""],
]),
};
export const OPT_LANGS_LIST = OPT_LANGS_TO.map(([lang]) => lang);
export const OPT_LANGS_MICROSOFT = new Map(
Array.from(OPT_LANGS_SPECIAL[OPT_TRANS_MICROSOFT].entries()).map(([k, v]) => [
v,
k,
])
);
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_LINE = "under_line"; // 下划线
@@ -144,6 +361,7 @@ export const OPT_STYLE_DASHLINE = "dash_line"; // 虚线
export const OPT_STYLE_WAVYLINE = "wavy_line"; // 波浪线
export const OPT_STYLE_FUZZY = "fuzzy"; // 模糊
export const OPT_STYLE_HIGHLIGHT = "highlight"; // 高亮
export const OPT_STYLE_BLOCKQUOTE = "blockquote"; // 引用
export const OPT_STYLE_DIY = "diy_style"; // 自定义样式
export const OPT_STYLE_ALL = [
OPT_STYLE_NONE,
@@ -153,6 +371,7 @@ export const OPT_STYLE_ALL = [
OPT_STYLE_WAVYLINE,
OPT_STYLE_FUZZY,
OPT_STYLE_HIGHLIGHT,
OPT_STYLE_BLOCKQUOTE,
OPT_STYLE_DIY,
];
export const OPT_STYLE_USE_COLOR = [
@@ -161,40 +380,116 @@ export const OPT_STYLE_USE_COLOR = [
OPT_STYLE_DASHLINE,
OPT_STYLE_WAVYLINE,
OPT_STYLE_HIGHLIGHT,
OPT_STYLE_BLOCKQUOTE,
];
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 OPT_TIMING_PAGESCROLL = "mk_pagescroll"; // 滚动加载翻译
export const OPT_TIMING_PAGEOPEN = "mk_pageopen"; // 直接翻译到底
export const OPT_TIMING_MOUSEOVER = "mk_mouseover";
export const OPT_TIMING_CONTROL = "mk_ctrlKey";
export const OPT_TIMING_SHIFT = "mk_shiftKey";
export const OPT_TIMING_ALT = "mk_altKey";
export const OPT_TIMING_ALL = [
OPT_TIMING_PAGESCROLL,
OPT_TIMING_PAGEOPEN,
OPT_TIMING_MOUSEOVER,
OPT_TIMING_CONTROL,
OPT_TIMING_SHIFT,
OPT_TIMING_ALT,
];
export const DEFAULT_FETCH_LIMIT = 10; // 默认最大任务数量
export const DEFAULT_FETCH_INTERVAL = 100; // 默认任务间隔时间
export const PROMPT_PLACE_FROM = "{{from}}"; // 占位符
export const PROMPT_PLACE_TO = "{{to}}"; // 占位符
export const INPUT_PLACE_URL = "{{url}}"; // 占位符
export const INPUT_PLACE_FROM = "{{from}}"; // 占位符
export const INPUT_PLACE_TO = "{{to}}"; // 占位符
export const INPUT_PLACE_TEXT = "{{text}}"; // 占位符
export const INPUT_PLACE_KEY = "{{key}}"; // 占位符
export const INPUT_PLACE_MODEL = "{{model}}"; // 占位符
export const DEFAULT_COLOR = "#209CEE"; // 默认高亮背景色/线条颜色
export const DEFAULT_TRANS_TAG = "span";
export const DEFAULT_SELECT_STYLE =
"-webkit-line-clamp: unset; max-height: none; height: auto;";
// 全局规则
export const GLOBLA_RULE = {
pattern: "*",
selector: DEFAULT_SELECTOR,
pattern: "*", // 匹配网址
selector: DEFAULT_SELECTOR, // 选择器
keepSelector: DEFAULT_KEEP_SELECTOR, // 保留元素选择器
terms: "", // 专业术语
translator: OPT_TRANS_MICROSOFT, // 翻译服务
fromLang: "auto", // 源语言
toLang: "zh-CN", // 目标语言
textStyle: OPT_STYLE_DASHLINE, // 译文样式
transOpen: "false", // 开启翻译
bgColor: "", // 译文颜色
textDiyStyle: "", // 自定义译文样式
selectStyle: DEFAULT_SELECT_STYLE, // 选择器节点样式
parentStyle: DEFAULT_SELECT_STYLE, // 选择器父节点样式
injectJs: "", // 注入JS
injectCss: "", // 注入CSS
transOnly: "false", // 是否仅显示译文
transTiming: OPT_TIMING_PAGESCROLL, // 翻译时机/鼠标悬停翻译
transTag: DEFAULT_TRANS_TAG, // 译文元素标签
transTitle: "false", // 是否同时翻译页面标题
detectRemote: "false", // 是否使用远程语言检测
skipLangs: [], // 不翻译的语言
fixerSelector: "", // 修复函数选择器
fixerFunc: "-", // 修复函数
transStartHook: "", // 钩子函数
transEndHook: "", // 钩子函数
transRemoveHook: "", // 钩子函数
};
// 输入框翻译
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 PHONIC_MAP = {
en_phonic: ["英", "uk"],
us_phonic: ["美", "en"],
};
export const OPT_TRANBOX_TRIGGER_CLICK = "click";
export const OPT_TRANBOX_TRIGGER_HOVER = "hover";
export const OPT_TRANBOX_TRIGGER_SELECT = "select";
export const OPT_TRANBOX_TRIGGER_ALL = [
OPT_TRANBOX_TRIGGER_CLICK,
OPT_TRANBOX_TRIGGER_HOVER,
OPT_TRANBOX_TRIGGER_SELECT,
];
export const DEFAULT_TRANBOX_SHORTCUT = ["AltLeft", "KeyS"];
export const DEFAULT_TRANBOX_SETTING = {
transOpen: true,
translator: OPT_TRANS_MICROSOFT,
fromLang: "auto",
toLang: "zh-CN",
textStyle: OPT_STYLE_DASHLINE,
transOpen: "false",
bgColor: "",
textDiyStyle: "",
toLang2: "en",
tranboxShortcut: DEFAULT_TRANBOX_SHORTCUT,
btnOffsetX: 10,
btnOffsetY: 10,
boxOffsetX: 0,
boxOffsetY: 10,
hideTranBtn: false, // 是否隐藏翻译按钮
hideClickAway: false, // 是否点击外部关闭弹窗
simpleStyle: false, // 是否简洁界面
followSelection: false, // 翻译框是否跟随选中文本
triggerMode: OPT_TRANBOX_TRIGGER_CLICK, // 触发翻译方式
extStyles: "", // 附加样式
enDict: OPT_DICT_BAIDU, // 英文词典
};
// 订阅列表
@@ -214,29 +509,101 @@ export const DEFAULT_SUBRULES_LIST = [
];
// 翻译接口
const defaultCustomApi = {
url: "",
key: "",
customOption: "", // (作废)
reqHook: "", // request 钩子函数
resHook: "", // response 钩子函数
fetchLimit: DEFAULT_FETCH_LIMIT,
fetchInterval: DEFAULT_FETCH_INTERVAL,
};
const defaultOpenaiApi = {
url: "https://api.openai.com/v1/chat/completions",
key: "",
model: "gpt-4",
prompt: `You will be provided with a sentence in ${INPUT_PLACE_FROM}, and your task is to translate it into ${INPUT_PLACE_TO}.`,
temperature: 0,
maxTokens: 256,
fetchLimit: 1,
fetchInterval: 500,
};
const defaultOllamaApi = {
url: "http://localhost:11434/api/generate",
key: "",
model: "llama3",
prompt: `Translate the following text from ${INPUT_PLACE_FROM} to ${INPUT_PLACE_TO}:\n\n${INPUT_PLACE_TEXT}`,
fetchLimit: 1,
fetchInterval: 500,
};
export const DEFAULT_TRANS_APIS = {
[OPT_TRANS_GOOGLE]: {
url: "https://translate.googleapis.com/translate_a/single",
url: URL_GOOGLE_TRAN,
key: "",
fetchLimit: DEFAULT_FETCH_LIMIT, // 最大任务数量
fetchInterval: DEFAULT_FETCH_INTERVAL, // 任务间隔时间
},
[OPT_TRANS_MICROSOFT]: {
url: "https://api-edge.cognitive.microsofttranslator.com/translate",
authUrl: "https://edge.microsoft.com/translate/auth",
fetchLimit: DEFAULT_FETCH_LIMIT,
fetchInterval: DEFAULT_FETCH_INTERVAL,
},
[OPT_TRANS_BAIDU]: {
fetchLimit: DEFAULT_FETCH_LIMIT,
fetchInterval: DEFAULT_FETCH_INTERVAL,
},
[OPT_TRANS_TENCENT]: {
fetchLimit: DEFAULT_FETCH_LIMIT,
fetchInterval: DEFAULT_FETCH_INTERVAL,
},
[OPT_TRANS_DEEPL]: {
url: "https://api-free.deepl.com/v2/translate",
key: "",
fetchLimit: 1,
fetchInterval: 500,
},
[OPT_TRANS_OPENAI]: {
url: "https://api.openai.com/v1/chat/completion",
[OPT_TRANS_DEEPLFREE]: {
fetchLimit: 1,
fetchInterval: 500,
},
[OPT_TRANS_DEEPLX]: {
url: "http://localhost:1188/translate",
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}.`,
fetchLimit: 1,
fetchInterval: 500,
},
[OPT_TRANS_CUSTOMIZE]: {
url: "",
[OPT_TRANS_NIUTRANS]: {
url: "https://api.niutrans.com/NiuTransServer/translation",
key: "",
dictNo: "",
memoryNo: "",
fetchLimit: DEFAULT_FETCH_LIMIT,
fetchInterval: DEFAULT_FETCH_INTERVAL,
},
[OPT_TRANS_OPENAI]: defaultOpenaiApi,
[OPT_TRANS_OPENAI_2]: defaultOpenaiApi,
[OPT_TRANS_OPENAI_3]: defaultOpenaiApi,
[OPT_TRANS_GEMINI]: {
url: `https://generativelanguage.googleapis.com/v1/models/${INPUT_PLACE_MODEL}:generateContent?key=${INPUT_PLACE_KEY}`,
key: "",
model: "gemini-pro",
prompt: `Translate the following text from ${INPUT_PLACE_FROM} to ${INPUT_PLACE_TO}:\n\n${INPUT_PLACE_TEXT}`,
fetchLimit: 1,
fetchInterval: 500,
},
[OPT_TRANS_CLOUDFLAREAI]: {
url: "https://api.cloudflare.com/client/v4/accounts/{{ACCOUNT_ID}}/ai/run/@cf/meta/m2m100-1.2b",
key: "",
fetchLimit: 1,
fetchInterval: 500,
},
[OPT_TRANS_OLLAMA]: defaultOllamaApi,
[OPT_TRANS_OLLAMA_2]: defaultOllamaApi,
[OPT_TRANS_OLLAMA_3]: defaultOllamaApi,
[OPT_TRANS_CUSTOMIZE]: defaultCustomApi,
[OPT_TRANS_CUSTOMIZE_2]: defaultCustomApi,
[OPT_TRANS_CUSTOMIZE_3]: defaultCustomApi,
[OPT_TRANS_CUSTOMIZE_4]: defaultCustomApi,
[OPT_TRANS_CUSTOMIZE_5]: defaultCustomApi,
};
// 默认快捷键
@@ -245,43 +612,68 @@ 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]: ["Alt", "q"],
[OPT_SHORTCUT_STYLE]: ["Alt", "c"],
[OPT_SHORTCUT_POPUP]: ["Alt", "k"],
[OPT_SHORTCUT_SETTING]: ["Alt", "o"],
[OPT_SHORTCUT_TRANSLATE]: ["AltLeft", "KeyQ"],
[OPT_SHORTCUT_STYLE]: ["AltLeft", "KeyC"],
[OPT_SHORTCUT_POPUP]: ["AltLeft", "KeyK"],
[OPT_SHORTCUT_SETTING]: ["AltLeft", "KeyO"],
};
export const TRANS_MIN_LENGTH = 5; // 最短翻译长度
export const TRANS_MAX_LENGTH = 5000; // 最长翻译长度
export const TRANS_NEWLINE_LENGTH = 40; // 换行字符数
export const TRANS_NEWLINE_LENGTH = 20; // 换行字符数
export const DEFAULT_BLACKLIST = [
"https://fishjar.github.io/kiss-translator/options.html",
"https://translate.google.com",
"https://www.deepl.com/translator",
"oapi.dingtalk.com",
"login.dingtalk.com",
]; // 禁用翻译名单
export const DEFAULT_CSPLIST = ["https://github.com"]; // 禁用CSP名单
export const DEFAULT_SETTING = {
darkMode: false, // 深色模式
uiLang: "en", // 界面语言
fetchLimit: DEFAULT_FETCH_LIMIT, // 最大任务数量
fetchInterval: DEFAULT_FETCH_INTERVAL, // 任务间隔时间
// fetchLimit: DEFAULT_FETCH_LIMIT, // 最大任务数量(移至transApis作废)
// fetchInterval: DEFAULT_FETCH_INTERVAL, // 任务间隔时间(移至transApis作废)
minLength: TRANS_MIN_LENGTH,
maxLength: TRANS_MAX_LENGTH,
newlineLength: TRANS_NEWLINE_LENGTH,
clearCache: false, // 是否在浏览器下次启动时清除缓存
injectRules: true, // 是否注入订阅规则
injectWebfix: true, // 是否注入修复补丁
// injectWebfix: true, // 是否注入修复补丁(作废)
// detectRemote: false, // 是否使用远程语言检测(移至rule作废)
// contextMenus: true, // 是否添加右键菜单(作废)
contextMenuType: 1, // 右键菜单类型(0不显示1简单菜单2多级菜单)
// transTag: DEFAULT_TRANS_TAG, // 译文元素标签(移至rule作废)
// transOnly: false, // 是否仅显示译文(移至rule作废)
// transTitle: false, // 是否同时翻译页面标题(移至rule作废)
subrulesList: DEFAULT_SUBRULES_LIST, // 订阅列表
owSubrule: DEFAULT_OW_RULE, // 覆写订阅规则
transApis: DEFAULT_TRANS_APIS, // 翻译接口
mouseKey: OPT_MOUSEKEY_DISABLE, // 鼠标悬停翻译
// mouseKey: OPT_TIMING_PAGESCROLL, // 翻译时机/鼠标悬停翻译(移至rule作废)
shortcuts: DEFAULT_SHORTCUTS, // 快捷键
hideFab: false, // 是否隐藏按钮
inputRule: DEFAULT_INPUT_RULE, // 输入框设置
tranboxSetting: DEFAULT_TRANBOX_SETTING, // 划词翻译设置
touchTranslate: 2, // 触屏翻译
blacklist: DEFAULT_BLACKLIST.join(",\n"), // 禁用翻译名单
csplist: DEFAULT_CSPLIST.join(",\n"), // 禁用CSP名单
// disableLangs: [], // 不翻译的语言(移至rule作废)
transInterval: 500, // 翻译间隔时间
langDetector: OPT_TRANS_MICROSOFT, // 远程语言识别服务
};
export const DEFAULT_RULES = [GLOBLA_RULE];
export const 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 = {
syncType: OPT_SYNCTYPE_WORKER, // 同步方式
syncUrl: "", // 数据同步接口
syncUser: "", // 数据同步用户名
syncKey: "", // 数据同步密钥
settingUpdateAt: 0,
settingSyncAt: 0,
rulesUpdateAt: 0,
rulesSyncAt: 0,
syncMeta: {}, // 数据更新及同步信息
subRulesSyncAt: 0, // 订阅规则同步时间
dataCaches: {}, // 缓存同步时间
};

View File

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

View File

@@ -1,56 +1,3 @@
import { browser } from "./libs/browser";
import {
MSG_TRANS_TOGGLE,
MSG_TRANS_TOGGLE_STYLE,
MSG_TRANS_GETRULE,
MSG_TRANS_PUTRULE,
} from "./config";
import { getSettingWithDefault, getRulesWithDefault } from "./libs/storage";
import { Translator } from "./libs/translator";
import { isIframe } from "./libs/iframe";
import { matchRule } from "./libs/rules";
import { webfix } from "./libs/webfix";
import { run } from "./common";
/**
* 入口函数
*/
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);
webfix(href, setting);
// 监听消息
browser?.runtime.onMessage.addListener(async ({ action, args }) => {
switch (action) {
case MSG_TRANS_TOGGLE:
translator.toggle();
break;
case MSG_TRANS_TOGGLE_STYLE:
translator.toggleStyle();
break;
case MSG_TRANS_GETRULE:
break;
case MSG_TRANS_PUTRULE:
translator.updateRule(args);
break;
default:
return { error: `message action is unavailable: ${action}` };
}
return { data: translator.rule };
});
};
(async () => {
try {
await init();
} catch (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);
}
})();
run();

View File

@@ -8,7 +8,10 @@ export function useApi(translator) {
const updateApi = useCallback(
async (obj) => {
const api = transApis[translator] || {};
const api = {
...DEFAULT_TRANS_APIS[translator],
...(transApis[translator] || {}),
};
Object.assign(transApis, { [translator]: { ...api, ...obj } });
await updateSetting({ transApis });
},
@@ -20,5 +23,12 @@ export function useApi(translator) {
await updateSetting({ transApis });
}, [translator, transApis, updateSetting]);
return { api: transApis[translator] || {}, updateApi, resetApi };
return {
api: {
...DEFAULT_TRANS_APIS[translator],
...(transApis[translator] || {}),
},
updateApi,
resetApi,
};
}

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

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

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 };
}

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

@@ -0,0 +1,68 @@
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";
import { kissLog } from "../libs/log";
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) {
kissLog(err, "query fav");
} finally {
setLoading(false);
}
})();
}, []);
return { loading, favWords, toggleFav, mergeWords, clearWords };
}

View File

@@ -2,6 +2,14 @@ import { useSetting } from "./Setting";
import { I18N, URL_RAW_PREFIX } from "../config";
import { useFetch } from "./Fetch";
export const getI18n = (uiLang, key, defaultText = "") => {
return I18N?.[key]?.[uiLang] ?? defaultText;
};
export const useLangMap = (uiLang) => {
return (key, defaultText = "") => getI18n(uiLang, key, defaultText);
};
/**
* 多语言 hook
* @returns
@@ -10,7 +18,7 @@ export const useI18n = () => {
const {
setting: { uiLang },
} = useSetting();
return (key, defaultText = "") => I18N?.[key]?.[uiLang] ?? defaultText;
return useLangMap(uiLang);
};
export const useI18nMd = (key) => {

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,8 +1,9 @@
import { STOKEY_RULES, DEFAULT_RULES } from "../config";
import { STOKEY_RULES, DEFAULT_RULES, KV_RULES_KEY } from "../config";
import { useStorage } from "./Storage";
import { trySyncRules } from "../libs/sync";
import { checkRules } from "../libs/rules";
import { useCallback } from "react";
import { useSyncMeta } from "./Sync";
/**
* 规则 hook
@@ -10,13 +11,15 @@ import { useCallback } from "react";
*/
export function useRules() {
const { data: list, save } = useStorage(STOKEY_RULES, DEFAULT_RULES);
const { updateSyncMeta } = useSyncMeta();
const updateRules = useCallback(
async (rules) => {
await save(rules);
await updateSyncMeta(KV_RULES_KEY);
trySyncRules();
},
[save]
[save, updateSyncMeta]
);
const add = useCallback(

View File

@@ -1,20 +1,19 @@
import { STOKEY_SETTING, DEFAULT_SETTING } from "../config";
import { STOKEY_SETTING, DEFAULT_SETTING, KV_SETTING_KEY } from "../config";
import { useStorage } from "./Storage";
import { trySyncSetting } from "../libs/sync";
import { createContext, useCallback, useContext, useMemo } from "react";
import { debounce } from "../libs/utils";
import { useSyncMeta } from "./Sync";
const SettingContext = createContext({
setting: {},
setting: null,
updateSetting: async () => {},
reloadSetting: async () => {},
});
export function SettingProvider({ children }) {
const { data, update, reload, loading } = useStorage(
STOKEY_SETTING,
DEFAULT_SETTING
);
const { data, update, reload } = useStorage(STOKEY_SETTING, DEFAULT_SETTING);
const { updateSyncMeta } = useSyncMeta();
const syncSetting = useMemo(
() =>
@@ -27,12 +26,13 @@ export function SettingProvider({ children }) {
const updateSetting = useCallback(
async (obj) => {
await update(obj);
await updateSyncMeta(KV_SETTING_KEY);
syncSetting();
},
[update, syncSetting]
[update, syncSetting, updateSyncMeta]
);
if (loading) {
if (!data) {
return;
}

View File

@@ -1,9 +1,16 @@
import { useCallback, useEffect, useState } from "react";
import { storage } from "../libs/storage";
import { kissLog } from "../libs/log";
export function useStorage(key, defaultVal = null) {
const [loading, setLoading] = useState(true);
const [data, setData] = useState(defaultVal);
/**
*
* @param {*} key
* @param {*} defaultVal 需为调用hook外的常量
* @returns
*/
export function useStorage(key, defaultVal) {
const [loading, setLoading] = useState(false);
const [data, setData] = useState(null);
const save = useCallback(
async (val) => {
@@ -15,7 +22,7 @@ export function useStorage(key, defaultVal = null) {
const update = useCallback(
async (obj) => {
setData((pre) => ({ ...pre, ...obj }));
setData((pre = {}) => ({ ...pre, ...obj }));
await storage.putObj(key, obj);
},
[key]
@@ -27,26 +34,37 @@ export function useStorage(key, defaultVal = null) {
}, [key]);
const reload = useCallback(async () => {
const val = await storage.getObj(key);
if (val) {
setData(val);
} else if (defaultVal) {
await storage.setObj(key, defaultVal);
try {
setLoading(true);
const val = await storage.getObj(key);
if (val) {
setData(val);
}
} catch (err) {
kissLog(err, "storage reload");
} finally {
setLoading(false);
}
}, [key, defaultVal]);
}, [key]);
useEffect(() => {
(async () => {
try {
setLoading(true);
await reload();
const val = await storage.getObj(key);
if (val) {
setData(val);
} else if (defaultVal) {
setData(defaultVal);
await storage.setObj(key, defaultVal);
}
} catch (err) {
//
kissLog(err, "storage load");
} finally {
setLoading(false);
}
})();
}, [reload]);
}, [key, defaultVal]);
return { data, save, update, remove, reload, loading };
}

View File

@@ -3,6 +3,7 @@ import { useSetting } from "./Setting";
import { useCallback, useEffect, useMemo, useState } from "react";
import { loadOrFetchSubRules } from "../libs/subRules";
import { delSubRules } from "../libs/storage";
import { kissLog } from "../libs/log";
/**
* 订阅规则
@@ -48,7 +49,7 @@ export function useSubRules() {
const addSub = useCallback(
async (url) => {
const subrulesList = [...list];
subrulesList.push({ url, selected: false, syncAt: Date.now() });
subrulesList.push({ url, selected: false });
await updateSetting({ subrulesList });
},
[list, updateSetting]
@@ -72,7 +73,7 @@ export function useSubRules() {
const rules = await loadOrFetchSubRules(selectedUrl);
setSelectedRules(rules);
} catch (err) {
console.log("[loadOrFetchSubRules]", err);
kissLog(err, "loadOrFetchSubRules");
} finally {
setLoading(false);
}

View File

@@ -1,3 +1,4 @@
import { useCallback } from "react";
import { STOKEY_SYNC, DEFAULT_SYNC } from "../config";
import { useStorage } from "./Storage";
@@ -6,6 +7,57 @@ import { useStorage } from "./Storage";
* @returns
*/
export function useSync() {
const { data, update } = useStorage(STOKEY_SYNC, DEFAULT_SYNC);
return { sync: data, updateSync: update };
const { data, update, reload } = useStorage(STOKEY_SYNC, DEFAULT_SYNC);
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,
};
}

View File

@@ -1,6 +1,6 @@
import { useMemo } from "react";
import { ThemeProvider, createTheme } from "@mui/material/styles";
import CssBaseline from "@mui/material/CssBaseline";
import { CssBaseline, GlobalStyles } from "@mui/material";
import { useDarkMode } from "./ColorMode";
import { THEME_DARK, THEME_LIGHT } from "../config";
@@ -9,13 +9,27 @@ import { THEME_DARK, THEME_LIGHT } from "../config";
* @param {*} param0
* @returns
*/
export default function Theme({ children, options }) {
export default function Theme({ children, options, styles }) {
const { darkMode } = useDarkMode();
const theme = useMemo(() => {
let htmlFontSize = 16;
try {
const s = window.getComputedStyle(document.body.parentNode).fontSize;
const fontSize = parseInt(s.replace("px", ""));
if (fontSize > 0 && fontSize < 1000) {
htmlFontSize = fontSize;
}
} catch (err) {
//
}
return createTheme({
palette: {
mode: darkMode ? THEME_DARK : THEME_LIGHT,
},
typography: {
htmlFontSize,
},
...options,
});
}, [darkMode, options]);
@@ -24,6 +38,7 @@ export default function Theme({ children, options }) {
<ThemeProvider theme={theme}>
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
<CssBaseline />
<GlobalStyles styles={styles} />
{children}
</ThemeProvider>
);

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

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

View File

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

View File

@@ -1,12 +1,13 @@
import { getMsauth, setMsauth } from "./storage";
import { URL_MICROSOFT_AUTH } from "../config";
import { fetchData } from "./fetch";
import { fetchHandle } from "./fetch";
import { kissLog } from "./log";
const parseMSToken = (token) => {
try {
return JSON.parse(atob(token.split(".")[1])).exp;
} catch (err) {
console.log("[parseMSToken]", err);
kissLog(err, "parseMSToken");
}
return 0;
};
@@ -34,7 +35,7 @@ const _msAuth = () => {
}
// 缓存没有或失效,查询接口
token = await fetchData(URL_MICROSOFT_AUTH);
token = await fetchHandle({ input: URL_MICROSOFT_AUTH });
exp = parseMSToken(token);
await setMsauth({ token, exp });
return [token, exp];

10
src/libs/blacklist.js Normal file
View File

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

View File

@@ -8,8 +8,10 @@ function _browser() {
try {
return require("webextension-polyfill");
} catch (err) {
// console.log("[browser]", err.message);
// kissLog(err, "browser");
}
}
export const browser = _browser();
export const isBg = () => globalThis?.ContextType === "BACKGROUND";

View File

@@ -3,16 +3,35 @@ import { sendBgMsg } from "./msg";
import { taskPool } from "./pool";
import {
MSG_FETCH,
MSG_FETCH_LIMIT,
MSG_FETCH_CLEAR,
MSG_GET_HTTPCACHE,
CACHE_NAME,
OPT_TRANS_MICROSOFT,
OPT_TRANS_DEEPL,
OPT_TRANS_OPENAI,
DEFAULT_FETCH_INTERVAL,
DEFAULT_FETCH_LIMIT,
} from "../config";
import { msAuth } from "./auth";
import { isBg } from "./browser";
import { genTransReq } from "../apis/trans";
import { kissLog } from "./log";
import { blobToBase64 } from "./utils";
const TIMEOUT = 5000;
/**
* 构造缓存 request
* @param {*} input
* @param {*} init
* @returns
*/
const newCacheReq = async (input, init) => {
let request = new Request(input, init);
if (request.method !== "GET") {
const body = await request.text();
const cacheUrl = new URL(request.url);
cacheUrl.pathname += body;
request = new Request(cacheUrl.toString(), { method: "GET" });
}
return request;
};
/**
* 油猴脚本的请求封装
@@ -27,55 +46,39 @@ export const fetchGM = async (input, { method = "GET", headers, body } = {}) =>
url: input,
headers,
data: body,
onload: (response) => {
if (response.status === 200) {
const headers = new Headers();
response.responseHeaders.split("\n").forEach((line) => {
const [name, value] = line.split(":").map((item) => item.trim());
if (name && value) {
headers.append(name, value);
}
});
resolve(new Response(response.response, { headers }));
} else {
reject(new Error(`[${response.status}] ${response.responseText}`));
}
// withCredentials: true,
timeout: TIMEOUT,
onload: ({ response, responseHeaders, status, statusText }) => {
const headers = {};
responseHeaders.split("\n").forEach((line) => {
const [name, value] = line.split(":").map((item) => item.trim());
if (name && value) {
headers[name] = value;
}
});
resolve({
body: response,
headers,
status,
statusText,
});
},
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
* @returns
*/
const fetchApi = async ({ input, init = {}, translator, token }) => {
if (token) {
if (translator === OPT_TRANS_DEEPL) {
init.headers["Authorization"] = `DeepL-Auth-Key ${token}`; // DeepL
} else if (translator === OPT_TRANS_OPENAI) {
init.headers["Authorization"] = `Bearer ${token}`; // OpenAI
init.headers["api-key"] = token; // Azure OpenAI
} else {
init.headers["Authorization"] = `Bearer ${token}`; // Microsoft & others
}
export const fetchPatcher = async (input, init, transOpts, apiSetting) => {
if (transOpts?.translator) {
[input, init] = await genTransReq(transOpts, apiSetting);
}
if (!input) {
throw new Error("url is empty");
}
if (isGm) {
@@ -85,113 +88,189 @@ const fetchApi = async ({ input, init = {}, translator, token }) => {
} else {
info = GM.info;
}
// Tampermonkey --> .connects
// Violentmonkey --> .connect
const connects = info?.script?.connects || info?.script?.connect || [];
const url = new URL(input);
const isSafe = connects.find((item) => url.hostname.endsWith(item));
if (isSafe) {
if (window.KISS_GM) {
return window.KISS_GM.fetch(input, init);
} else {
return fetchGM(input, init);
}
const { body, headers, status, statusText } = window.KISS_GM
? await window.KISS_GM.fetch(input, init)
: await fetchGM(input, init);
return new Response(body, {
headers: new Headers(headers),
status,
statusText,
});
}
}
if (AbortSignal?.timeout) {
Object.assign(init, { signal: AbortSignal.timeout(TIMEOUT) });
}
return fetch(input, init);
};
/**
* 解析 response
* @param {*} res
* @returns
*/
const parseResponse = async (res) => {
if (!res) {
return null;
}
const contentType = res.headers.get("Content-Type");
if (contentType?.includes("json")) {
return await res.json();
} else if (contentType?.includes("audio")) {
const blob = await res.blob();
return await blobToBase64(blob);
}
return await res.text();
};
/**
* 查询 caches
* @param {*} input
* @param {*} param1
* @returns
*/
export const getHttpCache = async (input, { method, headers, body }) => {
try {
const req = await newCacheReq(input, { method, headers, body });
const cache = await caches.open(CACHE_NAME);
const res = await cache.match(req);
return parseResponse(res);
} catch (err) {
kissLog(err, "get cache");
}
return null;
};
/**
* 插入 caches
* @param {*} input
* @param {*} param1
* @param {*} res
*/
export const putHttpCache = async (input, { method, headers, body }, res) => {
try {
const req = await newCacheReq(input, { method, headers, body });
const cache = await caches.open(CACHE_NAME);
await cache.put(req, res);
} catch (err) {
kissLog(err, "put cache");
}
};
/**
* 处理请求
* @param {*} param0
* @returns
*/
export const fetchHandle = async ({
input,
useCache,
transOpts,
apiSetting,
...init
}) => {
// 发送请求
const res = await fetchPatcher(input, init, transOpts, apiSetting);
if (!res) {
throw new Error("Unknow error");
} else if (!res.ok) {
const msg = {
url: res.url,
status: res.status,
};
if (res.headers.get("Content-Type")?.includes("json")) {
msg.response = await res.json();
}
throw new Error(JSON.stringify(msg));
}
// 插入缓存
if (useCache) {
await putHttpCache(input, init, res.clone());
}
return parseResponse(res);
};
/**
* fetch 兼容性封装
* @param {*} args
* @returns
*/
export const fetchPolyfill = (args) => {
// 插件
if (isExt && !isBg()) {
return sendBgMsg(MSG_FETCH, args);
}
// 油猴/网页/BackgroundPage
return fetchHandle(args);
};
/**
* getHttpCache 兼容性封装
* @param {*} input
* @param {*} init
* @returns
*/
export const getHttpCachePolyfill = (input, init) => {
// 插件
if (isExt && !isBg()) {
return sendBgMsg(MSG_GET_HTTPCACHE, { input, init });
}
// 油猴/网页/BackgroundPage
return getHttpCache(input, init);
};
/**
* 请求池实例
*/
export const fetchPool = taskPool(
fetchApi,
async ({ translator }) => {
if (translator === OPT_TRANS_MICROSOFT) {
const [token] = await msAuth();
return { token };
}
return {};
},
fetchPolyfill,
null,
DEFAULT_FETCH_INTERVAL,
DEFAULT_FETCH_LIMIT
);
/**
* 请求数据统一接口
* 数据请求
* @param {*} input
* @param {*} opts
* @param {*} param1
* @returns
*/
export const fetchData = async (
input,
{ useCache, usePool, translator, token, ...init } = {}
) => {
const cacheReq = await newCacheReq(new Request(input, init));
let res;
// 查询缓存
if (useCache) {
try {
const cache = await caches.open(CACHE_NAME);
res = await cache.match(cacheReq);
} catch (err) {
console.log("[cache match]", err.message);
}
}
if (!res) {
// 发送请求
if (usePool) {
res = await fetchPool.push({ input, init, translator, token });
} else {
res = await fetchApi({ input, init, translator, token });
}
if (!res?.ok) {
throw new Error(`response: ${res.statusText}`);
}
// 插入缓存
if (useCache) {
try {
const cache = await caches.open(CACHE_NAME);
await cache.put(cacheReq, res.clone());
} catch (err) {
console.log("[cache put]", err.message);
}
}
}
const contentType = res.headers.get("Content-Type");
if (contentType?.includes("json")) {
return await res.json();
}
return await res.text();
};
/**
* fetch 兼容性封装
* @param {*} input
* @param {*} opts
* @returns
*/
export const fetchPolyfill = async (input, { isBg = false, ...opts } = {}) => {
if (!input.trim()) {
export const fetchData = async (input, { useCache, usePool, ...args } = {}) => {
if (!input?.trim()) {
throw new Error("URL is empty");
}
// 插件
if (isExt && !isBg) {
const res = await sendBgMsg(MSG_FETCH, { input, opts });
if (res.error) {
throw new Error(res.error);
// 查询缓存
if (useCache) {
const cache = await getHttpCachePolyfill(input, args);
if (cache) {
return cache;
}
return res.data;
}
// 油猴/网页/BackgroundPage
return await fetchData(input, opts);
// 通过任务池发送请求
if (usePool) {
return fetchPool.push({ input, useCache, ...args });
}
// 直接请求
return fetchPolyfill({ input, useCache, ...args });
};
/**
@@ -199,27 +278,13 @@ export const fetchPolyfill = async (input, { isBg = false, ...opts } = {}) => {
* @param {*} interval
* @param {*} limit
*/
export const updateFetchPool = async (interval, limit) => {
if (isExt) {
const res = await sendBgMsg(MSG_FETCH_LIMIT, { interval, limit });
if (res.error) {
throw new Error(res.error);
}
} else {
fetchPool.update(interval, limit);
}
export const updateFetchPool = (interval, limit) => {
fetchPool.update(interval, limit);
};
/**
* 清空任务池
*/
export const clearFetchPool = async () => {
if (isExt) {
const res = await sendBgMsg(MSG_FETCH_CLEAR);
if (res.error) {
throw new Error(res.error);
}
} else {
fetchPool.clear();
}
export const clearFetchPool = () => {
fetchPool.clear();
};

View File

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

View File

@@ -1,5 +1,25 @@
import { CACHE_NAME } from "../config";
import {
CACHE_NAME,
OPT_TRANS_GOOGLE,
OPT_TRANS_MICROSOFT,
OPT_TRANS_BAIDU,
OPT_TRANS_TENCENT,
} from "../config";
import { browser } from "./browser";
import {
apiGoogleLangdetect,
apiMicrosoftLangdetect,
apiBaiduLangdetect,
apiTencentLangdetect,
} from "../apis";
import { kissLog } from "./log";
const langdetectMap = {
[OPT_TRANS_GOOGLE]: apiGoogleLangdetect,
[OPT_TRANS_MICROSOFT]: apiMicrosoftLangdetect,
[OPT_TRANS_BAIDU]: apiBaiduLangdetect,
[OPT_TRANS_TENCENT]: apiTencentLangdetect,
};
/**
* 清除缓存数据
@@ -8,20 +28,38 @@ export const tryClearCaches = async () => {
try {
caches.delete(CACHE_NAME);
} catch (err) {
console.log("[clean caches]", err.message);
kissLog(err, "clean caches");
}
};
/**
* 本地语言识别
* 语言识别
* @param {*} q
* @returns
*/
export const tryDetectLang = async (q) => {
try {
const res = await browser?.i18n?.detectLanguage(q);
return res?.languages?.[0]?.language;
} catch (err) {
console.log("[detect lang]", err.message);
export const tryDetectLang = async (
q,
useRemote = false,
langDetector = OPT_TRANS_MICROSOFT
) => {
let lang = "";
if (useRemote) {
try {
lang = await langdetectMap[langDetector](q);
} catch (err) {
kissLog(err, "detect lang remote");
}
}
if (!lang) {
try {
const res = await browser?.i18n?.detectLanguage(q);
lang = res?.languages?.[0]?.language;
} catch (err) {
kissLog(err, "detect lang local");
}
}
return lang;
};

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

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

199
src/libs/inputTranslate.js Normal file
View File

@@ -0,0 +1,199 @@
import {
DEFAULT_INPUT_RULE,
DEFAULT_TRANS_APIS,
DEFAULT_INPUT_SHORTCUT,
OPT_LANGS_LIST,
} from "../config";
import { genEventName, removeEndchar, matchInputStr, sleep } from "./utils";
import { stepShortcutRegister } from "./shortcut";
import { apiTranslate } from "../apis";
import { loadingSvg } from "./svg";
import { kissLog } from "./log";
function isInputNode(node) {
return node.nodeName === "INPUT" || node.nodeName === "TEXTAREA";
}
function isEditAbleNode(node) {
return node.hasAttribute("contenteditable");
}
function selectContent(node) {
node.focus();
const range = document.createRange();
range.selectNodeContents(node);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
}
function pasteContentEvent(node, text) {
node.focus();
const data = new DataTransfer();
data.setData("text/plain", text);
const event = new ClipboardEvent("paste", { clipboardData: data });
document.dispatchEvent(event);
data.clearData();
}
function pasteContentCommand(node, text) {
node.focus();
document.execCommand("insertText", false, text);
}
function collapseToEnd(node) {
node.focus();
const selection = window.getSelection();
selection.collapseToEnd();
}
function getNodeText(node) {
if (isInputNode(node)) {
return node.value;
}
return node.innerText || node.textContent || "";
}
function addLoading(node, loadingId) {
const div = document.createElement("div");
div.id = loadingId;
div.innerHTML = loadingSvg;
div.style.cssText = `
width: ${node.offsetWidth}px;
height: ${node.offsetHeight}px;
line-height: ${node.offsetHeight}px;
position: absolute;
text-align: center;
left: ${node.offsetLeft}px;
top: ${node.offsetTop}px;
z-index: 2147483647;
`;
node.offsetParent?.appendChild(div);
}
function removeLoading(node, loadingId) {
const div = node.offsetParent.querySelector(`#${loadingId}`);
if (div) {
div.remove();
}
}
/**
* 输入框翻译
*/
export default function inputTranslate({
inputRule: {
transOpen,
triggerShortcut,
translator,
fromLang,
toLang,
triggerCount,
triggerTime,
transSign,
} = DEFAULT_INPUT_RULE,
transApis,
}) {
if (!transOpen) {
return;
}
const apiSetting = transApis?.[translator] || DEFAULT_TRANS_APIS[translator];
if (triggerShortcut.length === 0) {
triggerShortcut = DEFAULT_INPUT_SHORTCUT;
triggerCount = 1;
}
stepShortcutRegister(
triggerShortcut,
async () => {
let node = document.activeElement;
if (!node) {
return;
}
while (node.shadowRoot) {
node = node.shadowRoot.activeElement;
}
if (!isInputNode(node) && !isEditAbleNode(node)) {
return;
}
let initText = getNodeText(node);
if (triggerShortcut.length === 1 && triggerShortcut[0].length === 1) {
// todo: remove multiple char
initText = removeEndchar(initText, triggerShortcut[0], triggerCount);
}
if (!initText.trim()) {
return;
}
let text = initText;
if (transSign) {
const res = matchInputStr(text, transSign);
if (res) {
let lang = res[1];
if (lang === "zh" || lang === "cn") {
lang = "zh-CN";
} else if (lang === "tw" || lang === "hk") {
lang = "zh-TW";
}
if (lang && OPT_LANGS_LIST.includes(lang)) {
toLang = lang;
}
text = res[2];
}
}
// console.log("input -->", text);
const loadingId = "kiss-" + genEventName();
try {
addLoading(node, loadingId);
const [trText, isSame] = await apiTranslate({
translator,
text,
fromLang,
toLang,
apiSetting,
});
if (!trText || isSame) {
return;
}
if (isInputNode(node)) {
node.value = trText;
node.dispatchEvent(
new Event("input", { bubbles: true, cancelable: true })
);
return;
}
selectContent(node);
await sleep(200);
pasteContentEvent(node, trText);
await sleep(200);
// todo: use includes?
if (getNodeText(node).startsWith(initText)) {
pasteContentCommand(node, trText);
await sleep(100);
} else {
collapseToEnd(node);
}
} catch (err) {
kissLog(err, "translate input");
} finally {
removeLoading(node, loadingId);
}
},
triggerCount,
triggerTime
);
}

16
src/libs/interpreter.js Normal file
View File

@@ -0,0 +1,16 @@
import Sval from "sval";
const interpreter = new Sval({
// ECMA Version of the code
// 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15
// or 2015 | 2016 | 2017 | 2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024
// or "latest"
ecmaVer: "latest",
// Code source type
// "script" or "module"
sourceType: "script",
// Whether the code runs in a sandbox
sandBox: true,
});
export default interpreter;

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

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

View File

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

View File

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

View File

@@ -6,11 +6,14 @@ import {
OPT_STYLE_ALL,
OPT_LANGS_FROM,
OPT_LANGS_TO,
OPT_TIMING_ALL,
GLOBLA_RULE,
DEFAULT_SUBRULES_LIST,
DEFAULT_OW_RULE,
} from "../config";
import { loadOrFetchSubRules } from "./subRules";
import { getRulesWithDefault, setRules } from "./storage";
import { trySyncRules } from "./sync";
import { FIXER_ALL } from "./webfix";
import { kissLog } from "./log";
/**
* 根据href匹配规则
@@ -19,15 +22,10 @@ import { loadOrFetchSubRules } from "./subRules";
* @returns
*/
export const matchRule = async (
rules,
href,
{
injectRules = true,
subrulesList = DEFAULT_SUBRULES_LIST,
owSubrule = DEFAULT_OW_RULE,
}
{ injectRules, subrulesList, owSubrule }
) => {
rules = [...rules];
const rules = await getRulesWithDefault();
if (injectRules) {
try {
const selectedSub = subrulesList.find((item) => item.selected);
@@ -47,25 +45,64 @@ export const matchRule = async (
mixRule[key] = val;
});
const subRules = (await loadOrFetchSubRules(selectedSub.url)).map(
(item) => ({ ...item, ...mixRule })
);
let subRules = await loadOrFetchSubRules(selectedSub.url);
subRules = subRules.map((item) => ({ ...item, ...mixRule }));
rules.splice(-1, 0, ...subRules);
}
} catch (err) {
console.log("[load injectRules]", err);
kissLog(err, "load injectRules");
}
}
const rule = rules.find((r) =>
r.pattern.split(",").some((p) => isMatch(href, p.trim()))
);
const globalRule = rules.find((r) => r.pattern === GLOBAL_KEY) || GLOBLA_RULE;
const globalRule = {
...GLOBLA_RULE,
...(rules.find((r) => r.pattern === GLOBAL_KEY) || {}),
};
if (!rule) {
return globalRule;
}
rule.selector = rule.selector?.trim() || globalRule.selector;
[
"selector",
"keepSelector",
"terms",
"selectStyle",
"parentStyle",
"injectJs",
"injectCss",
"fixerSelector",
"transStartHook",
"transEndHook",
"transRemoveHook",
].forEach((key) => {
if (!rule[key]?.trim()) {
rule[key] = globalRule[key];
}
});
[
"translator",
"fromLang",
"toLang",
"transOpen",
"transOnly",
"transTiming",
"transTag",
"transTitle",
"detectRemote",
"fixerFunc",
].forEach((key) => {
if (rule[key] === undefined || rule[key] === GLOBAL_KEY) {
rule[key] = globalRule[key];
}
});
if (!rule.skipLangs || rule.skipLangs.length === 0) {
rule.skipLangs = globalRule.skipLangs;
}
if (rule.textStyle === GLOBAL_KEY) {
rule.textStyle = globalRule.textStyle;
rule.bgColor = globalRule.bgColor;
@@ -74,11 +111,6 @@ export const matchRule = async (
rule.bgColor = rule.bgColor?.trim() || globalRule.bgColor;
rule.textDiyStyle = rule.textDiyStyle?.trim() || globalRule.textDiyStyle;
}
["translator", "fromLang", "toLang", "transOpen"].forEach((key) => {
if (rule[key] === GLOBAL_KEY) {
rule[key] = globalRule[key];
}
});
return rule;
};
@@ -112,6 +144,12 @@ export const checkRules = (rules) => {
({
pattern,
selector,
keepSelector,
terms,
selectStyle,
parentStyle,
injectJs,
injectCss,
translator,
fromLang,
toLang,
@@ -119,9 +157,26 @@ export const checkRules = (rules) => {
transOpen,
bgColor,
textDiyStyle,
transOnly,
transTiming,
transTag,
transTitle,
detectRemote,
skipLangs,
fixerSelector,
fixerFunc,
transStartHook,
transEndHook,
transRemoveHook,
}) => ({
pattern: pattern.trim(),
selector: type(selector) === "string" ? selector : "",
keepSelector: type(keepSelector) === "string" ? keepSelector : "",
terms: type(terms) === "string" ? terms : "",
selectStyle: type(selectStyle) === "string" ? selectStyle : "",
parentStyle: type(parentStyle) === "string" ? parentStyle : "",
injectJs: type(injectJs) === "string" ? injectJs : "",
injectCss: type(injectCss) === "string" ? injectCss : "",
bgColor: type(bgColor) === "string" ? bgColor : "",
textDiyStyle: type(textDiyStyle) === "string" ? textDiyStyle : "",
translator: matchValue([GLOBAL_KEY, ...OPT_TRANS_ALL], translator),
@@ -129,8 +184,36 @@ export const checkRules = (rules) => {
toLang: matchValue([GLOBAL_KEY, ...toLangs], toLang),
textStyle: matchValue([GLOBAL_KEY, ...OPT_STYLE_ALL], textStyle),
transOpen: matchValue([GLOBAL_KEY, "true", "false"], transOpen),
transOnly: matchValue([GLOBAL_KEY, "true", "false"], transOnly),
transTiming: matchValue([GLOBAL_KEY, ...OPT_TIMING_ALL], transTiming),
transTag: matchValue([GLOBAL_KEY, "span", "font"], transTag),
transTitle: matchValue([GLOBAL_KEY, "true", "false"], transTitle),
detectRemote: matchValue([GLOBAL_KEY, "true", "false"], detectRemote),
skipLangs: type(skipLangs) === "array" ? skipLangs : [],
fixerSelector: type(fixerSelector) === "string" ? fixerSelector : "",
transStartHook: type(transStartHook) === "string" ? transStartHook : "",
transEndHook: type(transEndHook) === "string" ? transEndHook : "",
transRemoveHook:
type(transRemoveHook) === "string" ? transRemoveHook : "",
fixerFunc: matchValue([GLOBAL_KEY, ...FIXER_ALL], fixerFunc),
})
);
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();
};

View File

@@ -22,22 +22,22 @@ export const shortcutListener = (fn, target = document, timeout = 3000) => {
}, timeout);
if (e.code) {
allkeys.add(e.key);
curkeys.add(e.key);
allkeys.add(e.code);
curkeys.add(e.code);
fn([...curkeys], [...allkeys]);
}
};
const handleKeyup = (e) => {
curkeys.delete(e.key);
curkeys.delete(e.code);
if (curkeys.size === 0) {
fn([...curkeys], [...allkeys]);
allkeys.clear();
}
};
target.addEventListener("keydown", handleKeydown);
target.addEventListener("keyup", handleKeyup);
target.addEventListener("keydown", handleKeydown, true);
target.addEventListener("keyup", handleKeyup, true);
return () => {
if (timer) {
clearTimeout(timer);
@@ -65,3 +65,48 @@ export const shortcutRegister = (targetKeys = [], fn, target = document) => {
}
}, 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,11 +1,12 @@
import {
STOKEY_SETTING,
STOKEY_RULES,
STOKEY_WORDS,
STOKEY_FAB,
STOKEY_SYNC,
STOKEY_MSAUTH,
STOKEY_BDAUTH,
STOKEY_RULESCACHE_PREFIX,
STOKEY_WEBFIXCACHE_PREFIX,
DEFAULT_SETTING,
DEFAULT_RULES,
DEFAULT_SYNC,
@@ -13,6 +14,7 @@ import {
} from "../config";
import { isExt, isGm } from "./client";
import { browser } from "./browser";
import { kissLog } from "./log";
async function set(key, val) {
if (isExt) {
@@ -83,8 +85,10 @@ export const storage = {
* 设置信息
*/
export const getSetting = () => getObj(STOKEY_SETTING);
export const getSettingWithDefault = async () =>
(await getSetting()) || DEFAULT_SETTING;
export const getSettingWithDefault = async () => ({
...DEFAULT_SETTING,
...((await getSetting()) || {}),
});
export const setSetting = (val) => setObj(STOKEY_SETTING, val);
export const updateSetting = (obj) => putObj(STOKEY_SETTING, obj);
@@ -96,6 +100,13 @@ export const getRulesWithDefault = async () =>
(await getRules()) || DEFAULT_RULES;
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);
/**
* 订阅规则
*/
@@ -105,20 +116,13 @@ export const delSubRules = (url) => del(STOKEY_RULESCACHE_PREFIX + url);
export const setSubRules = (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位置
*/
export const getFab = () => getObj(STOKEY_FAB);
export const getFabWithDefault = async () => (await getFab()) || {};
export const setFab = (obj) => setObj(STOKEY_FAB, obj);
export const updateFab = (obj) => putObj(STOKEY_FAB, obj);
/**
* 数据同步
@@ -133,6 +137,12 @@ export const updateSync = (obj) => putObj(STOKEY_SYNC, obj);
export const getMsauth = () => getObj(STOKEY_MSAUTH);
export const setMsauth = (val) => setObj(STOKEY_MSAUTH, val);
/**
* baidu auth
*/
export const getBdauth = () => getObj(STOKEY_BDAUTH);
export const setBdauth = (val) => setObj(STOKEY_BDAUTH, val);
/**
* 存入默认数据
*/
@@ -146,6 +156,6 @@ export const tryInitDefaultData = async () => {
BUILTIN_RULES
);
} catch (err) {
console.log("[init default]", err);
kissLog(err, "init default");
}
};

View File

@@ -4,19 +4,29 @@ import {
updateSync,
setSubRules,
getSubRules,
updateSetting,
} from "./storage";
import { apiFetch } from "../apis";
import { checkRules } from "./rules";
import { isAllchar } from "./utils";
import { kissLog } from "./log";
/**
* 更新缓存同步时间
* @param {*} url
*/
const updateSyncDataCache = async (url) => {
const { dataCaches = {} } = await getSyncWithDefault();
dataCaches[url] = Date.now();
await updateSync({ dataCaches });
};
/**
* 同步订阅规则
* @param {*} url
* @returns
*/
export const syncSubRules = async (url, isBg = false) => {
const res = await apiFetch(url, isBg);
export const syncSubRules = async (url) => {
const res = await apiFetch(url);
const rules = checkRules(res).filter(
({ pattern }) => !isAllchar(pattern, GLOBAL_KEY)
);
@@ -31,12 +41,13 @@ export const syncSubRules = async (url, isBg = false) => {
* @param {*} url
* @returns
*/
export const syncAllSubRules = async (subrulesList, isBg = false) => {
for (let subrules of subrulesList) {
export const syncAllSubRules = async (subrulesList) => {
for (const subrules of subrulesList) {
try {
await syncSubRules(subrules.url, isBg);
await syncSubRules(subrules.url);
await updateSyncDataCache(subrules.url);
} catch (err) {
console.log(`[sync subrule error]: ${subrules.url}`, err);
kissLog(err, `sync subrule error: ${subrules.url}`);
}
}
};
@@ -46,21 +57,18 @@ export const syncAllSubRules = async (subrulesList, isBg = false) => {
* @param {*} url
* @returns
*/
export const trySyncAllSubRules = async ({ subrulesList }, isBg = false) => {
export const trySyncAllSubRules = async ({ subrulesList }) => {
try {
const { subRulesSyncAt } = await getSyncWithDefault();
const now = Date.now();
const interval = 24 * 60 * 60 * 1000; // 间隔一天
if (now - subRulesSyncAt > interval) {
await syncAllSubRules(subrulesList, isBg);
// 同步订阅规则
await syncAllSubRules(subrulesList);
await updateSync({ subRulesSyncAt: now });
}
subrulesList.forEach((item) => {
item.syncAt = now;
});
await updateSetting({ subrulesList });
} catch (err) {
console.log("[try sync all subrules]", err);
kissLog(err, "try sync all subrules");
}
};
@@ -70,9 +78,10 @@ export const trySyncAllSubRules = async ({ subrulesList }, isBg = false) => {
* @returns
*/
export const loadOrFetchSubRules = async (url) => {
const rules = await getSubRules(url);
if (rules?.length) {
return rules;
let rules = await getSubRules(url);
if (!rules || rules.length === 0) {
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,58 +1,123 @@
import {
APP_LCNAME,
KV_SETTING_KEY,
KV_RULES_KEY,
KV_WORDS_KEY,
KV_RULES_SHARE_KEY,
KV_SALT_SHARE,
OPT_SYNCTYPE_WEBDAV,
} from "../config";
import {
getSyncWithDefault,
updateSync,
getSettingWithDefault,
getRulesWithDefault,
getWordsWithDefault,
setSetting,
setRules,
setWords,
} from "./storage";
import { apiSyncData } from "../apis";
import { sha256 } from "./utils";
import { sha256, removeEndchar } from "./utils";
import { createClient, getPatcher } from "webdav";
import { fetchPatcher } from "./fetch";
import { kissLog } from "./log";
getPatcher().patch("request", (opts) => {
return fetchPatcher(opts.url, {
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
*/
const syncSetting = async (isBg = false) => {
const { syncUrl, syncKey, settingUpdateAt = 0 } = await getSyncWithDefault();
if (!syncUrl || !syncKey) {
return;
}
const setting = await getSettingWithDefault();
const res = await apiSyncData(
syncUrl,
syncKey,
{
key: KV_SETTING_KEY,
value: setting,
updateAt: settingUpdateAt,
},
isBg
);
if (res.updateAt > settingUpdateAt) {
const syncSetting = async () => {
const res = await syncData(KV_SETTING_KEY, getSettingWithDefault);
if (res?.isNew) {
await setSetting(res.value);
}
await updateSync({
settingUpdateAt: res.updateAt,
settingSyncAt: Date.now(),
});
return res.value;
};
export const trySyncSetting = async (isBg = false) => {
export const trySyncSetting = async () => {
try {
return await syncSetting(isBg);
await syncSetting();
} catch (err) {
console.log("[sync setting]", err);
kissLog(err, "sync setting");
}
};
@@ -60,40 +125,37 @@ export const trySyncSetting = async (isBg = false) => {
* 同步规则
* @returns
*/
const syncRules = async (isBg = false) => {
const { syncUrl, syncKey, rulesUpdateAt } = await getSyncWithDefault();
if (!syncUrl || !syncKey) {
return;
}
const rules = await getRulesWithDefault();
const res = await apiSyncData(
syncUrl,
syncKey,
{
key: KV_RULES_KEY,
value: rules,
updateAt: rulesUpdateAt,
},
isBg
);
if (res.updateAt > rulesUpdateAt) {
const syncRules = async () => {
const res = await syncData(KV_RULES_KEY, getRulesWithDefault);
if (res?.isNew) {
await setRules(res.value);
}
await updateSync({
rulesUpdateAt: res.updateAt,
rulesSyncAt: Date.now(),
});
return res.value;
};
export const trySyncRules = async (isBg = false) => {
export const trySyncRules = async () => {
try {
return await syncRules(isBg);
await syncRules();
} catch (err) {
console.log("[sync user rules]", err);
kissLog(err, "sync user rules");
}
};
/**
* 同步词汇
* @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) {
kissLog(err, "sync fav words");
}
};
@@ -103,13 +165,18 @@ export const trySyncRules = async (isBg = false) => {
* @returns
*/
export const syncShareRules = async ({ rules, syncUrl, syncKey }) => {
await apiSyncData(syncUrl, syncKey, {
const data = {
key: KV_RULES_SHARE_KEY,
value: rules,
value: JSON.stringify(rules, null, 2),
updateAt: Date.now(),
});
};
const args = {
syncUrl,
syncKey,
};
await syncByWorker(data, args);
const psk = await sha256(syncKey, KV_SALT_SHARE);
const shareUrl = `${syncUrl}?psk=${psk}`;
const shareUrl = `${syncUrl}/rules?psk=${psk}`;
return shareUrl;
};
@@ -117,10 +184,14 @@ export const syncShareRules = async ({ rules, syncUrl, syncKey }) => {
* 同步个人设置和规则
* @returns
*/
export const syncSettingAndRules = async (isBg = false) => {
return [await syncSetting(isBg), await syncRules(isBg)];
export const syncSettingAndRules = async () => {
await syncSetting();
await syncRules();
await syncWords();
};
export const trySyncSettingAndRules = async (isBg = false) => {
return [await trySyncSetting(isBg), await trySyncRules(isBg)];
export const trySyncSettingAndRules = async () => {
await trySyncSetting();
await trySyncRules();
await trySyncWords();
};

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

@@ -0,0 +1,12 @@
export function touchTapListener(fn, touchsLength) {
const handleTouchend = (e) => {
if (e.touches.length === touchsLength) {
fn();
}
};
document.addEventListener("touchstart", handleTouchend);
return () => {
document.removeEventListener("touchstart", handleTouchend);
};
}

View File

@@ -4,15 +4,28 @@ import {
TRANS_MIN_LENGTH,
TRANS_MAX_LENGTH,
MSG_TRANS_CURRULE,
MSG_INJECT_JS,
MSG_INJECT_CSS,
OPT_STYLE_DASHLINE,
OPT_STYLE_FUZZY,
SHADOW_KEY,
OPT_MOUSEKEY_DISABLE,
OPT_MOUSEKEY_MOUSEOVER,
OPT_TIMING_PAGESCROLL,
OPT_TIMING_PAGEOPEN,
OPT_TIMING_MOUSEOVER,
DEFAULT_TRANS_APIS,
DEFAULT_FETCH_LIMIT,
DEFAULT_FETCH_INTERVAL,
} from "../config";
import Content from "../views/Content";
import { updateFetchPool, clearFetchPool } from "./fetch";
import { debounce, genEventName } from "./utils";
import { runFixer } from "./webfix";
import { apiTranslate } from "../apis";
import { sendBgMsg } from "./msg";
import { isExt } from "./client";
import { injectInlineJs, injectInternalCss } from "./injector";
import { kissLog } from "./log";
import interpreter from "./interpreter";
/**
* 翻译类
@@ -39,14 +52,18 @@ export class Translator {
"iframe",
];
_eventName = genEventName();
_mouseoverNode = null;
_keepSelector = [null, null];
_terms = [];
_docTitle = "";
// 显示
_interseObserver = new IntersectionObserver(
(intersections) => {
intersections.forEach((intersection) => {
if (intersection.isIntersecting) {
this._render(intersection.target);
this._interseObserver.unobserve(intersection.target);
(entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
observer.unobserve(entry.target);
this._render(entry.target);
}
});
},
@@ -90,14 +107,34 @@ export class Translator {
};
};
constructor(rule, setting) {
const { fetchInterval, fetchLimit } = setting;
_updatePool(translator) {
if (!translator) {
return;
}
const {
fetchInterval = DEFAULT_FETCH_INTERVAL,
fetchLimit = DEFAULT_FETCH_LIMIT,
} = this._setting.transApis[translator] || {};
updateFetchPool(fetchInterval, fetchLimit);
}
constructor(rule, setting) {
this._overrideAttachShadow();
this._setting = setting;
this._rule = rule;
this._keepSelector = (rule.keepSelector || "")
.split(SHADOW_KEY)
.map((item) => item.trim());
this._terms = (rule.terms || "")
.split(/\n|;/)
.map((item) => item.split(",").map((item) => item.trim()))
.filter(([term]) => Boolean(term));
this._updatePool(rule.translator);
if (rule.transOpen === "true") {
this._register();
}
@@ -134,6 +171,7 @@ export class Translator {
updateRule = (obj) => {
this.rule = { ...this.rule, ...obj };
this._updatePool(obj.translator);
};
toggle = () => {
@@ -154,11 +192,25 @@ export class Translator {
this.rule = { ...this.rule, textStyle };
};
translateText = async (text) => {
const { translator, fromLang, toLang } = this._rule;
const apiSetting =
this._setting.transApis?.[translator] || DEFAULT_TRANS_APIS[translator];
const [trText] = await apiTranslate({
text,
translator,
fromLang,
toLang,
apiSetting,
});
return trText;
};
_querySelectorAll = (selector, node) => {
try {
return Array.from(node.querySelectorAll(selector));
} catch (err) {
console.log(`[querySelectorAll err]: ${selector}`);
kissLog(selector, "querySelectorAll err");
}
return [];
};
@@ -169,6 +221,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) => {
// const childRoots = Array.from(rootNode.querySelectorAll("*"))
// .map((item) => item.shadowRoot)
@@ -191,14 +259,15 @@ export class Translator {
const outNodes = this._querySelectorAll(outSelector, rootNode);
outNodes.forEach((outNode) => {
if (outNode.shadowRoot) {
this._rootNodes.add(outNode.shadowRoot);
this._queryFilter(inSelector, outNode.shadowRoot).forEach(
(item) => {
if (!this._tranNodes.has(item)) {
this._tranNodes.set(item, "");
}
}
);
// this._rootNodes.add(outNode.shadowRoot);
// this._queryFilter(inSelector, outNode.shadowRoot).forEach(
// (item) => {
// if (!this._tranNodes.has(item)) {
// this._tranNodes.set(item, "");
// }
// }
// );
this._queryShadowNodes(inSelector, outNode.shadowRoot);
}
});
}
@@ -213,10 +282,26 @@ export class Translator {
};
_register = () => {
if (this._rule.fromLang === this._rule.toLang) {
const { fromLang, toLang, injectJs, injectCss, fixerSelector, fixerFunc } =
this._rule;
if (fromLang === toLang) {
return;
}
// webfix
if (fixerSelector && fixerFunc !== "-") {
runFixer(fixerSelector, fixerFunc);
}
// 注入用户JS/CSS
if (isExt) {
injectJs && sendBgMsg(MSG_INJECT_JS, injectJs);
injectCss && sendBgMsg(MSG_INJECT_CSS, injectCss);
} else {
injectJs && injectInlineJs(injectJs);
injectCss && injectInternalCss(injectCss);
}
// 搜索节点
this._queryNodes();
@@ -229,51 +314,131 @@ export class Translator {
});
});
this._tranNodes.forEach((_, node) => {
if (
!this._setting.mouseKey ||
this._setting.mouseKey === OPT_MOUSEKEY_DISABLE
) {
// 监听节点显示
if (
!this._rule.transTiming ||
this._rule.transTiming === OPT_TIMING_PAGESCROLL
) {
// 监听节点显示
this._tranNodes.forEach((_, node) => {
this._interseObserver.observe(node);
} else {
// 监听鼠标悬停
node.addEventListener("mouseover", this._handleMouseover);
}
});
});
} else if (this._rule.transTiming === OPT_TIMING_PAGEOPEN) {
// 全文直接翻译
this._tranNodes.forEach((_, node) => {
this._render(node);
});
} else {
// 监听鼠标悬停
window.addEventListener("keydown", this._handleKeydown);
this._tranNodes.forEach((_, node) => {
node.addEventListener("mouseenter", this._handleMouseover);
node.addEventListener("mouseleave", this._handleMouseout);
});
}
// 翻译页面标题
if (this._rule.transTitle === "true" && !this._docTitle) {
const title = document.title;
this._docTitle = title;
this.translateText(title).then((trText) => {
document.title = `${trText} | ${title}`;
});
}
};
_handleMouseover = (e) => {
const key = this._setting.mouseKey.slice(3);
if (this._setting.mouseKey === OPT_MOUSEKEY_MOUSEOVER || e[key]) {
e.target.removeEventListener("mouseover", this._handleMouseover);
// console.log("mouseenter", e);
if (!this._tranNodes.has(e.target)) {
return;
}
const key = this._rule.transTiming.slice(3);
if (this._rule.transTiming === OPT_TIMING_MOUSEOVER || e[key]) {
e.target.removeEventListener("mouseenter", this._handleMouseover);
e.target.removeEventListener("mouseleave", this._handleMouseout);
this._render(e.target);
} else {
this._mouseoverNode = e.target;
}
};
_handleMouseout = (e) => {
// console.log("mouseleave", e);
if (!this._tranNodes.has(e.target)) {
return;
}
this._mouseoverNode = null;
};
_handleKeydown = (e) => {
// console.log("keydown", e);
const key = this._rule.transTiming.slice(3);
if (e[key] && this._mouseoverNode) {
this._mouseoverNode.removeEventListener(
"mouseenter",
this._handleMouseover
);
this._mouseoverNode.removeEventListener(
"mouseleave",
this._handleMouseout
);
const node = this._mouseoverNode;
this._render(node);
this._mouseoverNode = null;
}
};
_unRegister = () => {
// 恢复页面标题
if (this._docTitle) {
document.title = this._docTitle;
this._docTitle = "";
}
// 解除节点变化监听
this._mutaObserver.disconnect();
// 解除节点显示监听
// this._interseObserver.disconnect();
this._tranNodes.forEach((_, node) => {
// 移除键盘监听
window.removeEventListener("keydown", this._handleKeydown);
const { transRemoveHook } = this._rule;
this._tranNodes.forEach((innerHTML, node) => {
if (
!this._setting.mouseKey ||
this._setting.mouseKey === OPT_MOUSEKEY_DISABLE
!this._rule.transTiming ||
this._rule.transTiming === OPT_TIMING_PAGESCROLL
) {
// 解除节点显示监听
this._interseObserver.unobserve(node);
} else {
} else if (this._rule.transTiming !== OPT_TIMING_PAGEOPEN) {
// 移除鼠标悬停监听
node.removeEventListener("mouseover", this._handleMouseover);
// node.style.pointerEvents = "none";
node.removeEventListener("mouseenter", this._handleMouseover);
node.removeEventListener("mouseleave", this._handleMouseout);
}
// 移除已插入元素
node.querySelector(APP_LCNAME)?.remove();
// 移除/恢复元素
if (innerHTML) {
if (this._rule.transOnly === "true") {
node.innerHTML = innerHTML;
} else {
node.querySelector(APP_LCNAME)?.remove();
}
// 钩子函数
if (transRemoveHook?.trim()) {
interpreter.run(`exports.transRemoveHook = ${transRemoveHook}`);
interpreter.exports.transRemoveHook(node);
}
}
});
// 移除用户JS/CSS
this._removeInjector();
// 清空节点集合
this._rootNodes.clear();
this._tranNodes.clear();
@@ -282,17 +447,36 @@ export class Translator {
clearFetchPool();
};
_removeInjector = () => {
document
.querySelectorAll(`[data-source^="KISS-Calendar"]`)
?.forEach((el) => el.remove());
};
_reTranslate = debounce(() => {
if (this._rule.transOpen === "true") {
window.removeEventListener("keydown", this._handleKeydown);
this._mutaObserver.disconnect();
this._interseObserver.disconnect();
this._removeInjector();
this._register();
}
}, 500);
}, this._setting.transInterval);
_invalidLength = (q) =>
!q ||
q.length < (this._setting.minLength ?? TRANS_MIN_LENGTH) ||
q.length > (this._setting.maxLength ?? TRANS_MAX_LENGTH);
_render = (el) => {
let traEl = el.querySelector(APP_LCNAME);
// 已翻译
if (traEl) {
if (this._rule.transOnly === "true") {
return;
}
const preText = this._tranNodes.get(el);
const curText = el.innerText.trim();
// const traText = traEl.innerText.trim();
@@ -307,31 +491,86 @@ export class Translator {
traEl.remove();
}
const q = el.innerText.trim();
this._tranNodes.set(el, q);
let q = el.innerText.trim();
if (this._rule.transOnly === "true") {
this._tranNodes.set(el, el.innerHTML);
} else {
this._tranNodes.set(el, q);
}
const keeps = [];
// 翻译开始钩子函数
const { transStartHook } = this._rule;
if (transStartHook?.trim()) {
interpreter.run(`exports.transStartHook = ${transStartHook}`);
interpreter.exports.transStartHook(el, q);
}
// 保留元素
const [matchSelector, subSelector] = this._keepSelector;
if (matchSelector || subSelector) {
let text = "";
el.childNodes.forEach((child) => {
if (
child.nodeType === 1 &&
((matchSelector && child.matches(matchSelector)) ||
(subSelector && child.querySelector(subSelector)))
) {
if (child.nodeName === "IMG") {
child.style.cssText += `width: ${child.width}px;`;
child.style.cssText += `height: ${child.height}px;`;
}
text += `[${keeps.length}]`;
keeps.push(child.outerHTML);
} else {
text += child.textContent;
}
});
if (keeps.length > 0) {
// textContent会保留些无用的换行符严重影响翻译质量
if (q.includes("\n")) {
q = text;
} else {
q = text.replaceAll("\n", " ");
}
}
}
// 太长或太短
if (
!q ||
q.length < (this._setting.minLength ?? TRANS_MIN_LENGTH) ||
q.length > (this._setting.maxLength ?? TRANS_MAX_LENGTH)
) {
if (this._invalidLength(q.replace(/\[(\d+)\]/g, "").trim())) {
return;
}
// console.log("---> ", q);
traEl = document.createElement(APP_LCNAME);
traEl.style.visibility = "visible";
el.appendChild(traEl);
el.style.cssText +=
"-webkit-line-clamp: unset; max-height: none; height: auto;";
if (el.parentElement) {
el.parentElement.style.cssText +=
"-webkit-line-clamp: unset; max-height: none; height: auto;";
// 专业术语
if (this._terms.length > 0) {
for (const term of this._terms) {
const re = new RegExp(term[0], "g");
q = q.replace(re, (t) => {
const text = `[${keeps.length}]`;
keeps.push(`<i class="kiss-trem">${term[1] || t}</i>`);
return text;
});
}
}
// 附加样式
const { selectStyle, parentStyle } = this._rule;
el.style.cssText += selectStyle;
if (el.parentElement) {
el.parentElement.style.cssText += parentStyle;
}
// 插入译文节点
traEl = document.createElement(APP_LCNAME);
traEl.style.visibility = "visible";
// if (this._rule.transOnly === "true") {
// el.innerHTML = "";
// }
el.appendChild(traEl);
// 渲染译文节点
const root = createRoot(traEl);
root.render(<Content q={q} translator={this} />);
root.render(<Content q={q} keeps={keeps} translator={this} $el={el} />);
};
}

View File

@@ -15,6 +15,16 @@ export const limitNumber = (num, min = 0, max = 100) => {
return number;
};
export const limitFloat = (num, min = 0, max = 100) => {
const number = parseFloat(num);
if (Number.isNaN(number) || number < min) {
return min;
} else if (number > max) {
return max;
}
return number;
};
/**
* 匹配是否为数组中的值
* @param {*} arr
@@ -179,3 +189,64 @@ 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) => {
switch (sign) {
case "//":
return str.match(/\/\/([\w-]+)\s+([^]+)/);
case "\\":
return str.match(/\\([\w-]+)\s+([^]+)/);
case "\\\\":
return str.match(/\\\\([\w-]+)\s+([^]+)/);
case ">":
return str.match(/>([\w-]+)\s+([^]+)/);
case ">>":
return str.match(/>>([\w-]+)\s+([^]+)/);
default:
}
return str.match(/\/([\w-]+)\s+([^]+)/);
};
/**
* 判断是否英文单词
* @param {*} str
* @returns
*/
export const isValidWord = (str) => {
const regex = /^[a-zA-Z-]+$/;
return regex.test(str);
};
/**
* blob转为base64
* @param {*} blob
* @returns
*/
export const blobToBase64 = (blob) => {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.readAsDataURL(blob);
});
};

View File

@@ -1,38 +1,24 @@
import { isMatch } from "./utils";
import { getWebfix, setWebfix } from "./storage";
import { apiFetch } from "../apis";
/**
* 修复程序类型
*/
const WEBFIX_BR = "br";
export const FIXER_NONE = "-";
export const FIXER_BR = "br";
export const FIXER_BN = "bn";
export const FIXER_BR_DIV = "brToDiv";
export const FIXER_BN_DIV = "bnToDiv";
/**
* 需要修复的站点列表
* - pattern 匹配网址
* - selector 需要修复的选择器
* - rootSlector 需要监听的选择器,可留空
* - fixer 修复函数,可针对不同网址,选用不同修复函数
*/
const DEFAULT_SITES = [
{
pattern: "www.phoronix.com",
selector: ".content",
rootSlector: "",
fixer: WEBFIX_BR,
},
{
pattern: "t.me/s/*",
selector: ".tgme_widget_message_text",
rootSlector: ".tgme_channel_history",
fixer: WEBFIX_BR,
},
export const FIXER_ALL = [
FIXER_NONE,
FIXER_BR,
FIXER_BN,
FIXER_BR_DIV,
FIXER_BN_DIV,
];
/**
* 修复过的标记
*/
const fixedSign = "kissfixed";
const fixedSign = "kiss-fixed";
/**
* 采用 `br` 换行网站的修复函数
@@ -40,14 +26,14 @@ const fixedSign = "kissfixed";
* @param {*} node
* @returns
*/
function brFixer(node) {
function brFixer(node, tag = "p") {
if (node.hasAttribute(fixedSign)) {
return;
}
node.setAttribute(fixedSign, "true");
var gapTags = ["BR", "WBR"];
var newlineTags = [
const gapTags = ["BR", "WBR"];
const newlineTags = [
"DIV",
"UL",
"OL",
@@ -62,115 +48,111 @@ function brFixer(node) {
"HR",
"PRE",
"TABLE",
"BLOCKQUOTE",
];
var html = "";
let html = "";
node.childNodes.forEach(function (child, index) {
if (index === 0) {
html += "<p>";
html += `<${tag} class="kiss-p">`;
}
if (gapTags.indexOf(child.nodeName) !== -1) {
html += "</p><p>";
html += `</${tag}><${tag} class="kiss-p">`;
} else if (newlineTags.indexOf(child.nodeName) !== -1) {
html += "</p>" + child.outerHTML + "<p>";
html += `</${tag}>${child.outerHTML}<${tag} class="kiss-p">`;
} else if (child.outerHTML) {
html += child.outerHTML;
} else if (child.nodeValue) {
html += child.nodeValue;
} else if (child.textContent) {
html += child.textContent;
}
if (index === node.childNodes.length - 1) {
html += "</p>";
html += `</${tag}>`;
}
});
node.innerHTML = html;
}
function brDivFixer(node) {
return brFixer(node, "div");
}
/**
* 目标是将 `\n` 替换成 `p`
* @param {*} node
* @returns
*/
function bnFixer(node, tag = "p") {
if (node.hasAttribute(fixedSign)) {
return;
}
node.setAttribute(fixedSign, "true");
node.innerHTML = node.innerHTML
.split("\n")
.map((item) => `<${tag} class="kiss-p">${item || "&nbsp;"}</${tag}>`)
.join("");
}
function bnDivFixer(node) {
return bnFixer(node, "div");
}
/**
* 查找、监听节点,并执行修复函数
* @param {*} selector
* @param {*} fixer
* @param {*} rootSelector
*/
function run(selector, fixer, rootSelector) {
const mutaObserver = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
mutation.addedNodes.forEach(function (addNode) {
if (addNode && addNode.querySelectorAll) {
addNode.querySelectorAll(selector).forEach(function (node) {
fixer(node);
});
}
});
});
});
let rootNodes = [document];
if (rootSelector) {
rootNodes = document.querySelectorAll(rootSelector);
}
rootNodes.forEach(function (rootNode) {
rootNode.querySelectorAll(selector).forEach(function (node) {
fixer(node);
});
mutaObserver.observe(rootNode, {
childList: true,
subtree: true,
});
});
}
/**
* 修复程序映射
*/
const fixerMap = {
[WEBFIX_BR]: brFixer,
[FIXER_BR]: brFixer,
[FIXER_BN]: bnFixer,
[FIXER_BR_DIV]: brDivFixer,
[FIXER_BN_DIV]: bnDivFixer,
};
/**
* 查找、监听节点,并执行修复函数
* @param {*} selector
* @param {*} fixer
* @param {*} rootSlector
* 执行fixer
* @param {*} param0
*/
function run(selector, fixer, rootSlector) {
var mutaObserver = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
mutation.addedNodes.forEach(function (addNode) {
addNode.querySelectorAll(selector).forEach(fixer);
});
});
});
var rootNodes = [document];
if (rootSlector) {
rootNodes = document.querySelectorAll(rootSlector);
}
rootNodes.forEach(function (rootNode) {
rootNode.querySelectorAll(selector).forEach(fixer);
mutaObserver.observe(rootNode, {
childList: 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) => {
export function runFixer(selector, fixer = "-", rootSelector) {
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 webfix(href, { injectWebfix }) {
try {
if (!injectWebfix) {
return;
}
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.rootSlector);
}
break;
}
if (Object.keys(fixerMap).includes(fixer)) {
run(selector, fixerMap[fixer], rootSelector);
}
} catch (err) {
console.error(`[kiss-webfix]: ${err.message}`);
console.error(`[kiss-webfix run]: ${err.message}`);
}
}

View File

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

View File

@@ -1,110 +1,3 @@
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 {
getSettingWithDefault,
getRulesWithDefault,
getFabWithDefault,
} from "./libs/storage";
import { Translator } from "./libs/translator";
import { trySyncAllSubRules } from "./libs/subRules";
import { MSG_TRANS_TOGGLE, MSG_TRANS_PUTRULE } from "./config";
import { isIframe } from "./libs/iframe";
import { handlePing, injectScript } from "./libs/gm";
import { matchRule } from "./libs/rules";
import { genEventName } from "./libs/utils";
import { webfix } from "./libs/webfix";
import { run } from "./common";
/**
* 入口函数
*/
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_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);
}
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);
webfix(href, 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>
);
// 同步订阅规则
trySyncAllSubRules(setting);
};
(async () => {
try {
await init();
} catch (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);
}
})();
run(true);

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

View File

@@ -1,4 +1,3 @@
import Paper from "@mui/material/Paper";
import Fab from "@mui/material/Fab";
import TranslateIcon from "@mui/icons-material/Translate";
import ThemeProvider from "../../hooks/Theme";
@@ -9,14 +8,21 @@ import Popup from "../Popup";
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";
import { kissLog } from "../../libs/log";
import { getI18n } from "../../hooks/I18n";
export default function Action({ translator, fab }) {
const fabWidth = 40;
@@ -51,15 +57,21 @@ export default function Action({ translator, fab }) {
}, []);
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], () => {
@@ -78,58 +90,58 @@ export default function Action({ translator, fab }) {
}, [translator]);
useEffect(() => {
if (!isGm) {
return;
}
// 注册菜单
const menuCommandIds = [];
if (isGm) {
try {
try {
const menuCommandIds = [];
const { contextMenuType, uiLang } = translator.setting;
contextMenuType !== 0 &&
menuCommandIds.push(
GM.registerMenuCommand(
"Toggle Translate (Alt+q)",
getI18n(uiLang, "translate_switch"),
(event) => {
translator.toggle();
sendIframeMsg(MSG_TRANS_TOGGLE);
setShowPopup(false);
},
"Q"
),
GM.registerMenuCommand(
"Toggle Style (Alt+c)",
getI18n(uiLang, "toggle_style"),
(event) => {
translator.toggleStyle();
sendIframeMsg(MSG_TRANS_TOGGLE_STYLE);
setShowPopup(false);
},
"C"
),
GM.registerMenuCommand(
"Open Menu (Alt+k)",
getI18n(uiLang, "open_menu"),
(event) => {
setShowPopup((pre) => !pre);
},
"K"
),
GM.registerMenuCommand(
"Open Setting (Alt+o)",
getI18n(uiLang, "open_setting"),
(event) => {
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank");
},
"O"
)
);
} catch (err) {
console.log("[registerMenuCommand]", err);
}
}
return () => {
if (isGm) {
try {
menuCommandIds.forEach((id) => {
GM.unregisterMenuCommand(id);
});
} catch (err) {
//
}
}
};
return () => {
menuCommandIds.forEach((id) => {
GM.unregisterMenuCommand(id);
});
};
} catch (err) {
kissLog(err, "registerMenuCommand");
}
}, [translator]);
useEffect(() => {
@@ -165,7 +177,7 @@ export default function Action({ translator, fab }) {
windowSize,
width: fabWidth,
height: fabWidth,
left: fab.x ?? 0,
left: fab.x ?? -fabWidth,
top: fab.y ?? windowSize.h / 2,
};
@@ -178,23 +190,23 @@ export default function Action({ translator, fab }) {
show={showPopup}
onStart={handleStart}
onMove={handleMove}
usePaper
handler={
<Paper style={{ cursor: "move" }} elevation={3}>
<Box style={{ cursor: "move" }}>
<Header setShowPopup={setShowPopup} />
</Paper>
<Divider />
</Box>
}
>
<Paper>
{showPopup && (
<Popup setShowPopup={setShowPopup} translator={translator} />
)}
</Paper>
{showPopup && (
<Popup setShowPopup={setShowPopup} translator={translator} />
)}
</Draggable>
<Draggable
key="fab"
snapEdge
{...fabProps}
show={translator.setting.hideFab ? false : !showPopup}
show={fab.isHide ? false : !showPopup}
onStart={handleStart}
onMove={handleMove}
handler={
@@ -207,7 +219,12 @@ export default function Action({ translator, fab }) {
}
}}
>
<TranslateIcon />
<TranslateIcon
sx={{
width: 24,
height: 24,
}}
/>
</Fab>
}
/>

View File

@@ -1,44 +1,14 @@
import { DEFAULT_COLOR } from "../../config";
import { loadingSvg } from "../../libs/svg";
export default function LoadingIcon() {
return (
<svg
viewBox="0 0 100 100"
<span
style={{
maxWidth: "1.2em",
maxHeight: "1.2em",
display: "inline-block",
width: "1.2em",
height: "1em",
}}
>
<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>
dangerouslySetInnerHTML={{ __html: loadingSvg }}
/>
);
}

View File

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

View File

@@ -1,13 +1,29 @@
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 LoadingButton from "@mui/lab/LoadingButton";
import {
OPT_TRANS_ALL,
OPT_TRANS_MICROSOFT,
OPT_TRANS_DEEPL,
OPT_TRANS_DEEPLX,
OPT_TRANS_DEEPLFREE,
OPT_TRANS_BAIDU,
OPT_TRANS_TENCENT,
OPT_TRANS_OPENAI,
OPT_TRANS_OPENAI_2,
OPT_TRANS_OPENAI_3,
OPT_TRANS_GEMINI,
OPT_TRANS_CLOUDFLAREAI,
OPT_TRANS_OLLAMA,
OPT_TRANS_OLLAMA_2,
OPT_TRANS_OLLAMA_3,
OPT_TRANS_CUSTOMIZE,
OPT_TRANS_NIUTRANS,
URL_KISS_PROXY,
URL_NIUTRANS_REG,
DEFAULT_FETCH_LIMIT,
DEFAULT_FETCH_INTERVAL,
} from "../../config";
import { useState } from "react";
import { useI18n } from "../../hooks/I18n";
@@ -22,6 +38,7 @@ import { useApi } from "../../hooks/Api";
import { apiTranslate } from "../../apis";
import Box from "@mui/material/Box";
import Link from "@mui/material/Link";
import { limitNumber, limitFloat } from "../../libs/utils";
function TestButton({ translator, api }) {
const i18n = useI18n();
@@ -35,45 +52,138 @@ function TestButton({ translator, api }) {
text: "hello world",
fromLang: "en",
toLang: "zh-CN",
apiSetting: { ...api, useCache: false },
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}`);
// alert.error(`${i18n("test_failed")}: ${err.message}`);
let msg = err.message;
try {
msg = JSON.stringify(JSON.parse(err.message), null, 2);
} catch (err) {
// skip
}
alert.error(
<>
<div>{i18n("test_failed")}</div>
{msg === err.message ? (
<div
style={{
maxWidth: 400,
}}
>
{msg}
</div>
) : (
<pre
style={{
maxWidth: 400,
overflow: "auto",
}}
>
{msg}
</pre>
)}
</>
);
} finally {
setLoading(false);
}
};
if (loading) {
return <CircularProgress size={16} />;
}
return (
<Button size="small" variant="contained" onClick={handleApiTest}>
<LoadingButton
size="small"
variant="contained"
onClick={handleApiTest}
loading={loading}
>
{i18n("click_test")}
</Button>
</LoadingButton>
);
}
function ApiFields({ translator }) {
const i18n = useI18n();
const { api, updateApi, resetApi } = useApi(translator);
const { url = "", key = "", model = "", prompt = "" } = api;
const {
url = "",
key = "",
model = "",
prompt = "",
fetchLimit = DEFAULT_FETCH_LIMIT,
fetchInterval = DEFAULT_FETCH_INTERVAL,
dictNo = "",
memoryNo = "",
reqHook = "",
resHook = "",
temperature = 0,
maxTokens = 256,
} = api;
const handleChange = (e) => {
const { name, value } = e.target;
let { name, value } = e.target;
switch (name) {
case "fetchLimit":
value = limitNumber(value, 1, 100);
break;
case "fetchInterval":
value = limitNumber(value, 0, 5000);
break;
case "temperature":
value = limitFloat(value, 0, 2);
break;
case "maxTokens":
value = limitNumber(value, 0, 2 ** 15);
break;
default:
}
updateApi({
[name]: value,
});
};
const builtinTranslators = [
OPT_TRANS_MICROSOFT,
OPT_TRANS_DEEPLFREE,
OPT_TRANS_BAIDU,
OPT_TRANS_TENCENT,
];
const mulkeysTranslators = [
OPT_TRANS_DEEPL,
OPT_TRANS_OPENAI,
OPT_TRANS_OPENAI_2,
OPT_TRANS_OPENAI_3,
OPT_TRANS_GEMINI,
OPT_TRANS_CLOUDFLAREAI,
OPT_TRANS_OLLAMA,
OPT_TRANS_OLLAMA_2,
OPT_TRANS_OLLAMA_3,
OPT_TRANS_NIUTRANS,
];
const keyHelper =
translator === OPT_TRANS_NIUTRANS ? (
<>
{i18n("mulkeys_help")}
<Link href={URL_NIUTRANS_REG} target="_blank">
{i18n("reg_niutrans")}
</Link>
</>
) : mulkeysTranslators.includes(translator) ? (
i18n("mulkeys_help")
) : (
""
);
return (
<Stack spacing={3}>
{translator !== OPT_TRANS_MICROSOFT && (
{!builtinTranslators.includes(translator) && (
<>
<TextField
size="small"
@@ -81,6 +191,11 @@ function ApiFields({ translator }) {
name="url"
value={url}
onChange={handleChange}
multiline={translator === OPT_TRANS_DEEPLX}
maxRows={10}
helperText={
translator === OPT_TRANS_DEEPLX ? i18n("mulkeys_help") : ""
}
/>
<TextField
size="small"
@@ -88,10 +203,16 @@ function ApiFields({ translator }) {
name="key"
value={key}
onChange={handleChange}
multiline={mulkeysTranslators.includes(translator)}
maxRows={10}
helperText={keyHelper}
/>
</>
)}
{translator === OPT_TRANS_OPENAI && (
{(translator.startsWith(OPT_TRANS_OPENAI) ||
translator.startsWith(OPT_TRANS_OLLAMA) ||
translator === OPT_TRANS_GEMINI) && (
<>
<TextField
size="small"
@@ -107,26 +228,106 @@ function ApiFields({ translator }) {
value={prompt}
onChange={handleChange}
multiline
maxRows={10}
/>
</>
)}
{translator.startsWith(OPT_TRANS_OPENAI) && (
<>
<TextField
size="small"
label={"Temperature"}
type="number"
name="temperature"
value={temperature}
onChange={handleChange}
/>
<TextField
size="small"
label={"Max Tokens"}
type="number"
name="maxTokens"
value={maxTokens}
onChange={handleChange}
/>
</>
)}
{translator === OPT_TRANS_NIUTRANS && (
<>
<TextField
size="small"
label={"DictNo"}
name="dictNo"
value={dictNo}
onChange={handleChange}
/>
<TextField
size="small"
label={"MemoryNo"}
name="memoryNo"
value={memoryNo}
onChange={handleChange}
/>
</>
)}
{translator.startsWith(OPT_TRANS_CUSTOMIZE) && (
<>
<TextField
size="small"
label={"Request Hook"}
name="reqHook"
value={reqHook}
onChange={handleChange}
multiline
maxRows={10}
/>
<TextField
size="small"
label={"Response Hook"}
name="resHook"
value={resHook}
onChange={handleChange}
multiline
maxRows={10}
/>
</>
)}
<TextField
size="small"
label={i18n("fetch_limit")}
type="number"
name="fetchLimit"
value={fetchLimit}
onChange={handleChange}
/>
<TextField
size="small"
label={i18n("fetch_interval")}
type="number"
name="fetchInterval"
value={fetchInterval}
onChange={handleChange}
/>
<Stack direction="row" spacing={2}>
<TestButton translator={translator} api={api} />
{translator !== OPT_TRANS_MICROSOFT && (
<Button
size="small"
variant="outlined"
onClick={() => {
resetApi();
}}
>
{i18n("restore_default")}
</Button>
)}
<Button
size="small"
variant="outlined"
onClick={() => {
resetApi();
}}
>
{i18n("restore_default")}
</Button>
</Stack>
{translator === OPT_TRANS_CUSTOMIZE && (
{translator.startsWith(OPT_TRANS_CUSTOMIZE) && (
<pre>{i18n("custom_api_help")}</pre>
)}
</Stack>

View File

@@ -0,0 +1,37 @@
import FileDownloadIcon from "@mui/icons-material/FileDownload";
import LoadingButton from "@mui/lab/LoadingButton";
import { useState } from "react";
import { kissLog } from "../../libs/log";
export default function DownloadButton({ handleData, text, fileName }) {
const [loading, setLoading] = useState(false);
const handleClick = async (e) => {
e.preventDefault();
try {
setLoading(true);
const data = await handleData();
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();
} catch (err) {
kissLog(err, "download");
} finally {
setLoading(false);
}
};
return (
<LoadingButton
size="small"
variant="outlined"
onClick={handleClick}
loading={loading}
startIcon={<FileDownloadIcon />}
>
{text}
</LoadingButton>
);
}

View File

@@ -0,0 +1,164 @@
import Stack from "@mui/material/Stack";
import { 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 Box from "@mui/material/Box";
import { useFavWords } from "../../hooks/FavWords";
import DictCont from "../Selection/DictCont";
import SugCont from "../Selection/SugCont";
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";
import { kissLog } from "../../libs/log";
import { apiTranslate } from "../../apis";
import { OPT_TRANS_BAIDU, PHONIC_MAP } from "../../config";
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 && (
<Stack spacing={2}>
<DictCont text={word} />
<SugCont text={word} />
</Stack>
)}
</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) {
kissLog(err, "import rules");
}
};
const handleTranslation = async () => {
const tranList = [];
for (const text of downloadList) {
try {
const dictRes = await apiTranslate({
text,
translator: OPT_TRANS_BAIDU,
fromLang: "en",
toLang: "zh-CN",
});
if (dictRes[2]?.type === 1) {
tranList.push(JSON.parse(dictRes[2].result));
}
} catch (err) {
// skip
}
}
return tranList
.map((dictResult) =>
[
`## ${dictResult.src}`,
dictResult.voice
?.map(Object.entries)
.map((item) => item[0])
.map(([key, val]) => `${PHONIC_MAP[key]?.[0] || key} ${val}`)
.join(" "),
dictResult.content[0].mean
.map(({ pre, cont }) => {
return ` - ${pre ? `[${pre}] ` : ""}${Object.keys(cont).join(
"; "
)}`;
})
.join("\n"),
].join("\n\n")
)
.join("\n\n");
};
return (
<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
handleData={() => downloadList.join("\n")}
text={i18n("export")}
fileName={`kiss-words_${Date.now()}.txt`}
/>
<DownloadButton
handleData={handleTranslation}
text={i18n("export_translation")}
fileName={`kiss-words_${Date.now()}.md`}
/>
<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,4 +1,3 @@
import PropTypes from "prop-types";
import AppBar from "@mui/material/AppBar";
import IconButton from "@mui/material/IconButton";
import MenuIcon from "@mui/icons-material/Menu";
@@ -7,6 +6,7 @@ import Box from "@mui/material/Box";
import Link from "@mui/material/Link";
import { useI18n } from "../../hooks/I18n";
import DarkModeButton from "./DarkModeButton";
import Typography from "@mui/material/Typography";
function Header(props) {
const i18n = useI18n();
@@ -31,22 +31,18 @@ function Header(props) {
<MenuIcon />
</IconButton>
</Box>
<Box sx={{ flexGrow: 1 }}>
<Typography component="div" sx={{ flexGrow: 1, fontWeight: "bold" }}>
<Link
underline="none"
color="inherit"
href={process.env.REACT_APP_HOMEPAGE}
target="_blank"
>{`${i18n("app_name")} v${process.env.REACT_APP_VERSION}`}</Link>
</Box>
</Typography>
<DarkModeButton />
</Toolbar>
</AppBar>
);
}
Header.propTypes = {
onDrawerToggle: PropTypes.func.isRequired,
};
export default Header;

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

@@ -11,7 +11,9 @@ import DesignServicesIcon from "@mui/icons-material/DesignServices";
import { useI18n } from "../../hooks/I18n";
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 }) {
const match = useMatch(url);
@@ -38,6 +40,18 @@ export default function Navigator(props) {
url: "/rules",
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"),
@@ -51,10 +65,10 @@ export default function Navigator(props) {
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 /> },
];

View File

@@ -7,6 +7,7 @@ import Alert from "@mui/material/Alert";
import {
GLOBAL_KEY,
DEFAULT_RULE,
GLOBLA_RULE,
OPT_LANGS_FROM,
OPT_LANGS_TO,
OPT_TRANS_ALL,
@@ -14,8 +15,12 @@ import {
OPT_STYLE_DIY,
OPT_STYLE_USE_COLOR,
URL_KISS_RULES_NEW_ISSUE,
OPT_SYNCTYPE_WORKER,
OPT_TIMING_PAGESCROLL,
DEFAULT_TRANS_TAG,
OPT_TIMING_ALL,
} from "../../config";
import { useState, useRef, useEffect, useMemo } from "react";
import { useState, useEffect, useMemo } from "react";
import { useI18n } from "../../hooks/I18n";
import Typography from "@mui/material/Typography";
import Accordion from "@mui/material/Accordion";
@@ -25,8 +30,6 @@ import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import { useRules } from "../../hooks/Rules";
import MenuItem from "@mui/material/MenuItem";
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 FormControlLabel from "@mui/material/FormControlLabel";
import Switch from "@mui/material/Switch";
@@ -48,11 +51,20 @@ 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";
import { FIXER_ALL } from "../../libs/webfix";
import AddIcon from "@mui/icons-material/Add";
import EditIcon from "@mui/icons-material/Edit";
import CancelIcon from "@mui/icons-material/Cancel";
import SaveIcon from "@mui/icons-material/Save";
import { kissLog } from "../../libs/log";
function RuleFields({ rule, rules, setShow, setKeyword }) {
const initFormValues = rule || {
...DEFAULT_RULE,
transOpen: "true",
const initFormValues = {
...(rule?.pattern === "*" ? GLOBLA_RULE : DEFAULT_RULE),
...(rule || {}),
};
const editMode = !!rule;
@@ -60,9 +72,16 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
const [disabled, setDisabled] = useState(editMode);
const [errors, setErrors] = useState({});
const [formValues, setFormValues] = useState(initFormValues);
const [showMore, setShowMore] = useState(!rules);
const {
pattern,
selector,
keepSelector = "",
terms = "",
selectStyle = "",
parentStyle = "",
injectJs = "",
injectCss = "",
translator,
fromLang,
toLang,
@@ -70,6 +89,17 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
transOpen,
bgColor,
textDiyStyle,
transOnly = "false",
transTiming = OPT_TIMING_PAGESCROLL,
transTag = DEFAULT_TRANS_TAG,
transTitle = "false",
detectRemote = "false",
skipLangs = [],
fixerSelector = "",
fixerFunc = "-",
transStartHook = "",
transEndHook = "",
transRemoveHook = "",
} = formValues;
const hasSamePattern = (str) => {
@@ -177,6 +207,16 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
onFocus={handleFocus}
multiline
/>
<TextField
size="small"
label={i18n("keep_selector")}
helperText={i18n("keep_selector_helper")}
name="keepSelector"
value={keepSelector}
disabled={disabled}
onChange={handleChange}
multiline
/>
<Box>
<Grid container spacing={2} columns={12}>
@@ -297,10 +337,242 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
value={textDiyStyle}
disabled={disabled}
onChange={handleChange}
maxRows={10}
multiline
/>
)}
{showMore && (
<>
<Box>
<Grid container spacing={2} columns={12}>
<Grid item xs={12} sm={6} md={3} lg={2}>
<TextField
select
size="small"
fullWidth
name="transOnly"
value={transOnly}
label={i18n("show_only_translations")}
disabled={disabled}
onChange={handleChange}
>
{GlobalItem}
<MenuItem value={"false"}>{i18n("disable")}</MenuItem>
<MenuItem value={"true"}>{i18n("enable")}</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={3} lg={2}>
<TextField
select
size="small"
fullWidth
name="transTiming"
value={transTiming}
label={i18n("trigger_mode")}
disabled={disabled}
onChange={handleChange}
>
{GlobalItem}
{OPT_TIMING_ALL.map((item) => (
<MenuItem key={item} value={item}>
{i18n(item)}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={3} lg={2}>
<TextField
select
size="small"
fullWidth
name="transTag"
value={transTag}
label={i18n("translation_element_tag")}
disabled={disabled}
onChange={handleChange}
>
{GlobalItem}
<MenuItem value={"span"}>{`<span>`}</MenuItem>
<MenuItem value={"font"}>{`<font>`}</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={3} lg={2}>
<TextField
select
size="small"
fullWidth
name="transTitle"
value={transTitle}
label={i18n("translate_page_title")}
disabled={disabled}
onChange={handleChange}
>
{GlobalItem}
<MenuItem value={"false"}>{i18n("disable")}</MenuItem>
<MenuItem value={"true"}>{i18n("enable")}</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={3} lg={2}>
<TextField
select
size="small"
fullWidth
name="detectRemote"
value={detectRemote}
label={i18n("detect_lang_remote")}
disabled={disabled}
onChange={handleChange}
>
{GlobalItem}
<MenuItem value={"false"}>{i18n("disable")}</MenuItem>
<MenuItem value={"true"}>{i18n("enable")}</MenuItem>
</TextField>
</Grid>
</Grid>
</Box>
<TextField
select
size="small"
label={i18n("skip_langs")}
helperText={i18n("skip_langs_helper")}
name="skipLangs"
value={skipLangs}
disabled={disabled}
onChange={handleChange}
SelectProps={{
multiple: true,
}}
>
{OPT_LANGS_TO.map(([langKey, langName]) => (
<MenuItem key={langKey} value={langKey}>
{langName}
</MenuItem>
))}
</TextField>
<TextField
size="small"
label={i18n("terms")}
helperText={i18n("terms_helper")}
name="terms"
value={terms}
disabled={disabled}
onChange={handleChange}
multiline
maxRows={10}
/>
<TextField
size="small"
label={i18n("fixer_selector")}
name="fixerSelector"
value={fixerSelector}
disabled={disabled}
onChange={handleChange}
multiline
maxRows={10}
/>
<TextField
select
size="small"
name="fixerFunc"
value={fixerFunc}
label={i18n("fixer_function")}
helperText={i18n("fixer_function_helper")}
disabled={disabled}
onChange={handleChange}
>
{GlobalItem}
{FIXER_ALL.map((item) => (
<MenuItem key={item} value={item}>
{item}
</MenuItem>
))}
</TextField>
<TextField
size="small"
label={i18n("translate_start_hook")}
helperText={i18n("translate_start_hook_helper")}
name="transStartHook"
value={transStartHook}
disabled={disabled}
onChange={handleChange}
multiline
maxRows={10}
/>
<TextField
size="small"
label={i18n("translate_end_hook")}
helperText={i18n("translate_end_hook_helper")}
name="transEndHook"
value={transEndHook}
disabled={disabled}
onChange={handleChange}
multiline
maxRows={10}
/>
<TextField
size="small"
label={i18n("translate_remove_hook")}
helperText={i18n("translate_remove_hook_helper")}
name="transRemoveHook"
value={transRemoveHook}
disabled={disabled}
onChange={handleChange}
multiline
maxRows={10}
/>
<TextField
size="small"
label={i18n("selector_style")}
helperText={i18n("selector_style_helper")}
name="selectStyle"
value={selectStyle}
disabled={disabled}
onChange={handleChange}
maxRows={10}
multiline
/>
<TextField
size="small"
label={i18n("selector_parent_style")}
helperText={i18n("selector_style_helper")}
name="parentStyle"
value={parentStyle}
disabled={disabled}
onChange={handleChange}
maxRows={10}
multiline
/>
<TextField
size="small"
label={i18n("inject_css")}
helperText={i18n("inject_css_helper")}
name="injectCss"
value={injectCss}
disabled={disabled}
onChange={handleChange}
maxRows={10}
multiline
/>
<TextField
size="small"
label={i18n("inject_js")}
helperText={i18n("inject_js_helper")}
name="injectJs"
value={injectJs}
disabled={disabled}
onChange={handleChange}
maxRows={10}
multiline
/>
</>
)}
{rules &&
(editMode ? (
// 编辑
@@ -314,6 +586,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
e.preventDefault();
setDisabled(false);
}}
startIcon={<EditIcon />}
>
{i18n("edit")}
</Button>
@@ -325,35 +598,87 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
e.preventDefault();
rules.del(rule.pattern);
}}
startIcon={<DeleteIcon />}
>
{i18n("delete")}
</Button>
)}
{!showMore && (
<Button
size="small"
variant="text"
onClick={() => {
setShowMore(true);
}}
startIcon={<ExpandMoreIcon />}
>
{i18n("more")}
</Button>
)}
</>
) : (
<>
<Button size="small" variant="contained" type="submit">
<Button
size="small"
variant="contained"
type="submit"
startIcon={<SaveIcon />}
>
{i18n("save")}
</Button>
<Button
size="small"
variant="outlined"
onClick={handleCancel}
startIcon={<CancelIcon />}
>
{i18n("cancel")}
</Button>
{!showMore && (
<Button
size="small"
variant="text"
onClick={() => {
setShowMore(true);
}}
startIcon={<ExpandMoreIcon />}
>
{i18n("more")}
</Button>
)}
</>
)}
</Stack>
) : (
// 添加
<Stack direction="row" spacing={2}>
<Button size="small" variant="contained" type="submit">
<Button
size="small"
variant="contained"
type="submit"
startIcon={<SaveIcon />}
>
{i18n("save")}
</Button>
<Button size="small" variant="outlined" onClick={handleCancel}>
<Button
size="small"
variant="outlined"
onClick={handleCancel}
startIcon={<CancelIcon />}
>
{i18n("cancel")}
</Button>
{!showMore && (
<Button
size="small"
variant="text"
onClick={() => {
setShowMore(true);
}}
>
{i18n("more")}
</Button>
)}
</Stack>
))}
</Stack>
@@ -362,6 +687,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
}
function RuleAccordion({ rule, rules }) {
const i18n = useI18n();
const [expanded, setExpanded] = useState(false);
const handleChange = (e) => {
@@ -372,11 +698,14 @@ function RuleAccordion({ rule, rules }) {
<Accordion expanded={expanded} onChange={handleChange}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography
style={{
sx={{
opacity: rules ? 1 : 0.5,
overflowWrap: "anywhere",
}}
>
{rule.pattern}
{rule.pattern === GLOBAL_KEY
? `[${i18n("global_rule")}] ${rule.pattern}`
: rule.pattern}
</Typography>
</AccordionSummary>
<AccordionDetails>
@@ -386,63 +715,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 }) {
const alert = useAlert();
const i18n = useI18n();
const handleClick = async () => {
try {
const { syncUrl, syncKey } = await getSyncWithDefault();
if (!syncUrl || !syncKey) {
const { syncType, syncUrl, syncKey } = await getSyncWithDefault();
if (syncType !== OPT_SYNCTYPE_WORKER || !syncUrl || !syncKey) {
alert.warning(i18n("error_sync_setting"));
return;
}
@@ -462,7 +741,7 @@ function ShareButton({ rules, injectRules, selectedUrl }) {
window.open(url, "_blank");
} catch (err) {
alert.warning(i18n("error_got_some_wrong"));
console.log("[share rules]", err);
kissLog(err, "share rules");
}
};
@@ -488,26 +767,12 @@ function UserRules({ subRules }) {
const injectRules = !!setting?.injectRules;
const { selectedUrl, selectedRules } = subRules;
const handleImport = (e) => {
const file = e.target.files[0];
if (!file) {
return;
const handleImport = async (data) => {
try {
await rules.merge(JSON.parse(data));
} catch (err) {
kissLog(err, "import rules");
}
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 = () => {
@@ -522,6 +787,10 @@ function UserRules({ subRules }) {
}
}, [showAdd]);
if (!rules.list) {
return;
}
return (
<Stack spacing={3}>
<Stack
@@ -539,14 +808,16 @@ function UserRules({ subRules }) {
e.preventDefault();
setShowAdd(true);
}}
startIcon={<AddIcon />}
>
{i18n("add")}
</Button>
<UploadButton text={i18n("import")} onChange={handleImport} />
<UploadButton text={i18n("import")} handleImport={handleImport} />
<DownloadButton
data={JSON.stringify([...rules.list].reverse(), null, "\t")}
handleData={() => JSON.stringify([...rules.list].reverse(), null, 2)}
text={i18n("export")}
fileName={`kiss-rules_${Date.now()}.json`}
/>
<ShareButton
@@ -621,8 +892,9 @@ function SubRulesItem({
syncAt,
selectedUrl,
delSub,
updateSub,
setSelectedRules,
updateDataCache,
deleteDataCache,
}) {
const [loading, setLoading] = useState(false);
@@ -630,8 +902,9 @@ function SubRulesItem({
try {
await delSub(url);
await delSubRules(url);
await deleteDataCache(url);
} catch (err) {
console.log("[del subrules]", err);
kissLog(err, "del subrules");
}
};
@@ -642,9 +915,9 @@ function SubRulesItem({
if (rules.length > 0 && url === selectedUrl) {
setSelectedRules(rules);
}
await updateSub(url, { syncAt: Date.now() });
await updateDataCache(url);
} catch (err) {
console.log("[sync sub rules]", err);
kissLog(err, "sync sub rules");
} finally {
setLoading(false);
}
@@ -652,7 +925,14 @@ function SubRulesItem({
return (
<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 }}>
@@ -677,7 +957,7 @@ function SubRulesItem({
);
}
function SubRulesEdit({ subList, addSub }) {
function SubRulesEdit({ subList, addSub, updateDataCache }) {
const i18n = useI18n();
const [inputText, setInputText] = useState("");
const [inputError, setInputError] = useState("");
@@ -712,10 +992,11 @@ function SubRulesEdit({ subList, addSub }) {
throw new Error("empty rules");
}
await addSub(url);
await updateDataCache(url);
setShowInput(false);
setInputText("");
} catch (err) {
console.log("[fetch rules]", err);
kissLog(err, "fetch rules");
setInputError(i18n("error_fetch_url"));
} finally {
setLoading(false);
@@ -743,6 +1024,7 @@ function SubRulesEdit({ subList, addSub }) {
e.preventDefault();
setShowInput(true);
}}
startIcon={<AddIcon />}
>
{i18n("add")}
</Button>
@@ -767,10 +1049,16 @@ function SubRulesEdit({ subList, addSub }) {
variant="contained"
onClick={handleSave}
disabled={loading}
startIcon={<SaveIcon />}
>
{i18n("save")}
</Button>
<Button size="small" variant="outlined" onClick={handleCancel}>
<Button
size="small"
variant="outlined"
onClick={handleCancel}
startIcon={<CancelIcon />}
>
{i18n("cancel")}
</Button>
</Stack>
@@ -784,7 +1072,6 @@ function SubRules({ subRules }) {
const {
subList,
selectSub,
updateSub,
addSub,
delSub,
selectedUrl,
@@ -792,27 +1079,38 @@ function SubRules({ subRules }) {
setSelectedRules,
loading,
} = subRules;
const { dataCaches, updateDataCache, deleteDataCache, reloadSync } =
useSyncCaches();
const handleSelect = (e) => {
const url = e.target.value;
selectSub(url);
};
useEffect(() => {
reloadSync();
}, [selectedRules, reloadSync]);
return (
<Stack spacing={3}>
<SubRulesEdit subList={subList} addSub={addSub} />
<SubRulesEdit
subList={subList}
addSub={addSub}
updateDataCache={updateDataCache}
/>
<RadioGroup value={selectedUrl} onChange={handleSelect}>
{subList.map((item, index) => (
<SubRulesItem
key={item.url}
url={item.url}
syncAt={item.syncAt}
syncAt={dataCaches[item.url]}
index={index}
selectedUrl={selectedUrl}
delSub={delSub}
updateSub={updateSub}
setSelectedRules={setSelectedRules}
updateDataCache={updateDataCache}
deleteDataCache={deleteDataCache}
/>
))}
</RadioGroup>
@@ -848,6 +1146,8 @@ export default function Rules() {
{i18n("rules_warn_1")}
<br />
{i18n("rules_warn_2")}
<br />
{i18n("rules_warn_3")}
</Alert>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>

View File

@@ -12,71 +12,34 @@ import { limitNumber } from "../../libs/utils";
import { useI18n } from "../../hooks/I18n";
import { useAlert } from "../../hooks/Alert";
import { isExt } from "../../libs/client";
import IconButton from "@mui/material/IconButton";
import EditIcon from "@mui/icons-material/Edit";
import Grid from "@mui/material/Grid";
import {
UI_LANGS,
TRANS_NEWLINE_LENGTH,
CACHE_NAME,
OPT_MOUSEKEY_ALL,
OPT_MOUSEKEY_DISABLE,
OPT_TRANS_MICROSOFT,
OPT_LANGDETECTOR_ALL,
OPT_SHORTCUT_TRANSLATE,
OPT_SHORTCUT_STYLE,
OPT_SHORTCUT_POPUP,
OPT_SHORTCUT_SETTING,
DEFAULT_BLACKLIST,
DEFAULT_CSPLIST,
MSG_CONTEXT_MENUS,
MSG_UPDATE_CSP,
} from "../../config";
import { useEffect, useState, useRef } from "react";
import { useShortcut } from "../../hooks/Shortcut";
import { shortcutListener } from "../../libs/shortcut";
import ShortcutInput from "./ShortcutInput";
import { useFab } from "../../hooks/Fab";
import { sendBgMsg } from "../../libs/msg";
import { kissLog } from "../../libs/log";
import UploadButton from "./UploadButton";
import DownloadButton from "./DownloadButton";
function ShortcutItem({ action, label }) {
const { shortcut, setShortcut } = useShortcut(action);
const [disabled, setDisabled] = useState(true);
const inputRef = useRef(null);
useEffect(() => {
if (disabled) {
return;
}
inputRef.current.focus();
setShortcut([]);
const clearShortcut = shortcutListener((curkeys, allkeys) => {
setShortcut(allkeys);
if (curkeys.length === 0) {
setDisabled(true);
}
}, inputRef.current);
return () => {
clearShortcut();
};
}, [disabled, setShortcut]);
return (
<Stack direction="row">
<TextField
size="small"
label={label}
name={label}
value={shortcut.join(" + ")}
fullWidth
inputRef={inputRef}
disabled={disabled}
onBlur={() => {
setDisabled(true);
}}
/>
<IconButton
onClick={() => {
setDisabled(false);
}}
>
{<EditIcon />}
</IconButton>
</Stack>
<ShortcutInput value={shortcut} onChange={setShortcut} label={label} />
);
}
@@ -84,6 +47,7 @@ export default function Settings() {
const i18n = useI18n();
const { setting, updateSetting } = useSetting();
const alert = useAlert();
const { fab, updateFab } = useFab();
const handleChange = (e) => {
e.preventDefault();
@@ -95,6 +59,9 @@ export default function Settings() {
case "fetchInterval":
value = limitNumber(value, 0, 5000);
break;
case "transInterval":
value = limitNumber(value, 100, 5000);
break;
case "minLength":
value = limitNumber(value, 1, 100);
break;
@@ -104,6 +71,15 @@ export default function Settings() {
case "newlineLength":
value = limitNumber(value, 1, 1000);
break;
case "touchTranslate":
value = limitNumber(value, 0, 4);
break;
case "contextMenuType":
isExt && sendBgMsg(MSG_CONTEXT_MENUS, value);
break;
case "csplist":
isExt && sendBgMsg(MSG_UPDATE_CSP, value);
break;
default:
}
updateSetting({
@@ -116,25 +92,51 @@ export default function Settings() {
caches.delete(CACHE_NAME);
alert.success(i18n("clear_success"));
} catch (err) {
console.log("[clear cache]", err);
kissLog(err, "clear cache");
}
};
const handleImport = async (data) => {
try {
await updateSetting(JSON.parse(data));
} catch (err) {
kissLog(err, "import setting");
}
};
const {
uiLang,
fetchLimit,
fetchInterval,
minLength,
maxLength,
clearCache,
newlineLength = TRANS_NEWLINE_LENGTH,
mouseKey = OPT_MOUSEKEY_DISABLE,
hideFab = false,
contextMenuType = 1,
touchTranslate = 2,
blacklist = DEFAULT_BLACKLIST.join(",\n"),
csplist = DEFAULT_CSPLIST.join(",\n"),
transInterval = 500,
langDetector = OPT_TRANS_MICROSOFT,
} = setting;
const { isHide = false } = fab || {};
return (
<Box>
<Stack spacing={3}>
<Stack
direction="row"
alignItems="center"
spacing={2}
useFlexGap
flexWrap="wrap"
>
<UploadButton text={i18n("import")} handleImport={handleImport} />
<DownloadButton
handleData={() => JSON.stringify(setting, null, 2)}
text={i18n("export")}
fileName={`kiss-setting_${Date.now()}.json`}
/>
</Stack>
<FormControl size="small">
<InputLabel>{i18n("ui_lang")}</InputLabel>
<Select
@@ -151,30 +153,12 @@ export default function Settings() {
</Select>
</FormControl>
<TextField
size="small"
label={i18n("fetch_limit")}
type="number"
name="fetchLimit"
value={fetchLimit}
onChange={handleChange}
/>
<TextField
size="small"
label={i18n("fetch_interval")}
type="number"
name="fetchInterval"
value={fetchInterval}
onChange={handleChange}
/>
<TextField
size="small"
label={i18n("min_translate_length")}
type="number"
name="minLength"
value={minLength}
defaultValue={minLength}
onChange={handleChange}
/>
@@ -183,7 +167,7 @@ export default function Settings() {
label={i18n("max_translate_length")}
type="number"
name="maxLength"
value={maxLength}
defaultValue={maxLength}
onChange={handleChange}
/>
@@ -192,86 +176,155 @@ export default function Settings() {
label={i18n("num_of_newline_characters")}
type="number"
name="newlineLength"
value={newlineLength}
defaultValue={newlineLength}
onChange={handleChange}
/>
<TextField
size="small"
label={i18n("translate_interval")}
type="number"
name="transInterval"
defaultValue={transInterval}
onChange={handleChange}
/>
<FormControl size="small">
<InputLabel>{i18n("mouseover_translation")}</InputLabel>
<InputLabel>{i18n("touch_translate_shortcut")}</InputLabel>
<Select
name="mouseKey"
value={mouseKey}
label={i18n("mouseover_translation")}
name="touchTranslate"
value={touchTranslate}
label={i18n("touch_translate_shortcut")}
onChange={handleChange}
>
{OPT_MOUSEKEY_ALL.map((item) => (
{[0, 2, 3, 4].map((item) => (
<MenuItem key={item} value={item}>
{i18n(item)}
{i18n(`touch_tap_${item}`)}
</MenuItem>
))}
</Select>
</FormControl>
<FormControl size="small">
<InputLabel>{i18n("hide_fab_button")}</InputLabel>
<Select
name="isHide"
value={isHide}
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>
<FormControl size="small">
<InputLabel>{i18n("context_menus")}</InputLabel>
<Select
name="contextMenuType"
value={contextMenuType}
label={i18n("context_menus")}
onChange={handleChange}
>
<MenuItem value={0}>{i18n("hide_context_menus")}</MenuItem>
<MenuItem value={1}>{i18n("simple_context_menus")}</MenuItem>
<MenuItem value={2}>{i18n("secondary_context_menus")}</MenuItem>
</Select>
</FormControl>
<FormControl size="small">
<InputLabel>{i18n("detect_lang_remote")}</InputLabel>
<Select
name="langDetector"
value={langDetector}
label={i18n("detect_lang_remote")}
onChange={handleChange}
>
{OPT_LANGDETECTOR_ALL.map((item) => (
<MenuItem value={item} key={item}>
{item}
</MenuItem>
))}
</Select>
</FormControl>
{isExt ? (
<FormControl size="small">
<InputLabel>{i18n("if_clear_cache")}</InputLabel>
<Select
name="clearCache"
value={clearCache}
label={i18n("if_clear_cache")}
onChange={handleChange}
>
<MenuItem value={false}>{i18n("clear_cache_never")}</MenuItem>
<MenuItem value={true}>{i18n("clear_cache_restart")}</MenuItem>
</Select>
<FormHelperText>
<Link component="button" onClick={handleClearCache}>
{i18n("clear_all_cache_now")}
</Link>
</FormHelperText>
</FormControl>
) : (
<>
<FormControl size="small">
<InputLabel>{i18n("hide_fab_button")}</InputLabel>
<InputLabel>{i18n("if_clear_cache")}</InputLabel>
<Select
name="hideFab"
value={hideFab}
label={i18n("hide_fab_button")}
name="clearCache"
value={clearCache}
label={i18n("if_clear_cache")}
onChange={handleChange}
>
<MenuItem value={false}>{i18n("show")}</MenuItem>
<MenuItem value={true}>{i18n("hide")}</MenuItem>
<MenuItem value={false}>{i18n("clear_cache_never")}</MenuItem>
<MenuItem value={true}>{i18n("clear_cache_restart")}</MenuItem>
</Select>
<FormHelperText>
<Link component="button" onClick={handleClearCache}>
{i18n("clear_all_cache_now")}
</Link>
</FormHelperText>
</FormControl>
<Grid container rowSpacing={2} columns={12}>
<Grid item xs={12} sm={12} md={3} lg={3}>
<ShortcutItem
action={OPT_SHORTCUT_TRANSLATE}
label={i18n("toggle_translate_shortcut")}
/>
<TextField
size="small"
label={i18n("disabled_csplist")}
helperText={
i18n("pattern_helper") + " " + i18n("disabled_csplist_helper")
}
name="csplist"
defaultValue={csplist}
onChange={handleChange}
multiline
/>
</>
) : (
<>
<Box>
<Grid container spacing={2} columns={12}>
<Grid item xs={12} sm={12} md={3} lg={3}>
<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>
<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>
</>
)}
<TextField
size="small"
label={i18n("translate_blacklist")}
helperText={i18n("pattern_helper")}
name="blacklist"
defaultValue={blacklist}
onChange={handleChange}
maxRows={10}
multiline
/>
</Stack>
</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,14 +5,20 @@ import { useI18n } from "../../hooks/I18n";
import { useSync } from "../../hooks/Sync";
import Alert from "@mui/material/Alert";
import Link from "@mui/material/Link";
import { URL_KISS_WORKER } from "../../config";
import MenuItem from "@mui/material/MenuItem";
import LoadingButton from "@mui/lab/LoadingButton";
import {
URL_KISS_WORKER,
OPT_SYNCTYPE_ALL,
OPT_SYNCTYPE_WORKER,
OPT_SYNCTYPE_WEBDAV,
} from "../../config";
import { useState } from "react";
import { syncSettingAndRules } from "../../libs/sync";
import Button from "@mui/material/Button";
import { useAlert } from "../../hooks/Alert";
import SyncIcon from "@mui/icons-material/Sync";
import CircularProgress from "@mui/material/CircularProgress";
import { useSetting } from "../../hooks/Setting";
import { kissLog } from "../../libs/log";
export default function SyncSetting() {
const i18n = useI18n();
@@ -37,20 +43,44 @@ export default function SyncSetting() {
await reloadSetting();
alert.success(i18n("sync_success"));
} catch (err) {
console.log("[sync all]", err);
kissLog(err, "sync all");
alert.error(i18n("sync_failed"));
} finally {
setLoading(false);
}
};
const { syncUrl, syncKey } = sync;
if (!sync) {
return;
}
const {
syncType = OPT_SYNCTYPE_WORKER,
syncUrl = "",
syncUser = "",
syncKey = "",
} = sync;
return (
<Box>
<Stack spacing={3}>
<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
size="small"
label={i18n("data_sync_url")}
@@ -58,12 +88,24 @@ export default function SyncSetting() {
value={syncUrl}
onChange={handleChange}
helperText={
<Link href={URL_KISS_WORKER} target="_blank">
{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
size="small"
type="password"
@@ -80,16 +122,16 @@ export default function SyncSetting() {
useFlexGap
flexWrap="wrap"
>
<Button
<LoadingButton
size="small"
variant="contained"
disabled={!syncUrl || !syncKey || loading}
onClick={handleSyncTest}
startIcon={<SyncIcon />}
loading={loading}
>
{i18n("sync_now")}
</Button>
{loading && <CircularProgress size={16} />}
</LoadingButton>
</Stack>
</Stack>
</Box>

View File

@@ -0,0 +1,279 @@
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_TRANBOX_TRIGGER_CLICK,
OPT_TRANBOX_TRIGGER_ALL,
OPT_DICT_BAIDU,
} 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";
import { isExt } from "../../libs/client";
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":
case "btnOffsetY":
case "boxOffsetX":
case "boxOffsetY":
value = limitNumber(value, -200, 200);
break;
default:
}
updateTranbox({
[name]: value,
});
};
const handleShortcutInput = useCallback(
(val) => {
updateTranbox({ tranboxShortcut: val });
},
[updateTranbox]
);
const {
transOpen,
translator,
fromLang,
toLang,
toLang2 = "en",
tranboxShortcut,
btnOffsetX,
btnOffsetY,
boxOffsetX = 0,
boxOffsetY = 10,
hideTranBtn = false,
hideClickAway = false,
simpleStyle = false,
followSelection = false,
triggerMode = OPT_TRANBOX_TRIGGER_CLICK,
extStyles = "",
enDict = OPT_DICT_BAIDU,
} = 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
select
size="small"
name="toLang2"
value={toLang2}
label={i18n("to_lang2")}
helperText={i18n("to_lang2_helper")}
onChange={handleChange}
>
{[["none", "None"], ...OPT_LANGS_TO].map(([lang, name]) => (
<MenuItem key={lang} value={lang}>
{name}
</MenuItem>
))}
</TextField>
<TextField
select
size="small"
name="enDict"
value={enDict}
label={i18n("english_dict")}
onChange={handleChange}
>
<MenuItem value={"-"}>{i18n("disable")}</MenuItem>
<MenuItem value={OPT_DICT_BAIDU}>{OPT_DICT_BAIDU}</MenuItem>
</TextField>
<TextField
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}
/>
<TextField
size="small"
label={i18n("tranbox_offset_x")}
type="number"
name="boxOffsetX"
defaultValue={boxOffsetX}
onChange={handleChange}
/>
<TextField
size="small"
label={i18n("tranbox_offset_y")}
type="number"
name="boxOffsetY"
defaultValue={boxOffsetY}
onChange={handleChange}
/>
<TextField
select
size="small"
name="hideTranBtn"
value={hideTranBtn}
label={i18n("hide_tran_button")}
onChange={handleChange}
>
<MenuItem value={false}>{i18n("show")}</MenuItem>
<MenuItem value={true}>{i18n("hide")}</MenuItem>
</TextField>
<TextField
select
size="small"
name="hideClickAway"
value={hideClickAway}
label={i18n("hide_click_away")}
onChange={handleChange}
>
<MenuItem value={false}>{i18n("disable")}</MenuItem>
<MenuItem value={true}>{i18n("enable")}</MenuItem>
</TextField>
<TextField
select
size="small"
name="simpleStyle"
value={simpleStyle}
label={i18n("use_simple_style")}
onChange={handleChange}
>
<MenuItem value={false}>{i18n("disable")}</MenuItem>
<MenuItem value={true}>{i18n("enable")}</MenuItem>
</TextField>
<TextField
select
size="small"
name="followSelection"
value={followSelection}
label={i18n("follow_selection")}
onChange={handleChange}
>
<MenuItem value={false}>{i18n("disable")}</MenuItem>
<MenuItem value={true}>{i18n("enable")}</MenuItem>
</TextField>
<TextField
select
size="small"
name="triggerMode"
value={triggerMode}
label={i18n("trigger_mode")}
onChange={handleChange}
>
{OPT_TRANBOX_TRIGGER_ALL.map((item) => (
<MenuItem key={item} value={item}>
{i18n(`trigger_${item}`)}
</MenuItem>
))}
</TextField>
<TextField
size="small"
label={i18n("extend_styles")}
name="extStyles"
defaultValue={extStyles}
onChange={handleChange}
maxRows={10}
multiline
/>
{!isExt && (
<ShortcutInput
value={tranboxShortcut}
onChange={handleShortcutInput}
label={i18n("trigger_tranbox_shortcut")}
/>
)}
</Stack>
</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>
);
}

View File

@@ -1,160 +0,0 @@
import Stack from "@mui/material/Stack";
import TextField from "@mui/material/TextField";
import { 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, rootSlector, fixer } = site;
return (
<Stack spacing={3}>
<TextField
size="small"
label={"rootSlector"}
name="rootSlector"
value={rootSlector || "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 handleSyncTest = async (e) => {
e.preventDefault();
try {
setLoading(true);
await syncWebfix(process.env.REACT_APP_WEBFIXURL);
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);
const sites = await loadOrFetchWebfix(process.env.REACT_APP_WEBFIXURL);
setSites(sites);
} catch (err) {
console.log("[load webfix]", err.message);
} finally {
setLoading(false);
}
})();
}, []);
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

@@ -18,7 +18,9 @@ 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() {
const [error, setError] = useState("");
@@ -36,9 +38,9 @@ export default function Options() {
// 检查版本是否一致
if (version !== process.env.REACT_APP_VERSION) {
setError(
`The version of the script(v${version}) and this page(v${process.env.REACT_APP_VERSION}) are inconsistent.`
`The version of the local script(v${version}) is not the latest version(v${process.env.REACT_APP_VERSION}). 本地脚本之版本(v${version})非最新版(v${process.env.REACT_APP_VERSION})。`
);
break;
return;
}
if (eventName) {
@@ -46,58 +48,41 @@ export default function Options() {
adaptScript(eventName);
}
// 同步数据
await trySyncSettingAndRules();
setReady(true);
break;
}
if (++i > 8) {
setError("Time out.");
break;
setError(
"Time out. Please confirm whether to install or enable KISS Translator GreaseMonkey script? 连接超时,请检查是否安装或启用简约翻译油猴脚本。"
);
return;
}
await sleep(1000);
}
} else {
// 同步数据
await trySyncSettingAndRules();
setReady(true);
}
// 同步数据
await trySyncSettingAndRules();
setReady(true);
})();
}, []);
if (error) {
return (
<center>
<Alert severity="error">{error}</Alert>
<Divider>
<Link
href={process.env.REACT_APP_HOMEPAGE}
>{`KISS Translator v${process.env.REACT_APP_VERSION}`}</Link>
</Divider>
<h2>
Please confirm whether to install or enable KISS Translator
GreaseMonkey script?
</h2>
<Alert severity="error">{error}</Alert>
<Stack spacing={2}>
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}>
Install Userscript 1
</Link>
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL2}>
Install Userscript 2
Install/Update Userscript for Tampermonkey/Violentmonkey
</Link>
<Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL}>
Install Userscript Safari 1
</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
Install/Update Userscript for iOS Safari
</Link>
</Stack>
</center>
@@ -126,9 +111,11 @@ export default function Options() {
<Route path="/" element={<Layout />}>
<Route index element={<Setting />} />
<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="webfix" element={<Webfix />} />
<Route path="words" element={<FavWords />} />
<Route path="about" element={<About />} />
</Route>
</Routes>

View File

@@ -1,9 +1,9 @@
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";
import Typography from "@mui/material/Typography";
export default function Header({ setShowPopup }) {
const handleHomepage = () => {
@@ -21,9 +21,16 @@ export default function Header({ setShowPopup }) {
<IconButton onClick={handleHomepage}>
<HomeIcon />
</IconButton>
<Box>
<Typography
component="div"
sx={{
userSelect: "none",
WebkitUserSelect: "none",
fontWeight: "bold",
}}
>
{`${process.env.REACT_APP_NAME} v${process.env.REACT_APP_VERSION}`}
</Box>
</Typography>
</Stack>
{setShowPopup ? (

View File

@@ -5,7 +5,7 @@ import MenuItem from "@mui/material/MenuItem";
import FormControlLabel from "@mui/material/FormControlLabel";
import Switch from "@mui/material/Switch";
import Button from "@mui/material/Button";
import { sendTabMsg } from "../../libs/msg";
import { sendBgMsg, sendTabMsg, getCurTab } from "../../libs/msg";
import { browser } from "../../libs/browser";
import { isExt } from "../../libs/client";
import { useI18n } from "../../hooks/I18n";
@@ -16,22 +16,29 @@ import {
MSG_TRANS_TOGGLE,
MSG_TRANS_GETRULE,
MSG_TRANS_PUTRULE,
MSG_OPEN_OPTIONS,
MSG_SAVE_RULE,
MSG_COMMAND_SHORTCUTS,
OPT_TRANS_ALL,
OPT_LANGS_FROM,
OPT_LANGS_TO,
OPT_STYLE_ALL,
OPT_STYLE_USE_COLOR,
CACHE_NAME,
} from "../../config";
import { sendIframeMsg } from "../../libs/iframe";
import { saveRule } from "../../libs/rules";
import { tryClearCaches } from "../../libs";
import { kissLog } from "../../libs/log";
export default function Popup({ setShowPopup, translator: tran }) {
const i18n = useI18n();
const [rule, setRule] = useState(tran?.rule);
const [commands, setCommands] = useState({});
const handleOpenSetting = () => {
if (isExt) {
if (!tran) {
browser?.runtime.openOptionsPage();
} else if (isExt) {
sendBgMsg(MSG_OPEN_OPTIONS);
} else {
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank");
}
@@ -42,14 +49,14 @@ export default function Popup({ setShowPopup, translator: tran }) {
try {
setRule({ ...rule, transOpen: e.target.checked ? "true" : "false" });
if (isExt) {
if (!tran) {
await sendTabMsg(MSG_TRANS_TOGGLE);
} else {
tran.toggle();
sendIframeMsg(MSG_TRANS_TOGGLE);
}
} catch (err) {
console.log("[toggle trans]", err);
kissLog(err, "toggle trans");
}
};
@@ -58,27 +65,41 @@ export default function Popup({ setShowPopup, translator: tran }) {
const { name, value } = e.target;
setRule((pre) => ({ ...pre, [name]: value }));
if (isExt) {
if (!tran) {
await sendTabMsg(MSG_TRANS_PUTRULE, { [name]: value });
} else {
tran.updateRule({ [name]: value });
sendIframeMsg(MSG_TRANS_PUTRULE, { [name]: value });
}
} catch (err) {
console.log("[update rule]", err);
kissLog(err, "update rule");
}
};
const handleClearCache = () => {
tryClearCaches();
};
const handleSaveRule = async () => {
try {
caches.delete(CACHE_NAME);
let href = window.location.href;
if (!tran) {
const tab = await getCurTab();
href = tab.url;
}
const newRule = { ...rule, pattern: href.split("/")[2] };
if (isExt && tran) {
sendBgMsg(MSG_SAVE_RULE, newRule);
} else {
saveRule(newRule);
}
} catch (err) {
console.log("[clear cache]", err);
kissLog(err, "save rule");
}
};
useEffect(() => {
if (!isExt) {
if (tran) {
return;
}
(async () => {
@@ -88,15 +109,39 @@ export default function Popup({ setShowPopup, translator: tran }) {
setRule(res.data);
}
} catch (err) {
console.log("[query rule]", err);
kissLog(err, "query rule");
}
})();
}, []);
}, [tran]);
useEffect(() => {
(async () => {
try {
const commands = {};
if (isExt) {
const res = await sendBgMsg(MSG_COMMAND_SHORTCUTS);
res.forEach(({ name, shortcut }) => {
commands[name] = shortcut;
});
} else {
const shortcuts = tran.setting.shortcuts;
if (shortcuts) {
Object.entries(shortcuts).forEach(([key, val]) => {
commands[key] = val.join("+");
});
}
}
setCommands(commands);
} catch (err) {
kissLog(err, "query cmds");
}
})();
}, [tran]);
if (!rule) {
return (
<Box minWidth={300}>
{isExt && (
{!tran && (
<>
<Header />
<Divider />
@@ -111,11 +156,11 @@ export default function Popup({ setShowPopup, translator: tran }) {
);
}
const { transOpen, translator, fromLang, toLang, textStyle, bgColor } = rule;
const { transOpen, translator, fromLang, toLang, textStyle } = rule;
return (
<Box minWidth={300}>
{isExt && (
{!tran && (
<>
<Header />
<Divider />
@@ -135,13 +180,12 @@ export default function Popup({ setShowPopup, translator: tran }) {
onChange={handleTransToggle}
/>
}
label={i18n("translate_alt")}
label={
commands["toggleTranslate"]
? `${i18n("translate_alt")}(${commands["toggleTranslate"]})`
: i18n("translate_alt")
}
/>
{!isExt && (
<Button variant="text" onClick={handleClearCache}>
{i18n("clear_cache")}
</Button>
)}
</Stack>
<TextField
@@ -198,7 +242,11 @@ export default function Popup({ setShowPopup, translator: tran }) {
size="small"
value={textStyle}
name="textStyle"
label={i18n("text_style_alt")}
label={
commands["toggleStyle"]
? `${i18n("text_style_alt")}(${commands["toggleStyle"]})`
: i18n("text_style_alt")
}
onChange={handleChange}
>
{OPT_STYLE_ALL.map((item) => (
@@ -208,7 +256,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
))}
</TextField>
{OPT_STYLE_USE_COLOR.includes(textStyle) && (
{/* {OPT_STYLE_USE_COLOR.includes(textStyle) && (
<TextField
size="small"
name="bgColor"
@@ -216,11 +264,26 @@ export default function Popup({ setShowPopup, translator: tran }) {
label={i18n("bg_color")}
onChange={handleChange}
/>
)}
)} */}
<Button variant="text" onClick={handleOpenSetting}>
{i18n("setting")}
</Button>
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
spacing={2}
>
<Button variant="text" onClick={handleSaveRule}>
{i18n("save_rule")}
</Button>
{!isExt && (
<Button variant="text" onClick={handleClearCache}>
{i18n("clear_cache")}
</Button>
)}
<Button variant="text" onClick={handleOpenSetting}>
{i18n("setting")}
</Button>
</Stack>
</Stack>
</Box>
);

View File

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

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 = async (e) => {
e.stopPropagation();
await 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,115 @@
import { useState, useEffect } from "react";
import Stack from "@mui/material/Stack";
import FavBtn from "./FavBtn";
import Typography from "@mui/material/Typography";
import AudioBtn from "./AudioBtn";
import CircularProgress from "@mui/material/CircularProgress";
import Alert from "@mui/material/Alert";
import { OPT_TRANS_BAIDU, PHONIC_MAP } from "../../config";
import { apiTranslate } from "../../apis";
import { isValidWord } from "../../libs/utils";
import CopyBtn from "./CopyBtn";
export default function DictCont({ text }) {
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [dictResult, setDictResult] = useState(null);
useEffect(() => {
(async () => {
try {
setLoading(true);
setError("");
setDictResult(null);
if (!isValidWord(text)) {
return;
}
const dictRes = await apiTranslate({
text,
translator: OPT_TRANS_BAIDU,
fromLang: "en",
toLang: "zh-CN",
});
if (dictRes[2]?.type === 1) {
setDictResult(JSON.parse(dictRes[2].result));
}
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
})();
}, [text]);
if (error) {
return <Alert severity="error">{error}</Alert>;
}
if (loading) {
return <CircularProgress size={16} />;
}
if (!text || !dictResult) {
return;
}
const copyText = [
dictResult.src,
dictResult.voice
?.map(Object.entries)
.map((item) => item[0])
.map(([key, val]) => `${PHONIC_MAP[key]?.[0] || key} ${val}`)
.join(" "),
dictResult.content[0].mean
.map(({ pre, cont }) => {
return `${pre ? `[${pre}] ` : ""}${Object.keys(cont).join("; ")}`;
})
.join("\n"),
].join("\n");
return (
<Stack className="KT-transbox-dict" spacing={1}>
<Stack direction="row" justifyContent="space-between">
<Typography variant="subtitle1" style={{ fontWeight: "bold" }}>
{dictResult.src}
</Typography>
<Stack direction="row" justifyContent="space-between">
<CopyBtn text={copyText} />
<FavBtn word={dictResult.src} />
</Stack>
</Stack>
<Typography component="div">
<Typography component="div">
{dictResult.voice
?.map(Object.entries)
.map((item) => item[0])
.map(([key, val]) => (
<Typography
component="div"
key={key}
style={{ display: "inline-block" }}
>
<Typography component="span">{`${
PHONIC_MAP[key]?.[0] || key
} ${val}`}</Typography>
<AudioBtn text={dictResult.src} lan={PHONIC_MAP[key]?.[1]} />
</Typography>
))}
</Typography>
<Typography component="ul">
{dictResult.content[0].mean.map(({ pre, cont }, idx) => (
<Typography component="li" key={idx}>
{pre && `[${pre}] `}
{Object.keys(cont).join("; ")}
</Typography>
))}
</Typography>
</Typography>
</Stack>
);
}

View File

@@ -0,0 +1,270 @@
import { useState } from "react";
import Paper from "@mui/material/Paper";
import Box from "@mui/material/Box";
import { isMobile } from "../../libs/mobile";
function Pointer({
direction,
size,
setSize,
position,
setPosition,
children,
minSize,
maxSize,
...props
}) {
const [origin, setOrigin] = useState(null);
function handlePointerDown(e) {
!isMobile && e.target.setPointerCapture(e.pointerId);
const { clientX, clientY } = isMobile ? e.targetTouches[0] : e;
setOrigin({
x: position.x,
y: position.y,
w: size.w,
h: size.h,
clientX,
clientY,
});
}
function handlePointerMove(e) {
const { clientX, clientY } = isMobile ? e.targetTouches[0] : e;
if (origin) {
const dx = clientX - origin.clientX;
const dy = 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) {
e.stopPropagation();
setOrigin(null);
}
const touchProps = isMobile
? {
onTouchStart: handlePointerDown,
onTouchMove: handlePointerMove,
onTouchEnd: handlePointerUp,
}
: {
onPointerDown: handlePointerDown,
onPointerMove: handlePointerMove,
onPointerUp: handlePointerUp,
};
return (
<div {...props} {...touchProps}>
{children}
</div>
);
}
export default function DraggableResizable({
header,
children,
position = {
x: 0,
y: 0,
},
size = {
w: 600,
h: 400,
},
minSize = {
w: 300,
h: 200,
},
maxSize = {
w: 1200,
h: 1200,
},
setSize,
setPosition,
onChangeSize,
onChangePosition,
...props
}) {
const lineWidth = 4;
const opts = {
size,
setSize,
position,
setPosition,
minSize,
maxSize,
};
return (
<Box
className="KT-draggable"
style={{
touchAction: "none",
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,
}}
{...props}
>
<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 className="KT-draggable-body" elevation={4}>
<Pointer
className="KT-draggable-header"
direction="Header"
style={{ cursor: "move" }}
{...opts}
>
{header}
</Pointer>
<Box
className="KT-draggable-container"
style={{
width: size.w,
height: size.h,
overflow: "hidden auto",
}}
>
{children}
</Box>
</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,32 @@
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";
import { kissLog } from "../../libs/log";
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) {
kissLog(err, "set fav");
} 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,35 @@
import { useState, useEffect } from "react";
import Typography from "@mui/material/Typography";
import { apiBaiduSuggest } from "../../apis";
import Stack from "@mui/material/Stack";
export default function SugCont({ text }) {
const [sugs, setSugs] = useState([]);
useEffect(() => {
(async () => {
try {
setSugs(await apiBaiduSuggest(text));
} catch (err) {
// skip
}
})();
}, [text]);
if (sugs.length === 0) {
return;
}
return (
<Stack className="KT-transbox-sug" spacing={1}>
{sugs.map(({ k, v }) => (
<Typography component="div" key={k}>
<Typography>{k}</Typography>
<Typography component="ul" style={{ margin: "0" }}>
<Typography component="li">{v}</Typography>
</Typography>
</Typography>
))}
</Stack>
);
}

View File

@@ -0,0 +1,328 @@
import { SettingProvider } from "../../hooks/Setting";
import ThemeProvider from "../../hooks/Theme";
import DraggableResizable from "./DraggableResizable";
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 DragIndicatorIcon from "@mui/icons-material/DragIndicator";
import UnfoldLessIcon from "@mui/icons-material/UnfoldLess";
import UnfoldMoreIcon from "@mui/icons-material/UnfoldMore";
import PushPinIcon from "@mui/icons-material/PushPin";
import PushPinOutlinedIcon from "@mui/icons-material/PushPinOutlined";
import LockIcon from "@mui/icons-material/Lock";
import LockOpenIcon from "@mui/icons-material/LockOpen";
import CloseIcon from "@mui/icons-material/Close";
import { useI18n } from "../../hooks/I18n";
import { OPT_TRANS_ALL, OPT_LANGS_FROM, OPT_LANGS_TO } from "../../config";
import { useState, useRef } from "react";
import TranCont from "./TranCont";
import DictCont from "./DictCont";
import SugCont from "./SugCont";
import CopyBtn from "./CopyBtn";
import { isValidWord } from "../../libs/utils";
import { isMobile } from "../../libs/mobile";
function Header({
setShowPopup,
simpleStyle,
setSimpleStyle,
hideClickAway,
setHideClickAway,
followSelection,
setFollowSelection,
mouseHover,
}) {
if (!isMobile && simpleStyle && !mouseHover) {
return;
}
return (
<Box
className="KT-transbox-header"
onMouseUp={(e) => e.stopPropagation()}
onTouchEnd={(e) => e.stopPropagation()}
>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<DragIndicatorIcon fontSize="small" />
<Stack direction="row" alignItems="center">
<IconButton
size="small"
onClick={() => {
setHideClickAway((pre) => !pre);
}}
>
{hideClickAway ? (
<LockOpenIcon fontSize="small" />
) : (
<LockIcon fontSize="small" />
)}
</IconButton>
<IconButton
size="small"
onClick={() => {
setFollowSelection((pre) => !pre);
}}
>
{followSelection ? (
<PushPinOutlinedIcon fontSize="small" />
) : (
<PushPinIcon fontSize="small" />
)}
</IconButton>
<IconButton
size="small"
onClick={() => {
setSimpleStyle((pre) => !pre);
}}
>
{simpleStyle ? (
<UnfoldMoreIcon fontSize="small" />
) : (
<UnfoldLessIcon fontSize="small" />
)}
</IconButton>
<IconButton
size="small"
onClick={() => {
setShowPopup(false);
}}
>
<CloseIcon fontSize="small" />
</IconButton>
</Stack>
</Stack>
<Divider />
</Box>
);
}
function TranForm({
text,
setText,
tranboxSetting,
transApis,
simpleStyle,
langDetector,
enDict,
}) {
const i18n = useI18n();
const [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
className="KT-transbox-container"
sx={{ p: simpleStyle ? 1 : 2 }}
spacing={simpleStyle ? 1 : 2}
>
{!simpleStyle && (
<>
<Box className="KT-transbox-select">
<Grid container spacing={simpleStyle ? 1 : 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 className="KT-transbox-origin">
<TextField
size="small"
label={i18n("original_text")}
inputRef={inputRef}
fullWidth
multiline
value={editMode ? editText : text}
onChange={(e) => {
setEditText(e.target.value);
}}
onFocus={() => {
setEditMode(true);
setEditText(text);
}}
onBlur={() => {
setEditMode(false);
setText(editText.trim());
}}
InputProps={{
endAdornment: (
<Stack
direction="row"
sx={{
position: "absolute",
right: 0,
top: 0,
}}
>
{editMode ? (
<IconButton
size="small"
onClick={(e) => {
e.stopPropagation();
}}
>
<DoneIcon fontSize="inherit" />
</IconButton>
) : (
<CopyBtn text={text} />
)}
</Stack>
),
}}
/>
</Box>
</>
)}
{(!simpleStyle ||
!isValidWord(text) ||
!toLang.startsWith("zh") ||
enDict === "-") && (
<TranCont
text={text}
translator={translator}
fromLang={fromLang}
toLang={toLang}
toLang2={tranboxSetting.toLang2}
transApis={transApis}
simpleStyle={simpleStyle}
langDetector={langDetector}
/>
)}
{enDict !== "-" && (
<>
<DictCont text={text} />
<SugCont text={text} />
</>
)}
</Stack>
);
}
export default function TranBox({
text,
setText,
setShowBox,
tranboxSetting,
transApis,
boxSize,
setBoxSize,
boxPosition,
setBoxPosition,
simpleStyle,
setSimpleStyle,
hideClickAway,
setHideClickAway,
followSelection,
setFollowSelection,
extStyles,
langDetector,
enDict,
}) {
const [mouseHover, setMouseHover] = useState(false);
return (
<SettingProvider>
<ThemeProvider styles={extStyles}>
<DraggableResizable
position={boxPosition}
size={boxSize}
setSize={setBoxSize}
setPosition={setBoxPosition}
header={
<Header
setShowPopup={setShowBox}
simpleStyle={simpleStyle}
setSimpleStyle={setSimpleStyle}
hideClickAway={hideClickAway}
setHideClickAway={setHideClickAway}
followSelection={followSelection}
setFollowSelection={setFollowSelection}
mouseHover={mouseHover}
/>
}
onClick={(e) => e.stopPropagation()}
onMouseEnter={() => setMouseHover(true)}
onMouseLeave={() => setMouseHover(false)}
>
<TranForm
text={text}
setText={setText}
tranboxSetting={tranboxSetting}
transApis={transApis}
simpleStyle={simpleStyle}
langDetector={langDetector}
enDict={enDict}
/>
</DraggableResizable>
</ThemeProvider>
</SettingProvider>
);
}

View File

@@ -0,0 +1,47 @@
import { isMobile } from "../../libs/mobile";
import { limitNumber } from "../../libs/utils";
export default function TranBtn({
onTrigger,
btnEvent,
position,
btnOffsetX,
btnOffsetY,
}) {
const left = limitNumber(position.x + btnOffsetX, 0, window.innerWidth - 32);
const top = limitNumber(position.y + btnOffsetY, 0, window.innerHeight - 32);
return (
<div
className="KT-tranbtn"
style={{
cursor: "pointer",
// position: "absolute",
position: "fixed",
left,
top,
zIndex: 2147483647,
}}
{...{ [btnEvent]: onTrigger }}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width={isMobile ? "32" : "20"}
height={isMobile ? "32" : "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,104 @@
import TextField from "@mui/material/TextField";
import Box from "@mui/material/Box";
import CircularProgress from "@mui/material/CircularProgress";
import Stack from "@mui/material/Stack";
import { useI18n } from "../../hooks/I18n";
import { DEFAULT_TRANS_APIS } from "../../config";
import { useEffect, useState } from "react";
import { apiTranslate } from "../../apis";
import CopyBtn from "./CopyBtn";
import Typography from "@mui/material/Typography";
import Alert from "@mui/material/Alert";
import { tryDetectLang } from "../../libs";
export default function TranCont({
text,
translator,
fromLang,
toLang,
toLang2 = "en",
transApis,
simpleStyle,
langDetector,
}) {
const i18n = useI18n();
const [trText, setTrText] = useState("");
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
useEffect(() => {
(async () => {
try {
setLoading(true);
setTrText("");
setError("");
let to = toLang;
if (toLang !== toLang2 && toLang2 !== "none") {
const detectLang = await tryDetectLang(text, true, langDetector);
if (detectLang === toLang) {
to = toLang2;
}
}
const apiSetting =
transApis[translator] || DEFAULT_TRANS_APIS[translator];
const tranRes = await apiTranslate({
text,
translator,
fromLang,
toLang: to,
apiSetting,
});
setTrText(tranRes[0]);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
})();
}, [text, translator, fromLang, toLang, toLang2, transApis, langDetector]);
if (simpleStyle) {
return (
<Box className="KT-transbox-target KT-transbox-target_simple">
{error ? (
<Alert severity="error">{error}</Alert>
) : loading ? (
<CircularProgress size={16} />
) : (
<Typography style={{ whiteSpace: "pre-line" }}>{trText}</Typography>
)}
</Box>
);
}
return (
<Box className="KT-transbox-target KT-transbox-target_default">
<TextField
size="small"
label={i18n("translated_text")}
// disabled
fullWidth
multiline
value={trText}
helperText={error}
InputProps={{
startAdornment: loading ? <CircularProgress size={16} /> : null,
endAdornment: (
<Stack
direction="row"
sx={{
position: "absolute",
right: 0,
top: 0,
}}
>
<CopyBtn text={trText} />
</Stack>
),
}}
/>
</Box>
);
}

View File

@@ -0,0 +1,259 @@
import { useState, useEffect, useCallback, useMemo } from "react";
import TranBtn from "./TranBtn";
import TranBox from "./TranBox";
import { shortcutRegister } from "../../libs/shortcut";
import { sleep, limitNumber } from "../../libs/utils";
import { isGm, isExt } from "../../libs/client";
import {
MSG_OPEN_TRANBOX,
DEFAULT_TRANBOX_SHORTCUT,
OPT_TRANBOX_TRIGGER_CLICK,
OPT_TRANBOX_TRIGGER_HOVER,
OPT_TRANBOX_TRIGGER_SELECT,
OPT_DICT_BAIDU,
} from "../../config";
import { isMobile } from "../../libs/mobile";
import { kissLog } from "../../libs/log";
import { useLangMap } from "../../hooks/I18n";
export default function Slection({
contextMenuType,
tranboxSetting,
transApis,
uiLang,
langDetector,
}) {
const {
hideTranBtn = false,
simpleStyle: initSimpleStyle = false,
hideClickAway: initHideClickAway = false,
followSelection: initFollowMouse = false,
tranboxShortcut = DEFAULT_TRANBOX_SHORTCUT,
triggerMode = OPT_TRANBOX_TRIGGER_CLICK,
extStyles,
btnOffsetX,
btnOffsetY,
boxOffsetX = 0,
boxOffsetY = 10,
enDict = OPT_DICT_BAIDU,
} = tranboxSetting;
const boxWidth =
isMobile || initSimpleStyle
? 300
: limitNumber(window.innerWidth, 300, 600);
const boxHeight =
isMobile || initSimpleStyle
? 200
: limitNumber(window.innerHeight, 200, 400);
const langMap = useLangMap(uiLang);
const [showBox, setShowBox] = useState(false);
const [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: boxWidth,
h: boxHeight,
});
const [boxPosition, setBoxPosition] = useState({
x: (window.innerWidth - boxWidth) / 2,
y: (window.innerHeight - boxHeight) / 2,
});
const [simpleStyle, setSimpleStyle] = useState(initSimpleStyle);
const [hideClickAway, setHideClickAway] = useState(initHideClickAway);
const [followSelection, setFollowSelection] = useState(initFollowMouse);
const handleTrigger = useCallback(
(text) => {
setShowBtn(false);
setText(text || selectedText);
setShowBox(true);
},
[selectedText]
);
const handleTranbox = useCallback(() => {
setShowBtn(false);
const selection = window.getSelection();
const selectedText = selection?.toString()?.trim() || "";
if (!selectedText) {
setShowBox((pre) => !pre);
return;
}
const rect = selection?.getRangeAt(0)?.getBoundingClientRect();
if (rect && followSelection) {
const x = (rect.left + rect.right) / 2 + boxOffsetX;
const y = rect.bottom + boxOffsetY;
setBoxPosition({
x: limitNumber(x, 0, window.innerWidth - 300),
y: limitNumber(y, 0, window.innerHeight - 200),
});
}
setSelText(selectedText);
setText(selectedText);
setShowBox(true);
}, [followSelection, boxOffsetX, boxOffsetY]);
const btnEvent = useMemo(() => {
if (isMobile) {
return "onTouchEnd";
} else if (triggerMode === OPT_TRANBOX_TRIGGER_HOVER) {
return "onMouseOver";
}
return "onMouseUp";
}, [triggerMode]);
useEffect(() => {
async function handleMouseup(e) {
e.stopPropagation();
await sleep(200);
const selection = window.getSelection();
const selectedText = selection?.toString()?.trim() || "";
setSelText(selectedText);
if (!selectedText) {
setShowBtn(false);
return;
}
const rect = selection?.getRangeAt(0)?.getBoundingClientRect();
if (rect && followSelection) {
const x = (rect.left + rect.right) / 2 + boxOffsetX;
const y = rect.bottom + boxOffsetY;
setBoxPosition({
x: limitNumber(x, 0, window.innerWidth - 300),
y: limitNumber(y, 0, window.innerHeight - 200),
});
}
if (triggerMode === OPT_TRANBOX_TRIGGER_SELECT) {
handleTrigger(selectedText);
return;
}
const { clientX, clientY } = isMobile ? e.changedTouches[0] : e;
setShowBtn(!hideTranBtn);
setPosition({ x: clientX, y: clientY });
}
// todo: mobile support
// window.addEventListener("mouseup", handleMouseup);
window.addEventListener(isMobile ? "touchend" : "mouseup", handleMouseup);
return () => {
window.removeEventListener(
isMobile ? "touchend" : "mouseup",
handleMouseup
);
};
}, [
hideTranBtn,
triggerMode,
followSelection,
boxOffsetX,
boxOffsetY,
handleTrigger,
]);
useEffect(() => {
if (isExt) {
return;
}
const clearShortcut = shortcutRegister(tranboxShortcut, handleTranbox);
return () => {
clearShortcut();
};
}, [tranboxShortcut, handleTranbox]);
useEffect(() => {
window.addEventListener(MSG_OPEN_TRANBOX, handleTranbox);
return () => {
window.removeEventListener(MSG_OPEN_TRANBOX, handleTranbox);
};
}, [handleTranbox]);
useEffect(() => {
if (!isGm) {
return;
}
// 注册菜单
try {
const menuCommandIds = [];
contextMenuType !== 0 &&
menuCommandIds.push(
GM.registerMenuCommand(
langMap("translate_selected_text"),
(event) => {
handleTranbox();
},
"S"
)
);
return () => {
menuCommandIds.forEach((id) => {
GM.unregisterMenuCommand(id);
});
};
} catch (err) {
kissLog(err, "registerMenuCommand");
}
}, [handleTranbox, contextMenuType, langMap]);
useEffect(() => {
if (hideClickAway) {
const handleHideBox = () => {
setShowBox(false);
};
window.addEventListener("click", handleHideBox);
return () => {
window.removeEventListener("click", handleHideBox);
};
}
}, [hideClickAway]);
return (
<>
{showBox && (
<TranBox
text={text}
setText={setText}
boxSize={boxSize}
setBoxSize={setBoxSize}
boxPosition={boxPosition}
setBoxPosition={setBoxPosition}
tranboxSetting={tranboxSetting}
transApis={transApis}
setShowBox={setShowBox}
simpleStyle={simpleStyle}
setSimpleStyle={setSimpleStyle}
hideClickAway={hideClickAway}
setHideClickAway={setHideClickAway}
followSelection={followSelection}
setFollowSelection={setFollowSelection}
extStyles={extStyles}
langDetector={langDetector}
enDict={enDict}
/>
)}
{showBtn && (
<TranBtn
position={position}
btnOffsetX={btnOffsetX}
btnOffsetY={btnOffsetY}
btnEvent={btnEvent}
onTrigger={(e) => {
e.stopPropagation();
handleTrigger();
}}
/>
)}
</>
);
}

15289
yarn.lock

File diff suppressed because it is too large Load Diff