Compare commits

...

508 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
Gabe Yuan
7b6148302d v1.6.10 2023-09-09 20:15:09 +08:00
Gabe Yuan
38c781b8f3 fix open setting shortcut 2023-09-09 20:13:36 +08:00
Gabe Yuan
64d827fdcd v1.6.9 2023-09-09 19:51:32 +08:00
Gabe Yuan
74ad812f37 text opacity 2023-09-09 19:43:12 +08:00
Gabe Yuan
364c829119 fix sync bug 2023-09-09 19:26:22 +08:00
Gabe Yuan
1ac2c5b61e fix shortcut 2023-09-09 17:15:13 +08:00
Gabe Yuan
0766199353 check shortcut length 2023-09-09 15:26:05 +08:00
Gabe Yuan
878bccf151 hide fab & open setting shortcut 2023-09-09 15:08:34 +08:00
Gabe Yuan
acbd258296 shorten english tab name 2023-09-09 14:10:01 +08:00
Gabe Yuan
54a14e6e5a shortcut set blank 2023-09-09 14:05:45 +08:00
Gabe Yuan
298e4b52f0 v1.6.8 2023-09-08 23:43:34 +08:00
Gabe Yuan
bee1fbcf88 follow global style bug 2023-09-08 23:38:36 +08:00
Gabe Yuan
345a34287e help button 2023-09-08 21:57:42 +08:00
Gabe Yuan
441a2ca2da webfix 2023-09-08 21:41:32 +08:00
Gabe Yuan
1ff1b21355 fix get setting with default 2023-09-08 17:10:54 +08:00
Gabe Yuan
117ca4e05b webfix setting 2023-09-08 16:56:00 +08:00
Gabe Yuan
07d457be4e add clear all rules button 2023-09-08 15:32:42 +08:00
Gabe Yuan
d48296046e show subrule sync time 2023-09-08 15:16:10 +08:00
Gabe Yuan
56350de2cf fix shortcut 2023-09-08 13:53:33 +08:00
Gabe Yuan
850dc0e83b fix throttle func 2023-09-08 10:52:42 +08:00
Gabe Yuan
35f01478b1 add throttle func 2023-09-08 10:32:44 +08:00
Gabe Yuan
f9a3ec012f fix shortcut 2023-09-07 23:51:08 +08:00
Gabe Yuan
3b9b404482 shortcuts dev 2023-09-07 23:47:24 +08:00
Gabe Yuan
d8b0cc4834 shortcuts dev... 2023-09-07 18:12:45 +08:00
Gabe Yuan
da13f5e218 add isAllchar func 2023-09-07 10:20:08 +08:00
Gabe Yuan
08e14ae11c fix i18n text 2023-09-07 00:03:19 +08:00
Gabe Yuan
c2902dff28 fix i18n text 2023-09-06 23:58:11 +08:00
Gabe Yuan
c4fb39f02f fix var text 2023-09-06 23:44:01 +08:00
Gabe Yuan
b7df44c35a hide clear cache in userscript 2023-09-06 23:39:11 +08:00
Gabe Yuan
9a2b21eee5 mouseover dev 2023-09-06 23:35:09 +08:00
Gabe Yuan
bdac67df88 mouseover dev... 2023-09-06 18:00:18 +08:00
Gabe Yuan
0b8f19bfad remove styled-components 2023-09-06 15:12:17 +08:00
Gabe Yuan
c7c5866131 customize api 2023-09-06 14:57:02 +08:00
Gabe Yuan
f772fa000c customize api... 2023-09-06 00:25:46 +08:00
Gabe Yuan
93fd82fcd9 fuzzy style 2023-09-05 17:00:18 +08:00
Gabe Yuan
3ae10bfd04 fix herf match func 2023-09-05 16:11:33 +08:00
Gabe Yuan
a44747ccad fix herf match func 2023-09-05 16:06:48 +08:00
Gabe Yuan
87ab45f936 fix readme text 2023-09-05 13:35:12 +08:00
Gabe Yuan
37b046eb46 fix rule selector 2023-09-05 13:27:04 +08:00
Gabe Yuan
c6f8a45027 v1.6.7 2023-09-04 23:28:41 +08:00
Gabe Yuan
6ec16e1f98 fix popup header 2023-09-04 23:24:50 +08:00
Gabe Yuan
40adf85b20 fix help link 2023-09-04 23:12:35 +08:00
Gabe Yuan
4c78f469c1 update readme 2023-09-04 23:08:00 +08:00
Gabe Yuan
55af58faac add rules help button 2023-09-04 22:29:39 +08:00
Gabe Yuan
4200caa641 fix popup header 2023-09-04 22:03:17 +08:00
Gabe Yuan
0ac06f8e3d use new rules link 2023-09-04 17:04:28 +08:00
Gabe Yuan
966c78fb16 optimize test link 2023-09-03 22:18:33 +08:00
Gabe Yuan
5c5a35d3bb v1.6.6 2023-09-03 21:47:38 +08:00
Gabe Yuan
2c24214f48 fix Violentmonkey --> .connect 2023-09-03 21:45:06 +08:00
Gabe Yuan
67d9e70b3c v1,6,5 2023-09-03 21:10:35 +08:00
Gabe Yuan
000a55f43b modify content.html 2023-09-03 21:08:45 +08:00
Gabe Yuan
4096a6976c add clear cache & api test button 2023-09-03 13:11:04 +08:00
Gabe Yuan
df4c4ebd50 fix i18n text 2023-09-03 00:26:57 +08:00
Gabe Yuan
b43bd4e0e2 add deepl @connect 2023-09-03 00:10:07 +08:00
Gabe Yuan
2660dbf866 update readme 2023-09-02 23:45:40 +08:00
Gabe Yuan
e0b7c60099 v1.6.4 2023-09-02 20:07:24 +08:00
Gabe Yuan
536b58bf67 fix fuzzy style hover bug 2023-09-02 19:55:26 +08:00
Gabe Yuan
6bb742f828 v1.6.3 2023-09-02 19:16:14 +08:00
Gabe Yuan
72742e5e12 fix build:script 2023-09-02 19:09:12 +08:00
Gabe Yuan
3667e0a509 update readme 2023-09-02 18:14:42 +08:00
Gabe Yuan
c2d7668ba7 v1.6.2 2023-09-02 17:26:37 +08:00
Gabe Yuan
aa830f5e20 update readme 2023-09-02 17:22:35 +08:00
Gabe Yuan
b593fa4146 add deepl api 2023-09-02 16:57:09 +08:00
Gabe Yuan
b00b906484 optimize inject script 2023-09-02 15:54:51 +08:00
Gabe Yuan
c1bd6a1be6 use random eventname 2023-09-02 14:14:27 +08:00
Gabe Yuan
36739f04b3 add more shortcut 2023-09-02 13:14:27 +08:00
Gabe Yuan
23eb92853e register a shortcut for userscript 2023-09-02 00:42:15 +08:00
Gabe Yuan
5ab2910dc7 v1.6.1 2023-09-01 22:46:41 +08:00
Gabe Yuan
40d07f6764 fix proxy link 2023-09-01 22:28:28 +08:00
Gabe Yuan
5c8e216169 overwrite subscribe rules 2023-09-01 22:27:25 +08:00
Gabe Yuan
5ba061deda merge yarn 2023-09-01 19:35:48 +08:00
Gabe Yuan
935c83185d fix style text 2023-09-01 17:07:21 +08:00
Gabe Yuan
6327391e65 hide color input when diy style 2023-09-01 17:02:47 +08:00
Gabe Yuan
3d656cf5b0 use debounce to sync setting 2023-09-01 16:39:57 +08:00
Gabe Yuan
d570a0f1a2 replace 'document.documentElement.clientWidth' to 'window.innerWidth' 2023-09-01 16:11:31 +08:00
Gabe Yuan
503a71302c support diy text styles 2023-09-01 15:57:05 +08:00
Gabe Yuan
3e36ceb5b9 num of newline characters 2023-09-01 11:21:06 +08:00
Gabe Yuan
cde7a1d49f add kiss-proxy link 2023-09-01 11:03:53 +08:00
Gabe Yuan
b14a38e4fb sync then reload setting 2023-09-01 10:15:57 +08:00
Gabe Yuan
732a526a8e v1.6.0 2023-08-31 21:24:40 +08:00
Gabe Yuan
2da5ffef44 update userscript download link 2023-08-31 21:21:36 +08:00
Gabe Yuan
2e6e52004f fix firefox bug 2023-08-31 20:57:51 +08:00
Gabe Yuan
4486ad353c dev.....! 2023-08-31 13:38:06 +08:00
Gabe Yuan
aa795e2731 dev...... 2023-08-31 00:18:57 +08:00
Gabe Yuan
c46fe7d1c6 dev... 2023-08-30 18:05:37 +08:00
Gabe Yuan
d7cee8cca6 catch caches is not defined 2023-08-29 21:33:27 +08:00
Gabe Yuan
11f790ace5 catch caches is not defined 2023-08-29 21:24:25 +08:00
Gabe Yuan
13e7c1b754 fix safari webkit 2023-08-29 17:34:37 +08:00
Gabe Yuan
d314d5515f v1.5.8 2023-08-29 16:52:48 +08:00
Gabe Yuan
09b19e3ca0 fix webkit style in safari 2023-08-29 16:48:38 +08:00
Gabe Yuan
687bd11fd1 fix some text 2023-08-29 13:14:12 +08:00
Gabe Yuan
56cb1cd30d fix links 2023-08-29 11:53:02 +08:00
Gabe Yuan
7a3df25521 generate version file to web 2023-08-29 10:41:20 +08:00
Gabe Yuan
ea8919ba07 update readme 2023-08-29 10:30:18 +08:00
Gabe Yuan
3dece4fcdb add version tag to loading page 2023-08-29 01:35:09 +08:00
Gabe Yuan
df950a1bd2 use createElement script 2023-08-29 01:17:22 +08:00
Gabe Yuan
74b9ee31fa eslint-disable-line 2023-08-29 00:52:37 +08:00
Gabe Yuan
64cd55fe58 set no-eval 2023-08-29 00:42:11 +08:00
Gabe Yuan
e80ede14fb v1.5.7 2023-08-29 00:09:09 +08:00
Gabe Yuan
45ba9d3320 use inject-into replace unsafeWindow 2023-08-29 00:06:50 +08:00
Gabe Yuan
47c7048538 injectscript... 2023-08-28 17:59:51 +08:00
Gabe Yuan
f9bfa8101f fix detectLanguage 2023-08-28 11:14:03 +08:00
Gabe Yuan
620ac464eb v1.5.6 2023-08-27 17:59:47 +08:00
Gabe Yuan
62289f8ab8 catch detect lang err 2023-08-27 17:43:27 +08:00
Gabe Yuan
d84594da96 catch global error and display on top of page 2023-08-27 16:45:57 +08:00
Gabe Yuan
e1d74aae6a catch global error and display on top of page 2023-08-27 16:41:14 +08:00
Gabe Yuan
c4980d9eb7 fix rules 2023-08-26 22:12:48 +08:00
Gabe Yuan
882d83c6b7 update helper text 2023-08-26 15:08:21 +08:00
Gabe Yuan
c4a7fd81f8 v1.5.5 2023-08-26 14:47:15 +08:00
Gabe Yuan
0e55799109 fix sync test button 2023-08-26 14:42:50 +08:00
Gabe Yuan
a3cdcb2a1a add sync test button 2023-08-26 14:31:13 +08:00
Gabe Yuan
e0ccc298f9 add foxnews rule 2023-08-26 13:49:44 +08:00
Gabe Yuan
36b49bb577 modify fab opacity to 0.2 2023-08-26 13:45:24 +08:00
Gabe Yuan
2636c24e84 re translate when text changed 2023-08-26 13:10:13 +08:00
Gabe Yuan
6bcf294635 userscript in iframe 2023-08-26 12:11:21 +08:00
Gabe Yuan
c5fa6689a4 content script in iframe 2023-08-26 12:02:16 +08:00
Gabe Yuan
3bf0cb2485 usescript in iframe 2023-08-26 11:43:00 +08:00
Gabe Yuan
19c9335527 shadow root 2023-08-26 00:08:12 +08:00
Gabe Yuan
20da2e1b97 shadow root 2023-08-25 22:48:47 +08:00
Gabe Yuan
9eceb8641d shadow root 2023-08-25 22:48:11 +08:00
Gabe Yuan
86bc915d74 shadow root 2023-08-25 17:07:53 +08:00
Gabe Yuan
6b35525207 run script in iframe 2023-08-24 16:40:42 +08:00
Gabe Yuan
4633bf4fc6 run script in iframe 2023-08-24 16:39:35 +08:00
Gabe Yuan
2665f31d94 fix iframe bug 2023-08-24 16:21:01 +08:00
Gabe Yuan
6c4d3149eb fix shadow dom 2023-08-24 15:07:13 +08:00
Gabe Yuan
a2762e6ce6 fix shadow dom 2023-08-24 14:57:54 +08:00
Gabe Yuan
792a1bfcad Merge branch 'master' into dev 2023-08-24 10:10:26 +08:00
Gabe Yuan
a0eba9d60e update readme 2023-08-24 10:10:00 +08:00
Gabe Yuan
c2e0064253 support shadow dom 2023-08-23 18:01:47 +08:00
Gabe Yuan
f246efc84b support shadow dom 2023-08-23 17:53:46 +08:00
Gabe Yuan
4a3bf7e96c some minor modifications 2023-08-23 10:39:01 +08:00
Gabe Yuan
523b81090d min length & max length can be set 2023-08-22 21:45:23 +08:00
Gabe Yuan
d706c405d9 add shortcut text to pop page 2023-08-22 21:14:33 +08:00
Gabe Yuan
1191791447 v1.5.4 2023-08-22 17:52:12 +08:00
Gabe Yuan
5c510f2df2 add rules filter when add rule 2023-08-22 17:51:40 +08:00
Gabe Yuan
7c0aa23177 add rules filter when add rule 2023-08-22 17:46:57 +08:00
Gabe Yuan
4bc1c26653 add rules filter when add rule 2023-08-22 17:37:42 +08:00
Gabe Yuan
ca1e1148d6 sync subscribe rules when browser start or userscript run 2023-08-22 16:27:09 +08:00
Gabe Yuan
2224455a7f add text description for rules 2023-08-22 10:35:57 +08:00
Gabe Yuan
f463f3ce08 v1.5.3 2023-08-21 23:50:32 +08:00
Gabe Yuan
c0872db98c auto use unsafe fetch 2023-08-21 23:50:14 +08:00
Gabe Yuan
d3a5d91f01 auto use unsafe fetch 2023-08-21 23:46:42 +08:00
Gabe Yuan
3e9338be0e v1.5.2 2023-08-21 22:24:42 +08:00
Gabe Yuan
ef7f1ad638 fetch subrules use unsafe fetch 2023-08-21 21:35:53 +08:00
Gabe Yuan
1f10ebe404 fetch subrules use unsafe fetch 2023-08-21 21:31:20 +08:00
Gabe Yuan
f4a8251c61 add shortcut: Toggle Style 2023-08-21 16:06:21 +08:00
Gabe Yuan
f585a43480 v1.5.1 2023-08-21 14:52:57 +08:00
Gabe Yuan
3a11465c24 fix stack useFlexGap 2023-08-21 14:43:22 +08:00
Gabe Yuan
3c3ebdf96c add command shortcuts & menu command 2023-08-21 14:03:39 +08:00
Gabe Yuan
6b30f443e1 v1.5.0 2023-08-20 23:32:06 +08:00
Gabe Yuan
232e9a47a2 share rules 2023-08-20 23:30:08 +08:00
Gabe Yuan
7ec43a1d3f Subscribe Rules 2023-08-20 19:27:29 +08:00
Gabe Yuan
a8caa34bbe v1.4.6 2023-08-19 15:16:33 +08:00
Gabe Yuan
c2fd1fe9e0 fix storage bug 2023-08-19 13:48:03 +08:00
Gabe Yuan
2773a76af8 yarn install 2023-08-18 23:50:06 +08:00
Gabe Yuan
1dc7026e8f add rules generate script 2023-08-18 16:48:44 +08:00
Gabe Yuan
b36ede7393 fix userscript grant 2023-08-18 13:19:40 +08:00
Gabe Yuan
b18721a4e5 wildcard is supported 2023-08-18 13:16:17 +08:00
Gabe Yuan
01676bc682 fix fab at left default 2023-08-17 16:22:04 +08:00
103 changed files with 21202 additions and 16923 deletions

5
.babelrc Normal file
View File

@@ -0,0 +1,5 @@
{
"presets": [
"@babel/preset-env"
]
}

19
.env
View File

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

View File

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

View File

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

View File

@@ -1,62 +1,92 @@
## KISS Translator # 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) [kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f)
### Inspiration ## Features
The inspiration for this project comes from [Immersive Translate](https://github.com/immersive-translate/immersive-translate). After trying it out, I found that it can be used together with the [Webpage Word Translation Extension](https://github.com/fishjar/kiss-dictionary) developed by me earlier, which just forms a very good supplement. - [x] Keep it simple, smart
But the function of this extension is a bit complicated for me, and only the compiled and obfuscated installation package is provided, and the source code is not provided, which cannot meet some of my personalized customization needs.
It just so happens that I am obsessed with translation tools. Based on the concept of "mainly for personal use, as long as you can use it", I made one. At present, the first version is completed, which basically meets the needs of personal use.
If you also like a little more simplicity, welcome to pick it up.
### Features
- Keep it simple, smart
### Schedule
- [x] Provide trial installation package
- [x] Adapt browser
- [x] Chrome
- [x] Edge
- [x] Firefox
- [ ] Safari
- [x] Kiwi
- [x] Support translation services
- [x] Google
- [x] Microsoft
- [x] OpenAI
- [ ] DeepL
- [ ] Upload to app Store
- [x] [Chrome](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof)
- [ ] Edge
- [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] Open source - [x] Open source
- [x] Data Synchronization Function - [x] Adapt to common browsers
- [x] Greasemonkey Script ([link 1](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)、[link 2](https://kiss-translator.rayjar.com/kiss-translator.user.js)) - [x] Chrome/Edge/Firefox/Kiwi/Orion
- [x] [Tampermonkey](https://www.tampermonkey.net/) (Chrome/Edge/Firefox) - [ ] Safari
- [ ] [Userscripts Safari](https://github.com/quoid/userscripts) (need test) - [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
### Guide ## 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.
- Can also be used to share personal private rule lists.
- Deploy by yourself, manage by yourself, data is private.
- Community subscription rules: [https://github.com/fishjar/kiss-rules](https://github.com/fishjar/kiss-rules)
- Provides the latest and most complete list of subscription rules maintained by the community.
- Help with rules-related issues.
- Translation interface agent: [https://github.com/fishjar/kiss-proxy](https://github.com/fishjar/kiss-proxy)
- If you encounter network problems when accessing a certain translation interface, this proxy service may help you.
- Deploy and manage by yourself.
- Minimalistic Dictionary Plugin: [https://github.com/fishjar/kiss-dictionary](https://github.com/fishjar/kiss-dictionary)
- A word-marking translation plug-in used with this project.
- Supports query of English words, sentences and Chinese characters.
- Supports history records and word collections.
## Development Guidelines
```sh ```sh
git clone https://github.com/fishjar/kiss-translator.git git clone https://github.com/fishjar/kiss-translator.git
cd kiss-translator cd kiss-translator
yarn install pnpm install
yarn build pnpm build
``` ```
### Data Sync ## Discussion
Goto: [https://github.com/fishjar/kiss-worker](https://github.com/fishjar/kiss-worker)
### Discussion
- Join [Telegram Group](https://t.me/+RRCu_4oNwrM2NmFl) - Join [Telegram Group](https://t.me/+RRCu_4oNwrM2NmFl)
## Appreciate
![appreciate](https://github.com/fishjar/kiss-translator/assets/1157624/ebaecabe-2934-4172-8085-af236f5ee399)

124
README.md
View File

@@ -1,62 +1,92 @@
## 简约翻译 # 简约翻译
一个简约的 [双语网页翻译扩展 & 油猴脚本](https://github.com/fishjar/kiss-translator)。 [English](README.en.md) | 简体中文
一个简约、开源的 [双语对照翻译扩展 & 油猴脚本](https://github.com/fishjar/kiss-translator)。
[kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f) [kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f)
### 缘由 ## 特性
本项目灵感来源于 [Immersive Translate](https://github.com/immersive-translate/immersive-translate),在试用了后,发现搭配本人早前开发的 [网页划词翻译扩展](https://github.com/fishjar/kiss-dictionary) 一起使用,刚好形成很好补充。 - [x] 保持简约
但该扩展的功能对我来说有些繁杂了,而且只提供编译混淆后的安装包,没有提供源代码,无法满足我的一些个性化定制需求。
恰巧本人对翻译类工具有些执念,本着`“自用为主,能用就行”`的理念,于是动手撸了一个,目前初版完成,基本达到个人使用需求。
如果你也喜欢简约一点的,欢迎自取。
### 特点
- 保持简约
### 进度
- [x] 提供试用安装包
- [x] 适配浏览器
- [x] Chrome
- [x] Edge
- [x] Firefox
- [ ] Safari
- [x] Kiwi
- [x] 支持翻译服务
- [x] Google
- [x] Microsoft
- [x] OpenAI
- [ ] DeepL
- [ ] 上架应用市场
- [x] [Chrome](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof?hl=zh-CN)
- [ ] Edge
- [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] 数据同步功能 - [x] 适配常见浏览器
- [x] 油猴脚本([链接 1](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)、[链接 2](https://kiss-translator.rayjar.com/kiss-translator.user.js)) - [x] Chrome/Edge/Firefox/Kiwi/Orion
- [x] [Tampermonkey](https://www.tampermonkey.net/) (Chrome/Edge/Firefox) - [ ] Safari
- [ ] [Userscripts Safari](https://github.com/quoid/userscripts) (待测) - [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)
## 关联项目
- 数据同步服务: [https://github.com/fishjar/kiss-worker](https://github.com/fishjar/kiss-worker)
- 可用于本项目的数据同步服务。
- 亦可用于分享个人的私有规则列表。
- 自己部署,自己管理,数据私有。
- 社区订阅规则: [https://github.com/fishjar/kiss-rules](https://github.com/fishjar/kiss-rules)
- 提供社区维护的,最新最全的订阅规则列表。
- 求助规则相关的问题。
- 翻译接口代理: [https://github.com/fishjar/kiss-proxy](https://github.com/fishjar/kiss-proxy)
- 如果访问某个翻译接口遇到网络问题,这个代理服务也许可以帮到你。
- 自己部署,自己管理。
- 简约词典插件: [https://github.com/fishjar/kiss-dictionary](https://github.com/fishjar/kiss-dictionary)
- 搭配本项目一起使用的划词翻译插件。
- 支持英文单词、句子、汉字的查询。
- 支持历史记录、单词收藏。
## 开发指引
```sh ```sh
git clone https://github.com/fishjar/kiss-translator.git git clone https://github.com/fishjar/kiss-translator.git
cd kiss-translator cd kiss-translator
yarn install pnpm install
yarn build pnpm build
``` ```
### 数据同步 ## 交流
移步: [https://github.com/fishjar/kiss-worker](https://github.com/fishjar/kiss-worker)
### 交流
- 加入 [Telegram 群](https://t.me/+RRCu_4oNwrM2NmFl) - 加入 [Telegram 群](https://t.me/+RRCu_4oNwrM2NmFl)
## 赞赏
![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} // @name ${process.env.REACT_APP_NAME}
// @namespace ${process.env.REACT_APP_HOMEPAGE} // @namespace ${process.env.REACT_APP_HOMEPAGE}
// @version ${process.env.REACT_APP_VERSION} // @version ${process.env.REACT_APP_VERSION}
// @description A minimalist bilingual translation Extension & Greasemonkey Script (一个简约的双语网页翻译扩展 & 油猴脚本) // @description A simple bilingual translation extension & Greasemonkey script (一个简约的双语对照翻译扩展 & 油猴脚本)
// @author Gabe<yugang2002@gmail.com> // @author Gabe<yugang2002@gmail.com>
// @homepageURL ${process.env.REACT_APP_HOMEPAGE} // @homepageURL ${process.env.REACT_APP_HOMEPAGE}
// @license GPL-3.0 // @license GPL-3.0
@@ -83,21 +83,37 @@ const userscriptWebpack = (config, env) => {
// @icon ${process.env.REACT_APP_LOGOURL} // @icon ${process.env.REACT_APP_LOGOURL}
// @downloadURL ${process.env.REACT_APP_USERSCRIPT_DOWNLOADURL} // @downloadURL ${process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}
// @updateURL ${process.env.REACT_APP_USERSCRIPT_DOWNLOADURL} // @updateURL ${process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}
// @grant GM_xmlhttpRequest // @grant GM.xmlHttpRequest
// @grant GM.xmlhttpRequest // @grant GM.registerMenuCommand
// @grant GM_setValue // @grant GM.unregisterMenuCommand
// @grant GM.setValue // @grant GM.setValue
// @grant GM_getValue
// @grant GM.getValue // @grant GM.getValue
// @grant GM_deleteValue
// @grant GM.deleteValue // @grant GM.deleteValue
// @grant GM.info
// @grant unsafeWindow // @grant unsafeWindow
// @connect translate.googleapis.com // @connect translate.googleapis.com
// @connect api-edge.cognitive.microsofttranslator.com // @connect api-edge.cognitive.microsofttranslator.com
// @connect edge.microsoft.com // @connect edge.microsoft.com
// @connect api-free.deepl.com
// @connect api.deepl.com
// @connect www2.deepl.com
// @connect api.openai.com // @connect api.openai.com
// @connect generativelanguage.googleapis.com
// @connect openai.azure.com // @connect openai.azure.com
// @connect workers.dev // @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 // @run-at document-end
// ==/UserScript== // ==/UserScript==

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
"manifest_version": 2, "manifest_version": 2,
"name": "__MSG_app_name__", "name": "__MSG_app_name__",
"description": "__MSG_app_description__", "description": "__MSG_app_description__",
"version": "1.4.5", "version": "1.8.11",
"default_locale": "en", "default_locale": "en",
"author": "Gabe<yugang2002@gmail.com>", "author": "Gabe<yugang2002@gmail.com>",
"homepage_url": "https://github.com/fishjar/kiss-translator", "homepage_url": "https://github.com/fishjar/kiss-translator",
@@ -12,10 +12,45 @@
"content_scripts": [ "content_scripts": [
{ {
"js": ["content.js"], "js": ["content.js"],
"matches": ["<all_urls>"] "matches": ["<all_urls>"],
"all_frames": true
} }
], ],
"permissions": ["<all_urls>", "storage"], "commands": {
"_execute_browser_action": {
"suggested_key": {
"default": "Alt+K"
}
},
"toggleTranslate": {
"suggested_key": {
"default": "Alt+Q"
},
"description": "__MSG_toggle_translate__"
},
"openTranbox": {
"suggested_key": {
"default": "Alt+S"
},
"description": "__MSG_open_tranbox__"
},
"toggleStyle": {
"suggested_key": {
"default": "Alt+C"
},
"description": "__MSG_toggle_style__"
},
"openOptions": {
"description": "__MSG_open_options__"
}
},
"permissions": [
"<all_urls>",
"storage",
"contextMenus",
"scripting",
"declarativeNetRequest"
],
"icons": { "icons": {
"16": "images/logo16.png", "16": "images/logo16.png",
"32": "images/logo32.png", "32": "images/logo32.png",

View File

@@ -2,7 +2,7 @@
"manifest_version": 3, "manifest_version": 3,
"name": "__MSG_app_name__", "name": "__MSG_app_name__",
"description": "__MSG_app_description__", "description": "__MSG_app_description__",
"version": "1.4.5", "version": "1.8.11",
"default_locale": "en", "default_locale": "en",
"author": "Gabe<yugang2002@gmail.com>", "author": "Gabe<yugang2002@gmail.com>",
"homepage_url": "https://github.com/fishjar/kiss-translator", "homepage_url": "https://github.com/fishjar/kiss-translator",
@@ -13,10 +13,44 @@
"content_scripts": [ "content_scripts": [
{ {
"js": ["content.js"], "js": ["content.js"],
"matches": ["<all_urls>"] "matches": ["<all_urls>"],
"all_frames": true
} }
], ],
"permissions": ["storage"], "commands": {
"_execute_action": {
"suggested_key": {
"default": "Alt+K"
}
},
"toggleTranslate": {
"suggested_key": {
"default": "Alt+Q"
},
"description": "__MSG_toggle_translate__"
},
"openTranbox": {
"suggested_key": {
"default": "Alt+S"
},
"description": "__MSG_open_tranbox__"
},
"toggleStyle": {
"suggested_key": {
"default": "Alt+C"
},
"description": "__MSG_toggle_style__"
},
"openOptions": {
"description": "__MSG_open_options__"
}
},
"permissions": [
"storage",
"contextMenus",
"scripting",
"declarativeNetRequest"
],
"host_permissions": ["<all_urls>"], "host_permissions": ["<all_urls>"],
"icons": { "icons": {
"16": "images/logo16.png", "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,16 +1,43 @@
import queryString from "query-string"; import queryString from "query-string";
import { fetchPolyfill } from "../libs/fetch"; import { fetchData } from "../libs/fetch";
import { import {
OPT_TRANS_GOOGLE, OPT_TRANS_GOOGLE,
OPT_TRANS_MICROSOFT, 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,
URL_MICROSOFT_TRANS, 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_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_SPECIAL,
PROMPT_PLACE_FROM, OPT_LANGS_MICROSOFT,
PROMPT_PLACE_TO,
KV_HEADER_KEY,
} from "../config"; } from "../config";
import { getSetting, detectLang } from "../libs"; import { sha256 } from "../libs/utils";
import interpreter from "../libs/interpreter";
import { msAuth } from "../libs/auth";
/** /**
* 同步数据 * 同步数据
@@ -20,114 +47,153 @@ import { getSetting, detectLang } from "../libs";
* @returns * @returns
*/ */
export const apiSyncData = async (url, key, data) => export const apiSyncData = async (url, key, data) =>
fetchPolyfill( fetchData(url, {
url, headers: {
{ "Content-type": "application/json",
headers: { Authorization: `Bearer ${await sha256(key, KV_SALT_SYNC)}`,
"Content-type": "application/json",
[KV_HEADER_KEY]: key,
},
method: "POST",
body: JSON.stringify(data),
}, },
{ useUnsafe: true } method: "POST",
); body: JSON.stringify(data),
});
/** /**
* 谷歌翻译 * 下载数据
* @param {*} text * @param {*} url
* @param {*} to
* @param {*} from
* @returns * @returns
*/ */
const apiGoogleTranslate = async (translator, text, to, from) => { export const apiFetch = (url) => fetchData(url);
/**
* Google语言识别
* @param {*} text
* @returns
*/
export const apiGoogleLangdetect = async (text) => {
const params = { const params = {
client: "gtx", client: "gtx",
dt: "t", dt: "t",
dj: 1, dj: 1,
ie: "UTF-8", ie: "UTF-8",
sl: from, sl: "auto",
tl: to, tl: "zh-CN",
q: text, q: text,
}; };
const { googleUrl } = await getSetting(); const input = `${URL_GOOGLE_TRAN}?${queryString.stringify(params)}`;
const input = `${googleUrl}?${queryString.stringify(params)}`; const res = await fetchData(input, {
return fetchPolyfill( headers: {
input, "Content-type": "application/json",
{
headers: {
"Content-type": "application/json",
},
}, },
{ useCache: true, usePool: true, translator } useCache: true,
); });
return res.src;
}; };
/** /**
* 微软翻译 * Microsoft语言识别
* @param {*} text * @param {*} text
* @param {*} to
* @param {*} from
* @returns * @returns
*/ */
const apiMicrosoftTranslate = (translator, text, to, from) => { export const apiMicrosoftLangdetect = async (text) => {
const params = { const [token] = await msAuth();
from, const res = await fetchData(URL_MICROSOFT_LANGDETECT, {
to, headers: {
"api-version": "3.0", "Content-type": "application/json",
}; Authorization: `Bearer ${token}`,
const input = `${URL_MICROSOFT_TRANS}?${queryString.stringify(params)}`;
return fetchPolyfill(
input,
{
headers: {
"Content-type": "application/json",
},
method: "POST",
body: JSON.stringify([{ Text: text }]),
}, },
{ useCache: true, usePool: true, translator } method: "POST",
); body: JSON.stringify([{ Text: text }]),
useCache: true,
});
return OPT_LANGS_MICROSOFT.get(res[0].language) ?? res[0].language;
}; };
/** /**
* OpenAI 翻译 * 百度语言识别
* @param {*} text * @param {*} text
* @param {*} to
* @param {*} from
* @returns * @returns
*/ */
const apiOpenaiTranslate = async (translator, text, to, from) => { export const apiBaiduLangdetect = async (text) => {
const { openaiUrl, openaiKey, openaiModel, openaiPrompt } = const res = await fetchData(URL_BAIDU_LANGDETECT, {
await getSetting(); headers: {
let prompt = openaiPrompt "Content-type": "application/json",
.replaceAll(PROMPT_PLACE_FROM, from)
.replaceAll(PROMPT_PLACE_TO, to);
return fetchPolyfill(
openaiUrl,
{
headers: {
"Content-type": "application/json",
},
method: "POST",
body: JSON.stringify({
model: openaiModel,
messages: [
{
role: "system",
content: prompt,
},
{
role: "user",
content: text,
},
],
temperature: 0,
max_tokens: 256,
}),
}, },
{ useCache: true, usePool: true, translator, token: openaiKey } method: "POST",
); body: JSON.stringify({
query: text,
}),
useCache: true,
});
if (res.error === 0) {
return OPT_LANGS_BAIDU.get(res.lan) ?? res.lan;
}
return "";
};
/**
* 百度翻译建议
* @param {*} text
* @returns
*/
export const apiBaiduSuggest = async (text) => {
const res = await fetchData(URL_BAIDU_SUGGEST, {
headers: {
"Content-type": "application/json",
},
method: "POST",
body: JSON.stringify({
kw: text,
}),
useCache: true,
});
if (res.errno === 0) {
return res.data;
}
return [];
};
/**
* 百度语音
* @param {*} text
* @param {*} lan
* @param {*} spd
* @returns
*/
export const apiBaiduTTS = (text, lan = "uk", spd = 3) => {
const url = `${URL_BAIDU_TTS}?${queryString.stringify({ lan, text, spd })}`;
return fetchData(url, {
useCache: false, // 为避免缓存过快增长,禁用缓存语音数据
});
};
/**
* 腾讯语言识别
* @param {*} text
* @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,
useCache: true,
});
return OPT_LANGS_TENCENT.get(res.language) ?? res.language;
}; };
/** /**
@@ -135,26 +201,142 @@ const apiOpenaiTranslate = async (translator, text, to, from) => {
* @param {*} param0 * @param {*} param0
* @returns * @returns
*/ */
export const apiTranslate = async ({ translator, q, fromLang, toLang }) => { export const apiTranslate = async ({
translator,
text,
fromLang,
toLang,
apiSetting = {},
useCache = true,
usePool = true,
}) => {
let trText = ""; let trText = "";
let isSame = false; let isSame = false;
let from = OPT_LANGS_SPECIAL?.[translator]?.get(fromLang) ?? fromLang; if (!text) {
let to = OPT_LANGS_SPECIAL?.[translator]?.get(toLang) ?? toLang; return [trText, true];
if (translator === OPT_TRANS_GOOGLE) {
const res = await apiGoogleTranslate(translator, q, to, from);
trText = res.sentences.map((item) => item.trans).join(" ");
isSame = to === res.src;
} else if (translator === OPT_TRANS_MICROSOFT) {
const res = await apiMicrosoftTranslate(translator, q, to, from);
trText = res[0].translations[0].text;
isSame = to === res[0].detectedLanguage.language;
} else if (translator === OPT_TRANS_OPENAI) {
const res = await apiOpenaiTranslate(translator, q, to, from);
trText = res?.choices?.[0].message.content;
isSame = (await detectLang(q)) === (await detectLang(trText));
} }
return [trText, isSame]; 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:
trText = res.sentences.map((item) => item.trans).join(" ");
isSame = to === res.src;
break;
case OPT_TRANS_MICROSOFT:
trText = res
.map((item) => item.translations.map((item) => item.text).join(" "))
.join(" ");
isSame = text === trText;
break;
case OPT_TRANS_DEEPL:
trText = res.translations.map((item) => item.text).join(" ");
isSame = to === res.translations[0].detected_source_language;
break;
case OPT_TRANS_DEEPLFREE:
trText = res.result?.texts.map((item) => item.text).join(" ");
isSame = to === res.result?.lang;
break;
case OPT_TRANS_DEEPLX:
trText = res.data;
isSame = to === res.source_lang;
break;
case OPT_TRANS_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:
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:
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 [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,75 +1,245 @@
import browser from "webextension-polyfill"; import browser from "webextension-polyfill";
import { import {
MSG_FETCH, MSG_FETCH,
MSG_FETCH_LIMIT, MSG_GET_HTTPCACHE,
MSG_FETCH_CLEAR, MSG_TRANS_TOGGLE,
DEFAULT_SETTING, MSG_OPEN_OPTIONS,
DEFAULT_RULES, MSG_SAVE_RULE,
DEFAULT_SYNC, MSG_TRANS_TOGGLE_STYLE,
STOKEY_SETTING, MSG_OPEN_TRANBOX,
STOKEY_RULES, MSG_CONTEXT_MENUS,
STOKEY_SYNC, MSG_COMMAND_SHORTCUTS,
CACHE_NAME, 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"; } from "./config";
import storage from "./libs/storage"; import { getSettingWithDefault, tryInitDefaultData } from "./libs/storage";
import { getSetting } from "./libs"; import { trySyncSettingAndRules } from "./libs/sync";
import { syncAll } from "./libs/sync"; import { fetchHandle, getHttpCache } from "./libs/fetch";
import { fetchData, fetchPool } 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(() => { browser.runtime.onInstalled.addListener(() => {
console.log("KISS Translator onInstalled"); tryInitDefaultData();
storage.trySetObj(STOKEY_SETTING, DEFAULT_SETTING);
storage.trySetObj(STOKEY_RULES, DEFAULT_RULES); // 右键菜单
storage.trySetObj(STOKEY_SYNC, DEFAULT_SYNC); addContextMenus();
// 禁用CSP
updateCspRules();
}); });
/** /**
* 浏览器启动 * 浏览器启动
*/ */
browser.runtime.onStartup.addListener(async () => { browser.runtime.onStartup.addListener(async () => {
console.log("browser onStartup");
// 同步数据 // 同步数据
await syncAll(); await trySyncSettingAndRules();
const { clearCache, contextMenuType, subrulesList, csplist } =
await getSettingWithDefault();
// 清除缓存 // 清除缓存
const { clearCache } = await getSetting();
if (clearCache) { if (clearCache) {
caches.delete(CACHE_NAME); tryClearCaches();
} }
// 右键菜单
// firefox重启后菜单会消失,故重复添加
addContextMenus(contextMenuType);
// 禁用CSP
updateCspRules(csplist);
// 同步订阅规则
trySyncAllSubRules({ subrulesList });
}); });
/** /**
* 监听消息 * 监听消息
*/ */
browser.runtime.onMessage.addListener( browser.runtime.onMessage.addListener(async ({ action, args }) => {
({ action, args }, sender, sendResponse) => { switch (action) {
switch (action) { case MSG_FETCH:
case MSG_FETCH: return await fetchHandle(args);
const { input, init, opts } = args; case MSG_GET_HTTPCACHE:
fetchData(input, init, opts) const { input, init } = args;
.then((data) => { return await getHttpCache(input, init);
sendResponse({ data }); case MSG_OPEN_OPTIONS:
}) return await browser.runtime.openOptionsPage();
.catch((error) => { case MSG_SAVE_RULE:
sendResponse({ error: error.message }); return await saveRule(args);
}); case MSG_INJECT_JS:
break; return await browser.scripting.executeScript({
case MSG_FETCH_LIMIT: target: { tabId: await getCurTabId(), allFrames: true },
const { interval, limit } = args; func: injectInlineJs,
fetchPool.update(interval, limit); args: [args],
sendResponse({ data: "ok" }); world: "MAIN",
break; });
case MSG_FETCH_CLEAR: case MSG_INJECT_CSS:
fetchPool.clear(); return await browser.scripting.executeScript({
sendResponse({ data: "ok" }); target: { tabId: await getCurTabId(), allFrames: true },
break; func: injectInternalCss,
default: args: [args],
sendResponse({ error: `message action is unavailable: ${action}` }); world: "MAIN",
} });
return true; 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}`);
} }
); });
/**
* 监听快捷键
*/
browser.commands.onCommand.addListener((command) => {
// console.log(`Command: ${command}`);
switch (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);
}
}

4
src/config/app.js Normal file
View File

@@ -0,0 +1,4 @@
export const APP_NAME = process.env.REACT_APP_NAME.trim()
.split(/\s+/)
.join("-");
export const APP_LCNAME = APP_NAME.toLowerCase();

View File

@@ -3,6 +3,124 @@ export const UI_LANGS = [
["zh", "中文"], ["zh", "中文"],
]; ];
const customApiLangs = `["en", "English - English"],
["zh-CN", "Simplified Chinese - 简体中文"],
["zh-TW", "Traditional Chinese - 繁體中文"],
["ar", "Arabic - العربية"],
["bg", "Bulgarian - Български"],
["ca", "Catalan - Català"],
["hr", "Croatian - Hrvatski"],
["cs", "Czech - Čeština"],
["da", "Danish - Dansk"],
["nl", "Dutch - Nederlands"],
["fi", "Finnish - Suomi"],
["fr", "French - Français"],
["de", "German - Deutsch"],
["el", "Greek - Ελληνικά"],
["hi", "Hindi - हिन्दी"],
["hu", "Hungarian - Magyar"],
["id", "Indonesian - Indonesia"],
["it", "Italian - Italiano"],
["ja", "Japanese - 日本語"],
["ko", "Korean - 한국어"],
["ms", "Malay - Melayu"],
["mt", "Maltese - Malti"],
["nb", "Norwegian - Norsk Bokmål"],
["pl", "Polish - Polski"],
["pt", "Portuguese - Português"],
["ro", "Romanian - Română"],
["ru", "Russian - Русский"],
["sk", "Slovak - Slovenčina"],
["sl", "Slovenian - Slovenščina"],
["es", "Spanish - Español"],
["sv", "Swedish - Svenska"],
["ta", "Tamil - தமிழ்"],
["te", "Telugu - తెలుగు"],
["th", "Thai - ไทย"],
["tr", "Turkish - Türkçe"],
["uk", "Ukrainian - Українська"],
["vi", "Vietnamese - Tiếng Việt"],
`;
const 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",
},
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}}", // 目标语言
},
}
// 返回数据默认格式
{
text: "", // 翻译后的文字
from: "", // 识别的源语言
to: "", // 目标语言(可选)
}
// Hook 范例
${hookExample}
// 支持的语言代码如下
${customApiLangs}
`;
const customApiHelpEN = `// Default request
{
"url": "{{url}}",
"method": "POST",
"headers": {
"Content-type": "application/json",
"Authorization": "Bearer {{key}}"
},
"body": {
"text": "{{text}}", // Text to be translated
"from": "{{from}}", // The language of the text (may be empty)
"to": "{{to}}", // Target language
},
}
// Default response
{
text: "", // translated text
from: "", // Recognized source language
to: "", // Target language (optional)
}
/// Hook Example
${hookExample}
// The supported language codes are as follows
${customApiLangs}
`;
export const I18N = { export const I18N = {
app_name: { app_name: {
zh: `简约翻译`, zh: `简约翻译`,
@@ -12,6 +130,14 @@ export const I18N = {
zh: `翻译`, zh: `翻译`,
en: `Translate`, en: `Translate`,
}, },
custom_api_help: {
zh: customApiHelpZH,
en: customApiHelpEN,
},
translate_alt: {
zh: `翻译`,
en: `Translate`,
},
basic_setting: { basic_setting: {
zh: `基本设置`, zh: `基本设置`,
en: `Basic Setting`, en: `Basic Setting`,
@@ -20,10 +146,26 @@ export const I18N = {
zh: `规则设置`, zh: `规则设置`,
en: `Rules Setting`, en: `Rules Setting`,
}, },
apis_setting: {
zh: `接口设置`,
en: `Apis Setting`,
},
sync_setting: { sync_setting: {
zh: `同步设置`, zh: `同步设置`,
en: `Sync Setting`, en: `Sync Setting`,
}, },
patch_setting: {
zh: `补丁设置`,
en: `Patch Setting`,
},
patch_setting_help: {
zh: `针对一些特殊网站的修正脚本,以便翻译软件得到更好的展示效果。`,
en: `Corrected scripts for some special websites so that the translation software can get better display results.`,
},
inject_webfix: {
zh: `注入修复补丁`,
en: `Inject Webfix`,
},
about: { about: {
zh: `关于`, zh: `关于`,
en: `About`, en: `About`,
@@ -41,17 +183,61 @@ export const I18N = {
en: `Interface Language`, en: `Interface Language`,
}, },
fetch_limit: { fetch_limit: {
zh: `最大请求数量`, zh: `最大并发请求数量 (1-100)`,
en: `Maximum Number Of Request`, en: `Maximum Number Of Concurrent Requests (1-100)`,
}, },
fetch_interval: { fetch_interval: {
zh: `请求间隔时间(ms)`, zh: `每次请求间隔时间 (0-5000ms)`,
en: `Request Interval(ms)`, en: `Time Between Requests (0-5000ms)`,
},
translate_interval: {
zh: `重新翻译间隔时间 (100-5000ms)`,
en: `Retranslation Interval (100-5000ms)`,
},
min_translate_length: {
zh: `最小翻译字符数 (1-100)`,
en: `Minimum number Of Translated Characters (1-100)`,
},
max_translate_length: {
zh: `最大翻译字符数 (100-10000)`,
en: `Maximum number Of Translated Characters (100-10000)`,
},
num_of_newline_characters: {
zh: `换行字符数 (1-1000)`,
en: `Number of Newline Characters (1-1000)`,
}, },
translate_service: { translate_service: {
zh: `翻译服务`, zh: `翻译服务`,
en: `Translate Service`, en: `Translate Service`,
}, },
translate_timing: {
zh: `翻译时机`,
en: `Translate Timing`,
},
mk_pagescroll: {
zh: `滚动加载翻译(推荐)`,
en: `Rolling Loading (Suggested)`,
},
mk_pageopen: {
zh: `页面打开全部翻译`,
en: `Page Open`,
},
mk_mouseover: {
zh: `鼠标悬停翻译`,
en: `Mouseover`,
},
mk_ctrlKey: {
zh: `Control + 鼠标悬停`,
en: `Control + Mouseover`,
},
mk_shiftKey: {
zh: `Shift + 鼠标悬停`,
en: `Shift + Mouseover`,
},
mk_altKey: {
zh: `Alt + 鼠标悬停`,
en: `Alt + Mouseover`,
},
from_lang: { from_lang: {
zh: `原文语言`, zh: `原文语言`,
en: `Source Language`, en: `Source Language`,
@@ -60,14 +246,30 @@ export const I18N = {
zh: `目标语言`, zh: `目标语言`,
en: `Target Language`, en: `Target Language`,
}, },
to_lang2: {
zh: `第二目标语言`,
en: `Target Language 2`,
},
to_lang2_helper: {
zh: `设定后,与目标语言产生互译效果,但依赖远程语言识别。`,
en: `After setting, it will produce mutual translation effect with the target language, but it relies on remote language recognition.`,
},
text_style: { text_style: {
zh: `样式`, zh: `文样式`,
en: `Text Style`,
},
text_style_alt: {
zh: `译文样式`,
en: `Text Style`, en: `Text Style`,
}, },
bg_color: { bg_color: {
zh: `样式颜色`, zh: `样式颜色`,
en: `Style Color`, en: `Style Color`,
}, },
remain_unchanged: {
zh: `保留不变`,
en: `Remain Unchanged`,
},
google_api: { google_api: {
zh: `谷歌翻译接口`, zh: `谷歌翻译接口`,
en: `Google Translate API`, en: `Google Translate API`,
@@ -105,8 +307,36 @@ export const I18N = {
en: `Add`, en: `Add`,
}, },
inject_rules: { inject_rules: {
zh: `注入内置规则`, zh: `注入订阅规则`,
en: `Inject Built-in Rules`, en: `Inject Subscribe Rules`,
},
personal_rules: {
zh: `个人规则`,
en: `Rules`,
},
subscribe_rules: {
zh: `订阅规则`,
en: `Subscribe`,
},
overwrite_subscribe_rules: {
zh: `覆写订阅规则`,
en: `Overwrite`,
},
subscribe_url: {
zh: `订阅地址`,
en: `Subscribe URL`,
},
rules_warn_1: {
zh: `1、“个人规则”一直生效选择“注入订阅规则”后“订阅规则”才会生效。`,
en: `1. The "Personal Rules" are always in effect. After selecting "Inject Subscription Rules", the "Subscription Rules" will take effect.`,
},
rules_warn_2: {
zh: `2、“订阅规则”的注入位置是倒数第二的位置因此除全局规则(*)外,“个人规则”优先级比“订阅规则”高,“个人规则”填写同样的网址会覆盖”订阅规则“的条目。`,
en: `2. The injection position of "Subscription Rules" is the penultimate position. Therefore, except for the global rules (*), the priority of "Personal Rules" is higher than that of "Subscription Rules". Filling in the same url in "Personal Rules" will overwrite "Subscription Rules" entry.`,
},
rules_warn_3: {
zh: `3、关于规则填写输入框留空或下拉框选“*”表示采用全局规则。`,
en: `3. Regarding filling in the rules: Leave the input box blank or select "*" in the drop-down box to use global rule.`,
}, },
sync_warn: { sync_warn: {
zh: `如果服务器存在其他客户端同步的数据,第一次同步将直接覆盖本地配置,后面则根据修改时间,新的覆盖旧的。`, zh: `如果服务器存在其他客户端同步的数据,第一次同步将直接覆盖本地配置,后面则根据修改时间,新的覆盖旧的。`,
@@ -116,6 +346,10 @@ export const I18N = {
zh: `查看关于数据同步接口部署`, zh: `查看关于数据同步接口部署`,
en: `View About Data Synchronization Interface Deployment`, en: `View About Data Synchronization Interface Deployment`,
}, },
about_api_proxy: {
zh: `查看自建一个翻译接口代理`,
en: `Check out the self-built translation interface proxy`,
},
style_none: { style_none: {
zh: ``, zh: ``,
en: `None`, en: `None`,
@@ -144,6 +378,18 @@ export const I18N = {
zh: `高亮`, zh: `高亮`,
en: `Highlight`, en: `Highlight`,
}, },
blockquote: {
zh: `引用`,
en: `Blockquote`,
},
diy_style: {
zh: `自定义样式`,
en: `Custom Style`,
},
diy_style_helper: {
zh: `遵循“CSS”的语法`,
en: `Follow the syntax of "CSS"`,
},
setting: { setting: {
zh: `设置`, zh: `设置`,
en: `Setting`, en: `Setting`,
@@ -153,12 +399,12 @@ export const I18N = {
en: `URL pattern`, en: `URL pattern`,
}, },
pattern_helper: { pattern_helper: {
zh: `多个URL支持英文逗号“,”分隔`, zh: `1、支持星号(*)通配符。2、多个URL用换行或英文逗号“,”分隔`,
en: `Multiple URLs can be separated by English commas ","`, en: `1. Supports the asterisk (*) wildcard character. 2. Separate multiple URLs with newlines or English commas ",".`,
}, },
selector_helper: { selector_helper: {
zh: `1、遵循CSS选择器规则。2、留空表示采用全局设置`, zh: `1、遵循CSS选择器语法。2、多个CSS选择器之间用“;”隔开。3、“shadow root”选择器和内部选择器用“>>>”隔开`,
en: `1. Follow CSS selector rules. 2. Leave blank to adopt the global setting.`, en: `1. Follow CSS selector syntax. 2. Separate multiple CSS selectors with ";". 3. The "shadow root" selector and the internal selector are separated by ">>>".`,
}, },
translate_switch: { translate_switch: {
zh: `开启翻译`, zh: `开启翻译`,
@@ -176,6 +422,62 @@ export const I18N = {
zh: `选择器`, zh: `选择器`,
en: `Selector`, 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: { import: {
zh: `导入`, zh: `导入`,
en: `Import`, en: `Import`,
@@ -184,6 +486,10 @@ export const I18N = {
zh: `导出`, zh: `导出`,
en: `Export`, en: `Export`,
}, },
export_translation: {
zh: `导出释义`,
en: `Export Translation`,
},
error_cant_be_blank: { error_cant_be_blank: {
zh: `不能为空`, zh: `不能为空`,
en: `Can not be blank`, en: `Can not be blank`,
@@ -196,6 +502,18 @@ export const I18N = {
zh: `错误的文件类型`, zh: `错误的文件类型`,
en: `Wrong file type`, en: `Wrong file type`,
}, },
error_fetch_url: {
zh: `请检查url地址是否正确或稍后再试。`,
en: `Please check if the url address is correct or try again later.`,
},
deepl_api: {
zh: `DeepL 接口`,
en: `DeepL API`,
},
deepl_key: {
zh: `DeepL 密钥`,
en: `DeepL Key`,
},
openai_api: { openai_api: {
zh: `OpenAI 接口`, zh: `OpenAI 接口`,
en: `OpenAI API`, en: `OpenAI API`,
@@ -212,7 +530,7 @@ export const I18N = {
zh: `OpenAI 提示词`, zh: `OpenAI 提示词`,
en: `OpenAI Prompt`, en: `OpenAI Prompt`,
}, },
clear_cache: { if_clear_cache: {
zh: `是否清除缓存`, zh: `是否清除缓存`,
en: `Whether clear cache`, en: `Whether clear cache`,
}, },
@@ -224,12 +542,396 @@ export const I18N = {
zh: `重启浏览器时清除缓存`, zh: `重启浏览器时清除缓存`,
en: `Clear cache when restarting browser`, en: `Clear cache when restarting browser`,
}, },
data_sync_type: {
zh: `数据同步方式`,
en: `Data Sync Type`,
},
data_sync_url: { data_sync_url: {
zh: `数据同步接口`, zh: `数据同步接口`,
en: `Data Sync API`, en: `Data Sync API`,
}, },
data_sync_user: {
zh: `数据同步账户`,
en: `Data Sync User`,
},
data_sync_key: { data_sync_key: {
zh: `数据同步密钥`, zh: `数据同步密钥`,
en: `Data Sync Key`, en: `Data Sync Key`,
}, },
sync_now: {
zh: `立即同步`,
en: `Sync Now`,
},
sync_success: {
zh: `同步成功!`,
en: `Sync Success`,
},
sync_failed: {
zh: `同步失败!`,
en: `Sync Error`,
},
error_got_some_wrong: {
zh: `抱歉,出错了!`,
en: `Sorry, something went wrong!`,
},
error_sync_setting: {
zh: `您的同步类型必须为“KISS-Worker”且需填写完整`,
en: `Your sync type must be "KISS-Worker" and must be filled in completely`,
},
click_test: {
zh: `点击测试`,
en: `Click Test`,
},
test_success: {
zh: `测试成功`,
en: `Test success`,
},
test_failed: {
zh: `测试失败`,
en: `Test failed`,
},
clear_all_cache_now: {
zh: `立即清除全部缓存`,
en: `Clear all cache now`,
},
clear_cache: {
zh: `清除缓存`,
en: `Clear Cache`,
},
clear_success: {
zh: `清除成功`,
en: `Clear success`,
},
clear_failed: {
zh: `清除失败`,
en: `Clear failed`,
},
share: {
zh: `分享`,
en: `Share`,
},
clear_all: {
zh: `清空`,
en: `Clear All`,
},
help: {
zh: `求助`,
en: `Help`,
},
restore_default: {
zh: `恢复默认`,
en: `Restore Default`,
},
shortcuts_setting: {
zh: `快捷键设置`,
en: `Shortcuts Setting`,
},
toggle_translate_shortcut: {
zh: `"开启翻译"快捷键`,
en: `"Toggle Translate" Shortcut`,
},
toggle_style_shortcut: {
zh: `"切换样式"快捷键`,
en: `"Toggle Style" Shortcut`,
},
toggle_popup_shortcut: {
zh: `"打开弹窗"快捷键`,
en: `"Open Popup" Shortcut`,
},
open_setting_shortcut: {
zh: `"打开设置"快捷键`,
en: `"Open Setting" Shortcut`,
},
hide_fab_button: {
zh: `隐藏悬浮按钮`,
en: `Hide Fab Button`,
},
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`,
},
hide: {
zh: `隐藏`,
en: `Hide`,
},
save_rule: {
zh: `保存规则`,
en: `Save Rule`,
},
global_rule: {
zh: `全局规则`,
en: `Global Rule`,
},
input_translate: {
zh: `输入框翻译`,
en: `Input Box Translation`,
},
use_input_box_translation: {
zh: `启用输入框翻译`,
en: `Input Box Translation`,
},
input_selector: {
zh: `输入框选择器`,
en: `Input Selector`,
},
input_selector_helper: {
zh: `用于输入框翻译。`,
en: `Used for input box translation.`,
},
trigger_trans_shortcut: {
zh: `触发翻译快捷键`,
en: `Trigger Translation Shortcut Keys`,
},
trigger_trans_shortcut_help: {
zh: `默认为单击“AltLeft+KeyI”`,
en: `Default is "AltLeft+KeyI"`,
},
shortcut_press_count: {
zh: `快捷键连击次数`,
en: `Shortcut Press Number`,
},
combo_timeout: {
zh: `连击超时时间 (10-1000ms)`,
en: `Combo Timeout (10-1000ms)`,
},
input_trans_start_sign: {
zh: `翻译起始标识`,
en: `Translation Start Sign`,
},
input_trans_start_sign_help: {
zh: `标识后面可以加目标语言代码,如: “/en 你好”、“/zh hello”`,
en: `The target language code can be added after the sign, such as: "/en 你好", "/zh hello"`,
},
detect_lang_remote: {
zh: `远程语言检测`,
en: `Remote language detection`,
},
detect_lang_remote_help: {
zh: `启用后检测准确度增加,但会降低翻译速度,请酌情开启`,
en: `After enabling, the detection accuracy will increase, but it will reduce the translation speed. Please enable it as appropriate.`,
},
disable: {
zh: `禁用`,
en: `Disable`,
},
enable: {
zh: `启用`,
en: `Enable`,
},
selection_translate: {
zh: `划词翻译`,
en: `Selection Translate`,
},
toggle_selection_translate: {
zh: `启用划词翻译`,
en: `Use Selection Translate`,
},
trigger_tranbox_shortcut: {
zh: `显示翻译框/翻译选中文字快捷键`,
en: `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,17 +1,38 @@
import { DEFAULT_SELECTOR, RULES } from "./rules"; import {
DEFAULT_SELECTOR,
DEFAULT_KEEP_SELECTOR,
GLOBAL_KEY,
REMAIN_KEY,
SHADOW_KEY,
DEFAULT_RULE,
DEFAULT_OW_RULE,
BUILTIN_RULES,
} from "./rules";
import { APP_NAME, APP_LCNAME } from "./app";
export { I18N, UI_LANGS } from "./i18n"; export { I18N, UI_LANGS } from "./i18n";
export {
const APP_NAME = process.env.REACT_APP_NAME.trim().split(/\s+/).join("-"); GLOBAL_KEY,
REMAIN_KEY,
export const APP_LCNAME = APP_NAME.toLowerCase(); SHADOW_KEY,
DEFAULT_RULE,
DEFAULT_OW_RULE,
BUILTIN_RULES,
APP_LCNAME,
};
export const STOKEY_MSAUTH = `${APP_NAME}_msauth`; export const STOKEY_MSAUTH = `${APP_NAME}_msauth`;
export const STOKEY_BDAUTH = `${APP_NAME}_bdauth`;
export const STOKEY_SETTING = `${APP_NAME}_setting`; export const STOKEY_SETTING = `${APP_NAME}_setting`;
export const STOKEY_RULES = `${APP_NAME}_rules`; export const STOKEY_RULES = `${APP_NAME}_rules`;
export const STOKEY_WORDS = `${APP_NAME}_words`;
export const STOKEY_SYNC = `${APP_NAME}_sync`; export const STOKEY_SYNC = `${APP_NAME}_sync`;
export const STOKEY_FAB = `${APP_NAME}_fab`; export const STOKEY_FAB = `${APP_NAME}_fab`;
export const STOKEY_RULESCACHE_PREFIX = `${APP_NAME}_rulescache_`;
export const GLOBAL_KEY = "*"; 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_WEB = "web";
export const CLIENT_CHROME = "chrome"; export const CLIENT_CHROME = "chrome";
@@ -20,39 +41,119 @@ export const CLIENT_FIREFOX = "firefox";
export const CLIENT_USERSCRIPT = "userscript"; export const CLIENT_USERSCRIPT = "userscript";
export const CLIENT_EXTS = [CLIENT_CHROME, CLIENT_EDGE, CLIENT_FIREFOX]; export const CLIENT_EXTS = [CLIENT_CHROME, CLIENT_EDGE, CLIENT_FIREFOX];
export const KV_HEADER_KEY = "X-KISS-PSK"; export const KV_RULES_KEY = "kiss-rules.json";
export const KV_RULES_KEY = "KT_RULES"; export const KV_WORDS_KEY = "kiss-words.json";
export const KV_SETTING_KEY = "KT_SETTING"; export const KV_RULES_SHARE_KEY = "kiss-rules-share.json";
export const KV_SETTING_KEY = "kiss-setting.json";
export const KV_SALT_SYNC = "KISS-Translator-SYNC";
export const KV_SALT_SHARE = "KISS-Translator-SHARE";
export const CACHE_NAME = `${APP_NAME}_cache`; export const CACHE_NAME = `${APP_NAME}_cache`;
export const MSG_FETCH = "fetch"; export const MSG_FETCH = "fetch";
export const MSG_FETCH_LIMIT = "fetch_limit"; export const MSG_GET_HTTPCACHE = "get_httpcache";
export const MSG_FETCH_CLEAR = "fetch_clear"; export const MSG_OPEN_OPTIONS = "open_options";
export const MSG_SAVE_RULE = "save_rule";
export const MSG_TRANS_TOGGLE = "trans_toggle"; export const MSG_TRANS_TOGGLE = "trans_toggle";
export const MSG_TRANS_TOGGLE_STYLE = "trans_toggle_style";
export const MSG_OPEN_TRANBOX = "open_tranbox";
export const MSG_TRANS_GETRULE = "trans_getrule"; export const MSG_TRANS_GETRULE = "trans_getrule";
export const MSG_TRANS_PUTRULE = "trans_putrule"; export const MSG_TRANS_PUTRULE = "trans_putrule";
export const MSG_TRANS_CURRULE = "trans_currule"; export const MSG_TRANS_CURRULE = "trans_currule";
export const MSG_CONTEXT_MENUS = "context_menus";
export const EVENT_KISS = "kissEvent"; export const MSG_COMMAND_SHORTCUTS = "command_shortcuts";
export const MSG_INJECT_JS = "inject_js";
export const MSG_INJECT_CSS = "inject_css";
export const MSG_UPDATE_CSP = "update_csp";
export const THEME_LIGHT = "light"; export const THEME_LIGHT = "light";
export const THEME_DARK = "dark"; export const THEME_DARK = "dark";
export const URL_KISS_WORKER = "https://github.com/fishjar/kiss-worker"; export const URL_KISS_WORKER = "https://github.com/fishjar/kiss-worker";
export const URL_KISS_PROXY = "https://github.com/fishjar/kiss-proxy";
export const URL_KISS_RULES = "https://github.com/fishjar/kiss-rules";
export const URL_KISS_RULES_NEW_ISSUE =
"https://github.com/fishjar/kiss-rules/issues/new";
export const URL_RAW_PREFIX = export const URL_RAW_PREFIX =
"https://raw.githubusercontent.com/fishjar/kiss-translator/master"; "https://raw.githubusercontent.com/fishjar/kiss-translator/master";
export const URL_MICROSOFT_AUTH = "https://edge.microsoft.com/translate/auth";
export const URL_MICROSOFT_TRANS = export const URL_CACHE_TRAN = `https://${APP_LCNAME}/translate`;
// api.cognitive.microsofttranslator.com
export const URL_MICROSOFT_TRAN =
"https://api-edge.cognitive.microsofttranslator.com/translate"; "https://api-edge.cognitive.microsofttranslator.com/translate";
export const URL_MICROSOFT_AUTH = "https://edge.microsoft.com/translate/auth";
export const URL_MICROSOFT_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_GOOGLE = "Google";
export const OPT_TRANS_MICROSOFT = "Microsoft"; export const OPT_TRANS_MICROSOFT = "Microsoft";
export const OPT_TRANS_DEEPL = "DeepL";
export const OPT_TRANS_DEEPLX = "DeepLX";
export const OPT_TRANS_DEEPLFREE = "DeepLFree";
export const OPT_TRANS_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 = "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 = [ export const OPT_TRANS_ALL = [
OPT_TRANS_GOOGLE, OPT_TRANS_GOOGLE,
OPT_TRANS_MICROSOFT, OPT_TRANS_MICROSOFT,
OPT_TRANS_BAIDU,
OPT_TRANS_TENCENT,
OPT_TRANS_DEEPL,
OPT_TRANS_DEEPLFREE,
OPT_TRANS_DEEPLX,
OPT_TRANS_NIUTRANS,
OPT_TRANS_OPENAI, 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 = [ export const OPT_LANGS_TO = [
@@ -96,15 +197,162 @@ export const OPT_LANGS_TO = [
]; ];
export const OPT_LANGS_FROM = [["auto", "Auto-detect"], ...OPT_LANGS_TO]; export const OPT_LANGS_FROM = [["auto", "Auto-detect"], ...OPT_LANGS_TO];
export const OPT_LANGS_SPECIAL = { export const OPT_LANGS_SPECIAL = {
[OPT_TRANS_GOOGLE]: new Map(OPT_LANGS_FROM.map(([key]) => [key, key])),
[OPT_TRANS_MICROSOFT]: new Map([ [OPT_TRANS_MICROSOFT]: new Map([
...OPT_LANGS_FROM.map(([key]) => [key, key]),
["auto", ""], ["auto", ""],
["zh-CN", "zh-Hans"], ["zh-CN", "zh-Hans"],
["zh-TW", "zh-Hant"], ["zh-TW", "zh-Hant"],
]), ]),
[OPT_TRANS_DEEPL]: new Map([
...OPT_LANGS_FROM.map(([key]) => [key, key.toUpperCase()]),
["auto", ""],
["zh-CN", "ZH"],
["zh-TW", "ZH"],
]),
[OPT_TRANS_DEEPLFREE]: new Map([
...OPT_LANGS_FROM.map(([key]) => [key, key.toUpperCase()]),
["auto", "auto"],
["zh-CN", "ZH"],
["zh-TW", "ZH"],
]),
[OPT_TRANS_DEEPLX]: new Map([
...OPT_LANGS_FROM.map(([key]) => [key, key.toUpperCase()]),
["auto", "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_TRANS_OPENAI]: new Map(
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]]) OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
), ),
[OPT_TRANS_OPENAI_2]: new Map(
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
),
[OPT_TRANS_OPENAI_3]: new Map(
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
),
[OPT_TRANS_GEMINI]: new Map(
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
),
[OPT_TRANS_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_NONE = "style_none"; // 无
export const OPT_STYLE_LINE = "under_line"; // 下划线 export const OPT_STYLE_LINE = "under_line"; // 下划线
@@ -112,7 +360,9 @@ export const OPT_STYLE_DOTLINE = "dot_line"; // 点状线
export const OPT_STYLE_DASHLINE = "dash_line"; // 虚线 export const OPT_STYLE_DASHLINE = "dash_line"; // 虚线
export const OPT_STYLE_WAVYLINE = "wavy_line"; // 波浪线 export const OPT_STYLE_WAVYLINE = "wavy_line"; // 波浪线
export const OPT_STYLE_FUZZY = "fuzzy"; // 模糊 export const OPT_STYLE_FUZZY = "fuzzy"; // 模糊
export const OPT_STYLE_HIGHTLIGHT = "highlight"; // 高亮 export const OPT_STYLE_HIGHLIGHT = "highlight"; // 高亮
export const OPT_STYLE_BLOCKQUOTE = "blockquote"; // 引用
export const OPT_STYLE_DIY = "diy_style"; // 自定义样式
export const OPT_STYLE_ALL = [ export const OPT_STYLE_ALL = [
OPT_STYLE_NONE, OPT_STYLE_NONE,
OPT_STYLE_LINE, OPT_STYLE_LINE,
@@ -120,78 +370,310 @@ export const OPT_STYLE_ALL = [
OPT_STYLE_DASHLINE, OPT_STYLE_DASHLINE,
OPT_STYLE_WAVYLINE, OPT_STYLE_WAVYLINE,
OPT_STYLE_FUZZY, OPT_STYLE_FUZZY,
OPT_STYLE_HIGHTLIGHT, OPT_STYLE_HIGHLIGHT,
OPT_STYLE_BLOCKQUOTE,
OPT_STYLE_DIY,
];
export const OPT_STYLE_USE_COLOR = [
OPT_STYLE_LINE,
OPT_STYLE_DOTLINE,
OPT_STYLE_DASHLINE,
OPT_STYLE_WAVYLINE,
OPT_STYLE_HIGHLIGHT,
OPT_STYLE_BLOCKQUOTE,
];
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_LIMIT = 10; // 默认最大任务数量
export const DEFAULT_FETCH_INTERVAL = 100; // 默认任务间隔时间 export const DEFAULT_FETCH_INTERVAL = 100; // 默认任务间隔时间
export const PROMPT_PLACE_FROM = "{{from}}"; // 占位符 export const INPUT_PLACE_URL = "{{url}}"; // 占位符
export const PROMPT_PLACE_TO = "{{to}}"; // 占位符 export const INPUT_PLACE_FROM = "{{from}}"; // 占位符
export const INPUT_PLACE_TO = "{{to}}"; // 占位符
export const INPUT_PLACE_TEXT = "{{text}}"; // 占位符
export const INPUT_PLACE_KEY = "{{key}}"; // 占位符
export const INPUT_PLACE_MODEL = "{{model}}"; // 占位符
export const DEFAULT_COLOR = "#209CEE"; // 默认高亮背景色/线条颜色 export const DEFAULT_COLOR = "#209CEE"; // 默认高亮背景色/线条颜色
export const DEFAULT_TRANS_TAG = "span";
export const DEFAULT_SELECT_STYLE =
"-webkit-line-clamp: unset; max-height: none; height: auto;";
// 全局规则 // 全局规则
export const GLOBLA_RULE = { export const GLOBLA_RULE = {
pattern: "*", pattern: "*", // 匹配网址
selector: DEFAULT_SELECTOR, selector: DEFAULT_SELECTOR, // 选择器
keepSelector: DEFAULT_KEEP_SELECTOR, // 保留元素选择器
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, translator: OPT_TRANS_MICROSOFT,
fromLang: "auto", fromLang: "auto",
toLang: "zh-CN", toLang: "zh-CN",
textStyle: OPT_STYLE_DASHLINE, toLang2: "en",
transOpen: "false", tranboxShortcut: DEFAULT_TRANBOX_SHORTCUT,
bgColor: "", 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, // 英文词典
}; };
// 默认规则 // 订阅列表
export const DEFAULT_RULE = { export const DEFAULT_SUBRULES_LIST = [
pattern: "", {
selector: "", url: process.env.REACT_APP_RULESURL,
translator: GLOBAL_KEY, selected: false,
fromLang: GLOBAL_KEY, },
toLang: GLOBAL_KEY, {
textStyle: GLOBAL_KEY, url: process.env.REACT_APP_RULESURL_ON,
transOpen: GLOBAL_KEY, selected: true,
bgColor: "", },
{
url: process.env.REACT_APP_RULESURL_OFF,
selected: false,
},
];
// 翻译接口
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: URL_GOOGLE_TRAN,
key: "",
fetchLimit: DEFAULT_FETCH_LIMIT, // 最大任务数量
fetchInterval: DEFAULT_FETCH_INTERVAL, // 任务间隔时间
},
[OPT_TRANS_MICROSOFT]: {
fetchLimit: DEFAULT_FETCH_LIMIT,
fetchInterval: DEFAULT_FETCH_INTERVAL,
},
[OPT_TRANS_BAIDU]: {
fetchLimit: DEFAULT_FETCH_LIMIT,
fetchInterval: DEFAULT_FETCH_INTERVAL,
},
[OPT_TRANS_TENCENT]: {
fetchLimit: DEFAULT_FETCH_LIMIT,
fetchInterval: DEFAULT_FETCH_INTERVAL,
},
[OPT_TRANS_DEEPL]: {
url: "https://api-free.deepl.com/v2/translate",
key: "",
fetchLimit: 1,
fetchInterval: 500,
},
[OPT_TRANS_DEEPLFREE]: {
fetchLimit: 1,
fetchInterval: 500,
},
[OPT_TRANS_DEEPLX]: {
url: "http://localhost:1188/translate",
key: "",
fetchLimit: 1,
fetchInterval: 500,
},
[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,
};
// 默认快捷键
export const OPT_SHORTCUT_TRANSLATE = "toggleTranslate";
export const OPT_SHORTCUT_STYLE = "toggleStyle";
export const OPT_SHORTCUT_POPUP = "togglePopup";
export const OPT_SHORTCUT_SETTING = "openSetting";
export const DEFAULT_SHORTCUTS = {
[OPT_SHORTCUT_TRANSLATE]: ["AltLeft", "KeyQ"],
[OPT_SHORTCUT_STYLE]: ["AltLeft", "KeyC"],
[OPT_SHORTCUT_POPUP]: ["AltLeft", "KeyK"],
[OPT_SHORTCUT_SETTING]: ["AltLeft", "KeyO"],
};
export const TRANS_MIN_LENGTH = 5; // 最短翻译长度
export const TRANS_MAX_LENGTH = 5000; // 最长翻译长度
export const TRANS_NEWLINE_LENGTH = 20; // 换行字符数
export const DEFAULT_BLACKLIST = [
"https://fishjar.github.io/kiss-translator/options.html",
"https://translate.google.com",
"https://www.deepl.com/translator",
"oapi.dingtalk.com",
"login.dingtalk.com",
]; // 禁用翻译名单
export const DEFAULT_CSPLIST = ["https://github.com"]; // 禁用CSP名单
export const DEFAULT_SETTING = { export const DEFAULT_SETTING = {
darkMode: false, // 深色模式 darkMode: false, // 深色模式
uiLang: "en", // 界面语言 uiLang: "en", // 界面语言
fetchLimit: DEFAULT_FETCH_LIMIT, // 最大任务数量 // fetchLimit: DEFAULT_FETCH_LIMIT, // 最大任务数量(移至transApis作废)
fetchInterval: DEFAULT_FETCH_INTERVAL, // 任务间隔时间 // fetchInterval: DEFAULT_FETCH_INTERVAL, // 任务间隔时间(移至transApis作废)
minLength: TRANS_MIN_LENGTH,
maxLength: TRANS_MAX_LENGTH,
newlineLength: TRANS_NEWLINE_LENGTH,
clearCache: false, // 是否在浏览器下次启动时清除缓存 clearCache: false, // 是否在浏览器下次启动时清除缓存
injectRules: true, // 是否注入内置规则 injectRules: true, // 是否注入订阅规则
googleUrl: "https://translate.googleapis.com/translate_a/single", // 谷歌翻译接口 // injectWebfix: true, // 是否注入修复补丁(作废)
openaiUrl: "https://api.openai.com/v1/chat/completions", // detectRemote: false, // 是否使用远程语言检测(移至rule作废)
openaiKey: "", // contextMenus: true, // 是否添加右键菜单(作废)
openaiModel: "gpt-4", contextMenuType: 1, // 右键菜单类型(0不显示1简单菜单2多级菜单)
openaiPrompt: `You will be provided with a sentence in ${PROMPT_PLACE_FROM}, and your task is to translate it into ${PROMPT_PLACE_TO}.`, // 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_TIMING_PAGESCROLL, // 翻译时机/鼠标悬停翻译(移至rule作废)
shortcuts: DEFAULT_SHORTCUTS, // 快捷键
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 = [ export const DEFAULT_RULES = [GLOBLA_RULE];
{
...DEFAULT_RULE,
...RULES[0],
transOpen: "true",
},
GLOBLA_RULE,
];
export const BUILTIN_RULES = RULES.map((item) => ({ export const OPT_SYNCTYPE_WORKER = "KISS-Worker";
...DEFAULT_RULE, export const OPT_SYNCTYPE_WEBDAV = "WebDAV";
...item, export const OPT_SYNCTYPE_ALL = [OPT_SYNCTYPE_WORKER, OPT_SYNCTYPE_WEBDAV];
transOpen: "true",
}));
export const TRANS_MIN_LENGTH = 5; // 最短翻译长度
export const TRANS_MAX_LENGTH = 5000; // 最长翻译长度
export const DEFAULT_SYNC = { export const DEFAULT_SYNC = {
syncType: OPT_SYNCTYPE_WORKER, // 同步方式
syncUrl: "", // 数据同步接口 syncUrl: "", // 数据同步接口
syncUser: "", // 数据同步用户名
syncKey: "", // 数据同步密钥 syncKey: "", // 数据同步密钥
settingUpdateAt: 0, syncMeta: {}, // 数据更新及同步信息
settingSyncAt: 0, subRulesSyncAt: 0, // 订阅规则同步时间
rulesUpdateAt: 0, dataCaches: {}, // 缓存同步时间
rulesSyncAt: 0,
}; };

View File

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

View File

@@ -1,35 +1,3 @@
import { browser } from "./libs/browser"; import { run } from "./common";
import {
MSG_TRANS_TOGGLE,
MSG_TRANS_GETRULE,
MSG_TRANS_PUTRULE,
} from "./config";
import { getSetting, getRules, matchRule } from "./libs";
import { Translator } from "./libs/translator";
/** run();
* 入口函数
*/
(async () => {
const setting = await getSetting();
const rules = await getRules();
const rule = matchRule(rules, document.location.href, setting);
const translator = new Translator(rule, setting);
// 监听消息
browser?.runtime.onMessage.addListener(async ({ action, args }) => {
switch (action) {
case MSG_TRANS_TOGGLE:
translator.toggle();
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 };
});
})();

60
src/hooks/Alert.js Normal file
View File

@@ -0,0 +1,60 @@
import { createContext, useContext, useState, forwardRef } from "react";
import Snackbar from "@mui/material/Snackbar";
import MuiAlert from "@mui/material/Alert";
const Alert = forwardRef(function Alert(props, ref) {
return <MuiAlert elevation={6} ref={ref} variant="filled" {...props} />;
});
const AlertContext = createContext(null);
/**
* 左下角提示注入context后方便全局调用
* @param {*} param0
* @returns
*/
export function AlertProvider({ children }) {
const vertical = "top";
const horizontal = "center";
const [open, setOpen] = useState(false);
const [severity, setSeverity] = useState("info");
const [message, setMessage] = useState("");
const showAlert = (msg, type) => {
setOpen(true);
setMessage(msg);
setSeverity(type);
};
const handleClose = (_, reason) => {
if (reason === "clickaway") {
return;
}
setOpen(false);
};
const error = (msg) => showAlert(msg, "error");
const warning = (msg) => showAlert(msg, "warning");
const info = (msg) => showAlert(msg, "info");
const success = (msg) => showAlert(msg, "success");
return (
<AlertContext.Provider value={{ error, warning, info, success }}>
{children}
<Snackbar
open={open}
autoHideDuration={3000}
onClose={handleClose}
anchorOrigin={{ vertical, horizontal }}
>
<Alert onClose={handleClose} severity={severity} sx={{ width: "100%" }}>
{message}
</Alert>
</Snackbar>
</AlertContext.Provider>
);
}
export function useAlert() {
return useContext(AlertContext);
}

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

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

View File

@@ -1,22 +1,19 @@
import { useSetting, useSettingUpdate } from "./Setting"; import { useCallback } from "react";
import { useSetting } from "./Setting";
/** /**
* 深色模式hook * 深色模式hook
* @returns * @returns
*/ */
export function useDarkMode() { export function useDarkMode() {
const setting = useSetting(); const {
return !!setting?.darkMode; setting: { darkMode },
} updateSetting,
} = useSetting();
/** const toggleDarkMode = useCallback(async () => {
* 切换深色模式
* @returns
*/
export function useDarkModeSwitch() {
const darkMode = useDarkMode();
const updateSetting = useSettingUpdate();
return async () => {
await updateSetting({ darkMode: !darkMode }); await updateSetting({ darkMode: !darkMode });
}; }, [darkMode, updateSetting]);
return { darkMode, toggleDarkMode };
} }

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,13 +2,23 @@ import { useSetting } from "./Setting";
import { I18N, URL_RAW_PREFIX } from "../config"; import { I18N, URL_RAW_PREFIX } from "../config";
import { useFetch } from "./Fetch"; import { useFetch } from "./Fetch";
export const getI18n = (uiLang, key, defaultText = "") => {
return I18N?.[key]?.[uiLang] ?? defaultText;
};
export const useLangMap = (uiLang) => {
return (key, defaultText = "") => getI18n(uiLang, key, defaultText);
};
/** /**
* 多语言 hook * 多语言 hook
* @returns * @returns
*/ */
export const useI18n = () => { export const useI18n = () => {
const { uiLang } = useSetting() ?? {}; const {
return (key, defaultText = "") => I18N?.[key]?.[uiLang] ?? defaultText; setting: { uiLang },
} = useSetting();
return useLangMap(uiLang);
}; };
export const useI18nMd = (key) => { 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,92 +1,78 @@
import { import { STOKEY_RULES, DEFAULT_RULES, KV_RULES_KEY } from "../config";
STOKEY_RULES, import { useStorage } from "./Storage";
OPT_TRANS_ALL, import { trySyncRules } from "../libs/sync";
OPT_STYLE_ALL, import { checkRules } from "../libs/rules";
OPT_LANGS_FROM, import { useCallback } from "react";
OPT_LANGS_TO, import { useSyncMeta } from "./Sync";
GLOBAL_KEY,
} from "../config";
import storage from "../libs/storage";
import { useStorages } from "./Storage";
import { matchValue } from "../libs/utils";
import { syncRules } from "../libs/sync";
import { useSync } from "./Sync";
/** /**
* 匹配规则增删改查 hook * 规则 hook
* @returns * @returns
*/ */
export function useRules() { export function useRules() {
const storages = useStorages(); const { data: list, save } = useStorage(STOKEY_RULES, DEFAULT_RULES);
const list = storages?.[STOKEY_RULES] || []; const { updateSyncMeta } = useSyncMeta();
const sync = useSync();
const update = async (rules) => { const updateRules = useCallback(
const updateAt = sync.opt?.rulesUpdateAt ? Date.now() : 0; async (rules) => {
await storage.setObj(STOKEY_RULES, rules); await save(rules);
await sync.update({ rulesUpdateAt: updateAt }); await updateSyncMeta(KV_RULES_KEY);
syncRules(); trySyncRules();
}; },
[save, updateSyncMeta]
);
const add = async (rule) => { const add = useCallback(
const rules = [...list]; async (rule) => {
if (rule.pattern === "*") { const rules = [...list];
return; if (rule.pattern === "*") {
} return;
if (rules.map((item) => item.pattern).includes(rule.pattern)) { }
return; if (rules.map((item) => item.pattern).includes(rule.pattern)) {
} return;
rules.unshift(rule); }
await update(rules); rules.unshift(rule);
}; await updateRules(rules);
},
[list, updateRules]
);
const del = async (pattern) => { const del = useCallback(
async (pattern) => {
let rules = [...list];
if (pattern === "*") {
return;
}
rules = rules.filter((item) => item.pattern !== pattern);
await updateRules(rules);
},
[list, updateRules]
);
const clear = useCallback(async () => {
let rules = [...list]; let rules = [...list];
if (pattern === "*") { rules = rules.filter((item) => item.pattern === "*");
return; await updateRules(rules);
} }, [list, updateRules]);
rules = rules.filter((item) => item.pattern !== pattern);
await update(rules);
};
const put = async (pattern, obj) => { const put = useCallback(
const rules = [...list]; async (pattern, obj) => {
if (pattern === "*") { const rules = [...list];
obj.pattern = "*"; if (pattern === "*") {
} obj.pattern = "*";
const rule = rules.find((r) => r.pattern === pattern); }
rule && Object.assign(rule, obj); const rule = rules.find((r) => r.pattern === pattern);
await update(rules); rule && Object.assign(rule, obj);
}; await updateRules(rules);
},
[list, updateRules]
);
const merge = async (newRules) => { const merge = useCallback(
const rules = [...list]; async (newRules) => {
const fromLangs = OPT_LANGS_FROM.map((item) => item[0]); const rules = [...list];
const toLangs = OPT_LANGS_TO.map((item) => item[0]); newRules = checkRules(newRules);
newRules newRules.forEach((newRule) => {
.filter(({ pattern }) => pattern && typeof pattern === "string")
.map(
({
pattern,
selector,
translator,
fromLang,
toLang,
textStyle,
transOpen,
bgColor,
}) => ({
pattern,
selector: typeof selector === "string" ? selector : "",
bgColor: typeof bgColor === "string" ? bgColor : "",
translator: matchValue([GLOBAL_KEY, ...OPT_TRANS_ALL], translator),
fromLang: matchValue([GLOBAL_KEY, ...fromLangs], fromLang),
toLang: matchValue([GLOBAL_KEY, ...toLangs], toLang),
textStyle: matchValue([GLOBAL_KEY, ...OPT_STYLE_ALL], textStyle),
transOpen: matchValue([GLOBAL_KEY, "true", "false"], transOpen),
})
)
.forEach((newRule) => {
const rule = rules.find( const rule = rules.find(
(oldRule) => oldRule.pattern === newRule.pattern (oldRule) => oldRule.pattern === newRule.pattern
); );
@@ -96,8 +82,10 @@ export function useRules() {
rules.unshift(newRule); rules.unshift(newRule);
} }
}); });
await update(rules); await updateRules(rules);
}; },
[list, updateRules]
);
return { list, add, del, put, merge }; return { list, add, del, clear, put, merge };
} }

View File

@@ -1,28 +1,58 @@
import { STOKEY_SETTING } from "../config"; import { STOKEY_SETTING, DEFAULT_SETTING, KV_SETTING_KEY } from "../config";
import storage from "../libs/storage"; import { useStorage } from "./Storage";
import { useStorages } from "./Storage"; import { trySyncSetting } from "../libs/sync";
import { useSync } from "./Sync"; import { createContext, useCallback, useContext, useMemo } from "react";
import { syncSetting } from "../libs/sync"; import { debounce } from "../libs/utils";
import { useSyncMeta } from "./Sync";
const SettingContext = createContext({
setting: null,
updateSetting: async () => {},
reloadSetting: async () => {},
});
export function SettingProvider({ children }) {
const { data, update, reload } = useStorage(STOKEY_SETTING, DEFAULT_SETTING);
const { updateSyncMeta } = useSyncMeta();
const syncSetting = useMemo(
() =>
debounce(() => {
trySyncSetting();
}, [2000]),
[]
);
const updateSetting = useCallback(
async (obj) => {
await update(obj);
await updateSyncMeta(KV_SETTING_KEY);
syncSetting();
},
[update, syncSetting, updateSyncMeta]
);
if (!data) {
return;
}
return (
<SettingContext.Provider
value={{
setting: data,
updateSetting,
reloadSetting: reload,
}}
>
{children}
</SettingContext.Provider>
);
}
/** /**
* 设置hook * 设置 hook
* @returns * @returns
*/ */
export function useSetting() { export function useSetting() {
const storages = useStorages(); return useContext(SettingContext);
return storages?.[STOKEY_SETTING];
}
/**
* 更新设置
* @returns
*/
export function useSettingUpdate() {
const sync = useSync();
return async (obj) => {
const updateAt = sync.opt?.settingUpdateAt ? Date.now() : 0;
await storage.putObj(STOKEY_SETTING, obj);
await sync.update({ settingUpdateAt: updateAt });
syncSetting();
};
} }

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

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

View File

@@ -1,89 +1,70 @@
import { createContext, useContext, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { browser, isExt, isGm, isWeb } from "../libs/browser"; import { storage } from "../libs/storage";
import { import { kissLog } from "../libs/log";
STOKEY_SETTING,
STOKEY_RULES,
STOKEY_MSAUTH,
STOKEY_SYNC,
DEFAULT_SETTING,
DEFAULT_RULES,
DEFAULT_SYNC,
} from "../config";
import storage from "../libs/storage";
/** /**
* 默认配置 *
* @param {*} key
* @param {*} defaultVal 需为调用hook外的常量
* @returns
*/ */
export const defaultStorage = { export function useStorage(key, defaultVal) {
[STOKEY_MSAUTH]: null, const [loading, setLoading] = useState(false);
[STOKEY_SETTING]: DEFAULT_SETTING, const [data, setData] = useState(null);
[STOKEY_RULES]: DEFAULT_RULES,
[STOKEY_SYNC]: DEFAULT_SYNC,
};
const StoragesContext = createContext(null); const save = useCallback(
async (val) => {
setData(val);
await storage.setObj(key, val);
},
[key]
);
export function StoragesProvider({ children }) { const update = useCallback(
const [storages, setStorages] = useState(null); async (obj) => {
setData((pre = {}) => ({ ...pre, ...obj }));
await storage.putObj(key, obj);
},
[key]
);
const handleChanged = (changes) => { const remove = useCallback(async () => {
if (isWeb || isGm) { setData(null);
const { key, oldValue, newValue } = changes; await storage.del(key);
changes = { }, [key]);
[key]: {
oldValue, const reload = useCallback(async () => {
newValue, try {
}, setLoading(true);
}; const val = await storage.getObj(key);
if (val) {
setData(val);
}
} catch (err) {
kissLog(err, "storage reload");
} finally {
setLoading(false);
} }
const newStorages = {}; }, [key]);
Object.entries(changes)
.filter(([_, { oldValue, newValue }]) => oldValue !== newValue)
.forEach(([key, { newValue }]) => {
newStorages[key] = JSON.parse(newValue);
});
if (Object.keys(newStorages).length !== 0) {
setStorages((pre) => ({ ...pre, ...newStorages }));
}
};
useEffect(() => { useEffect(() => {
// 首次从storage同步配置到内存
(async () => { (async () => {
const curStorages = {}; try {
const keys = Object.keys(defaultStorage); setLoading(true);
for (const key of keys) { const val = await storage.getObj(key);
const val = await storage.get(key);
if (val) { if (val) {
curStorages[key] = JSON.parse(val); setData(val);
} else { } else if (defaultVal) {
await storage.setObj(key, defaultStorage[key]); setData(defaultVal);
curStorages[key] = defaultStorage[key]; await storage.setObj(key, defaultVal);
} }
} catch (err) {
kissLog(err, "storage load");
} finally {
setLoading(false);
} }
setStorages(curStorages);
})(); })();
}, [key, defaultVal]);
// 监听storage并同步到内存中 return { data, save, update, remove, reload, loading };
storage.onChanged(handleChanged);
// 解除监听
return () => {
if (isExt) {
browser.storage.onChanged.removeListener(handleChanged);
} else {
window.removeEventListener("storage", handleChanged);
}
};
}, []);
return (
<StoragesContext.Provider value={storages}>
{children}
</StoragesContext.Provider>
);
}
export function useStorages() {
return useContext(StoragesContext);
} }

114
src/hooks/SubRules.js Normal file
View File

@@ -0,0 +1,114 @@
import { DEFAULT_SUBRULES_LIST, DEFAULT_OW_RULE } from "../config";
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";
/**
* 订阅规则
* @returns
*/
export function useSubRules() {
const [loading, setLoading] = useState(false);
const [selectedRules, setSelectedRules] = useState([]);
const { setting, updateSetting } = useSetting();
const list = setting?.subrulesList || DEFAULT_SUBRULES_LIST;
const selectedSub = useMemo(() => list.find((item) => item.selected), [list]);
const selectedUrl = selectedSub.url;
const selectSub = useCallback(
async (url) => {
const subrulesList = [...list];
subrulesList.forEach((item) => {
if (item.url === url) {
item.selected = true;
} else {
item.selected = false;
}
});
await updateSetting({ subrulesList });
},
[list, updateSetting]
);
const updateSub = useCallback(
async (url, obj) => {
const subrulesList = [...list];
subrulesList.forEach((item) => {
if (item.url === url) {
Object.assign(item, obj);
}
});
await updateSetting({ subrulesList });
},
[list, updateSetting]
);
const addSub = useCallback(
async (url) => {
const subrulesList = [...list];
subrulesList.push({ url, selected: false });
await updateSetting({ subrulesList });
},
[list, updateSetting]
);
const delSub = useCallback(
async (url) => {
let subrulesList = [...list];
subrulesList = subrulesList.filter((item) => item.url !== url);
await updateSetting({ subrulesList });
await delSubRules(url);
},
[list, updateSetting]
);
useEffect(() => {
(async () => {
if (selectedUrl) {
try {
setLoading(true);
const rules = await loadOrFetchSubRules(selectedUrl);
setSelectedRules(rules);
} catch (err) {
kissLog(err, "loadOrFetchSubRules");
} finally {
setLoading(false);
}
}
})();
}, [selectedUrl]);
return {
subList: list,
selectSub,
updateSub,
addSub,
delSub,
selectedSub,
selectedUrl,
selectedRules,
setSelectedRules,
loading,
};
}
/**
* 覆写订阅规则
* @returns
*/
export function useOwSubRule() {
const { setting, updateSetting } = useSetting();
const { owSubrule = DEFAULT_OW_RULE } = setting;
const updateOwSubrule = useCallback(
async (obj) => {
await updateSetting({ owSubrule: { ...owSubrule, ...obj } });
},
[owSubrule, updateSetting]
);
return { owSubrule, updateOwSubrule };
}

View File

@@ -1,20 +1,63 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { STOKEY_SYNC } from "../config"; import { STOKEY_SYNC, DEFAULT_SYNC } from "../config";
import storage from "../libs/storage"; import { useStorage } from "./Storage";
import { useStorages } from "./Storage";
/** /**
* sync hook * sync hook
* @returns * @returns
*/ */
export function useSync() { export function useSync() {
const storages = useStorages(); const { data, update, reload } = useStorage(STOKEY_SYNC, DEFAULT_SYNC);
const opt = storages?.[STOKEY_SYNC]; return { sync: data, updateSync: update, reloadSync: reload };
const update = useCallback(async (obj) => { }
await storage.putObj(STOKEY_SYNC, obj);
}, []); /**
* 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 { return {
opt, dataCaches: sync?.dataCaches || {},
update, updateDataCache,
deleteDataCache,
reloadSync,
}; };
} }

View File

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

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

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

View File

@@ -1,19 +1,47 @@
import React from "react"; import React, { useState } from "react";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import CircularProgress from "@mui/material/CircularProgress"; import CircularProgress from "@mui/material/CircularProgress";
import Divider from "@mui/material/Divider"; import Divider from "@mui/material/Divider";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
import Paper from "@mui/material/Paper"; import Paper from "@mui/material/Paper";
import Stack from "@mui/material/Stack";
import Button from "@mui/material/Button";
import Link from "@mui/material/Link";
import { useFetch } from "./hooks/Fetch"; import { useFetch } from "./hooks/Fetch";
import { I18N, URL_RAW_PREFIX } from "./config"; import { I18N, URL_RAW_PREFIX } from "./config";
function App() { function App() {
const [lang, setLang] = useState("zh");
const [data, loading, error] = useFetch( const [data, loading, error] = useFetch(
`${URL_RAW_PREFIX}/${I18N?.["about_md"]?.["zh"]}` `${URL_RAW_PREFIX}/${I18N?.["about_md"]?.[lang]}`
); );
return ( return (
<Paper sx={{ padding: 2, margin: 2 }}> <Paper sx={{ padding: 2, margin: 2 }}>
<Divider>{`KISS Translator v${process.env.REACT_APP_VERSION}`}</Divider> <Stack spacing={2} direction="row" justifyContent="flex-end">
<Button
variant="text"
onClick={() => {
setLang((pre) => (pre === "zh" ? "en" : "zh"));
}}
>
{lang === "zh" ? "ENGLISH" : "中文"}
</Button>
</Stack>
<Divider>
<Link
href={process.env.REACT_APP_HOMEPAGE}
>{`KISS Translator v${process.env.REACT_APP_VERSION}`}</Link>
</Divider>
<Stack spacing={2}>
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}>
Install/Update Userscript for Tampermonkey/Violentmonkey
</Link>
<Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL}>
Install/Update Userscript for iOS Safari
</Link>
<Link href={process.env.REACT_APP_OPTIONSPAGE}>Open Options Page</Link>
</Stack>
{loading ? ( {loading ? (
<center> <center>
<CircularProgress /> <CircularProgress />

View File

@@ -1,12 +1,13 @@
import storage from "./storage"; import { getMsauth, setMsauth } from "./storage";
import { STOKEY_MSAUTH, URL_MICROSOFT_AUTH } from "../config"; import { URL_MICROSOFT_AUTH } from "../config";
import { fetchData } from "./fetch"; import { fetchHandle } from "./fetch";
import { kissLog } from "./log";
const parseMSToken = (token) => { const parseMSToken = (token) => {
try { try {
return JSON.parse(atob(token.split(".")[1])).exp; return JSON.parse(atob(token.split(".")[1])).exp;
} catch (err) { } catch (err) {
console.log("[parseMSToken]", err); kissLog(err, "parseMSToken");
} }
return 0; return 0;
}; };
@@ -26,17 +27,17 @@ const _msAuth = () => {
} }
// 查询storage缓存 // 查询storage缓存
const res = (await storage.getObj(STOKEY_MSAUTH)) || {}; const res = await getMsauth();
token = res.token; token = res?.token;
exp = res.exp; exp = res?.exp;
if (token && exp * 1000 > now + 1000) { if (token && exp * 1000 > now + 1000) {
return [token, exp]; return [token, exp];
} }
// 缓存没有或失效,查询接口 // 缓存没有或失效,查询接口
token = await fetchData(URL_MICROSOFT_AUTH); token = await fetchHandle({ input: URL_MICROSOFT_AUTH });
exp = parseMSToken(token); exp = parseMSToken(token);
await storage.setObj(STOKEY_MSAUTH, { token, exp }); await setMsauth({ token, exp });
return [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

@@ -1,4 +1,4 @@
import { CLIENT_EXTS, CLIENT_USERSCRIPT, CLIENT_WEB } from "../config"; // import { CLIENT_EXTS, CLIENT_USERSCRIPT, CLIENT_WEB } from "../config";
/** /**
* 浏览器兼容插件,另可用于判断是插件模式还是网页模式,方便开发 * 浏览器兼容插件,另可用于判断是插件模式还是网页模式,方便开发
@@ -8,12 +8,10 @@ function _browser() {
try { try {
return require("webextension-polyfill"); return require("webextension-polyfill");
} catch (err) { } catch (err) {
// console.log("[browser]", err.message); // kissLog(err, "browser");
} }
} }
export const browser = _browser(); export const browser = _browser();
export const client = process.env.REACT_APP_CLIENT;
export const isExt = CLIENT_EXTS.includes(client); export const isBg = () => globalThis?.ContextType === "BACKGROUND";
export const isGm = client === CLIENT_USERSCRIPT;
export const isWeb = client === CLIENT_WEB;

6
src/libs/client.js Normal file
View File

@@ -0,0 +1,6 @@
import { CLIENT_EXTS, CLIENT_USERSCRIPT, CLIENT_WEB } from "../config";
export const client = process.env.REACT_APP_CLIENT;
export const isExt = CLIENT_EXTS.includes(client);
export const isGm = client === CLIENT_USERSCRIPT;
export const isWeb = client === CLIENT_WEB;

View File

@@ -1,55 +1,28 @@
import { isExt, isGm } from "./browser"; import { isExt, isGm } from "./client";
import { sendMsg } from "./msg"; import { sendBgMsg } from "./msg";
import { taskPool } from "./pool"; import { taskPool } from "./pool";
import { import {
MSG_FETCH, MSG_FETCH,
MSG_FETCH_LIMIT, MSG_GET_HTTPCACHE,
MSG_FETCH_CLEAR,
CACHE_NAME, CACHE_NAME,
OPT_TRANS_MICROSOFT,
OPT_TRANS_OPENAI,
DEFAULT_FETCH_INTERVAL, DEFAULT_FETCH_INTERVAL,
DEFAULT_FETCH_LIMIT, DEFAULT_FETCH_LIMIT,
} from "../config"; } from "../config";
import { msAuth } from "./auth"; import { isBg } from "./browser";
import { genTransReq } from "../apis/trans";
import { kissLog } from "./log";
import { blobToBase64 } from "./utils";
const TIMEOUT = 5000;
/** /**
* 油猴脚本的请求封装 * 构造缓存 request
* @param {*} input * @param {*} input
* @param {*} init * @param {*} init
* @returns * @returns
*/ */
const fetchGM = async (input, { method = "GET", headers, body } = {}) => const newCacheReq = async (input, init) => {
new Promise((resolve, reject) => { let request = new Request(input, init);
GM.xmlHttpRequest({
method,
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}`));
}
},
onerror: reject,
});
});
/**
* 构造缓存 request
* @param {*} request
* @returns
*/
const newCacheReq = async (request) => {
if (request.method !== "GET") { if (request.method !== "GET") {
const body = await request.text(); const body = await request.text();
const cacheUrl = new URL(request.url); const cacheUrl = new URL(request.url);
@@ -60,114 +33,244 @@ const newCacheReq = async (request) => {
return request; return request;
}; };
/**
* 油猴脚本的请求封装
* @param {*} input
* @param {*} init
* @returns
*/
export const fetchGM = async (input, { method = "GET", headers, body } = {}) =>
new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method,
url: input,
headers,
data: body,
// 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,
});
});
/** /**
* 发起请求 * 发起请求
* @param {*} param0 * @param {*} param0
* @returns * @returns
*/ */
const fetchApi = async ({ input, init, useUnsafe, translator, token }) => { export const fetchPatcher = async (input, init, transOpts, apiSetting) => {
if (translator === OPT_TRANS_MICROSOFT) { if (transOpts?.translator) {
init.headers["Authorization"] = `Bearer ${token}`; [input, init] = await genTransReq(transOpts, apiSetting);
} else if (translator === OPT_TRANS_OPENAI) {
init.headers["Authorization"] = `Bearer ${token}`; // // OpenAI
init.headers["api-key"] = token; // Azure OpenAI
} }
if (isGm && !useUnsafe) { if (!input) {
return fetchGM(input, init); throw new Error("url is empty");
} }
if (isGm) {
let info;
if (window.KISS_GM) {
info = await window.KISS_GM.getInfo();
} 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) {
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); return fetch(input, init);
}; };
/**
* 解析 response
* @param {*} res
* @returns
*/
const parseResponse = async (res) => {
if (!res) {
return null;
}
const contentType = res.headers.get("Content-Type");
if (contentType?.includes("json")) {
return await res.json();
} else if (contentType?.includes("audio")) {
const blob = await res.blob();
return await blobToBase64(blob);
}
return await res.text();
};
/**
* 查询 caches
* @param {*} input
* @param {*} param1
* @returns
*/
export const getHttpCache = async (input, { method, headers, body }) => {
try {
const req = await newCacheReq(input, { method, headers, body });
const cache = await caches.open(CACHE_NAME);
const res = await cache.match(req);
return parseResponse(res);
} catch (err) {
kissLog(err, "get cache");
}
return null;
};
/**
* 插入 caches
* @param {*} input
* @param {*} param1
* @param {*} res
*/
export const putHttpCache = async (input, { method, headers, body }, res) => {
try {
const req = await newCacheReq(input, { method, headers, body });
const cache = await caches.open(CACHE_NAME);
await cache.put(req, res);
} catch (err) {
kissLog(err, "put cache");
}
};
/**
* 处理请求
* @param {*} param0
* @returns
*/
export const fetchHandle = async ({
input,
useCache,
transOpts,
apiSetting,
...init
}) => {
// 发送请求
const res = await fetchPatcher(input, init, transOpts, apiSetting);
if (!res) {
throw new Error("Unknow error");
} else if (!res.ok) {
const msg = {
url: res.url,
status: res.status,
};
if (res.headers.get("Content-Type")?.includes("json")) {
msg.response = await res.json();
}
throw new Error(JSON.stringify(msg));
}
// 插入缓存
if (useCache) {
await putHttpCache(input, init, res.clone());
}
return parseResponse(res);
};
/**
* fetch 兼容性封装
* @param {*} args
* @returns
*/
export const fetchPolyfill = (args) => {
// 插件
if (isExt && !isBg()) {
return sendBgMsg(MSG_FETCH, args);
}
// 油猴/网页/BackgroundPage
return fetchHandle(args);
};
/**
* getHttpCache 兼容性封装
* @param {*} input
* @param {*} init
* @returns
*/
export const getHttpCachePolyfill = (input, init) => {
// 插件
if (isExt && !isBg()) {
return sendBgMsg(MSG_GET_HTTPCACHE, { input, init });
}
// 油猴/网页/BackgroundPage
return getHttpCache(input, init);
};
/** /**
* 请求池实例 * 请求池实例
*/ */
export const fetchPool = taskPool( export const fetchPool = taskPool(
fetchApi, fetchPolyfill,
async ({ translator }) => { null,
if (translator === OPT_TRANS_MICROSOFT) {
const [token] = await msAuth();
return { token };
}
return {};
},
DEFAULT_FETCH_INTERVAL, DEFAULT_FETCH_INTERVAL,
DEFAULT_FETCH_LIMIT DEFAULT_FETCH_LIMIT
); );
/** /**
* 请求数据统一接口 * 数据请求
* @param {*} input * @param {*} input
* @param {*} init * @param {*} param1
* @param {*} opts
* @returns * @returns
*/ */
export const fetchData = async ( export const fetchData = async (input, { useCache, usePool, ...args } = {}) => {
input, if (!input?.trim()) {
init, throw new Error("URL is empty");
{ useCache, usePool, translator, useUnsafe, token } = {} }
) => {
const cacheReq = await newCacheReq(new Request(input, init));
const cache = await caches.open(CACHE_NAME);
let res;
// 查询缓存 // 查询缓存
if (useCache) { if (useCache) {
try { const cache = await getHttpCachePolyfill(input, args);
res = await cache.match(cacheReq); if (cache) {
} catch (err) { return cache;
console.log("[cache match]", err);
} }
} }
if (!res) { // 通过任务池发送请求
// 发送请求 if (usePool) {
if (usePool) { return fetchPool.push({ input, useCache, ...args });
res = await fetchPool.push({ input, init, useUnsafe, translator, token });
} else {
res = await fetchApi({ input, init, useUnsafe, translator, token });
}
if (!res?.ok) {
throw new Error(`response: ${res.statusText}`);
}
// 插入缓存
if (useCache) {
try {
await cache.put(cacheReq, res.clone());
} catch (err) {
console.log("[cache put]", err);
}
}
} }
const contentType = res.headers.get("Content-Type"); // 直接请求
if (contentType?.includes("json")) { return fetchPolyfill({ input, useCache, ...args });
return await res.json();
}
return await res.text();
};
/**
* fetch 兼容性封装
* @param {*} input
* @param {*} init
* @param {*} opts
* @returns
*/
export const fetchPolyfill = async (input, init, opts) => {
// 插件
if (isExt) {
const res = await sendMsg(MSG_FETCH, { input, init, opts });
if (res.error) {
throw new Error(res.error);
}
return res.data;
}
// 油猴/网页
return await fetchData(input, init, opts);
}; };
/** /**
@@ -175,27 +278,13 @@ export const fetchPolyfill = async (input, init, opts) => {
* @param {*} interval * @param {*} interval
* @param {*} limit * @param {*} limit
*/ */
export const fetchUpdate = async (interval, limit) => { export const updateFetchPool = (interval, limit) => {
if (isExt) { fetchPool.update(interval, limit);
const res = await sendMsg(MSG_FETCH_LIMIT, { interval, limit });
if (res.error) {
throw new Error(res.error);
}
} else {
fetchPool.update(interval, limit);
}
}; };
/** /**
* 清空任务池 * 清空任务池
*/ */
export const fetchClear = async () => { export const clearFetchPool = () => {
if (isExt) { fetchPool.clear();
const res = await sendMsg(MSG_FETCH_CLEAR);
if (res.error) {
throw new Error(res.error);
}
} else {
fetchPool.clear();
}
}; };

102
src/libs/gm.js Normal file
View File

@@ -0,0 +1,102 @@
import { fetchGM } from "./fetch";
import { genEventName } from "./utils";
const MSG_GM_xmlHttpRequest = "xmlHttpRequest";
const MSG_GM_setValue = "setValue";
const MSG_GM_getValue = "getValue";
const MSG_GM_deleteValue = "deleteValue";
const MSG_GM_info = "info";
/**
* 注入页面的脚本请求并接受GM接口信息
* @param {*} param0
*/
export const injectScript = (ping) => {
window.APP_INFO = {
name: process.env.REACT_APP_NAME,
version: process.env.REACT_APP_VERSION,
eventName: ping,
};
};
/**
* 适配GM脚本
*/
export const adaptScript = (ping) => {
const promiseGM = (action, args, timeout = 5000) =>
new Promise((resolve, reject) => {
const pong = genEventName();
const handleEvent = (e) => {
window.removeEventListener(pong, handleEvent);
const { data, error } = e.detail;
if (error) {
reject(new Error(error));
} else {
resolve(data);
}
};
window.addEventListener(pong, handleEvent);
window.dispatchEvent(
new CustomEvent(ping, { detail: { action, args, pong } })
);
setTimeout(() => {
window.removeEventListener(pong, handleEvent);
reject(new Error("timeout"));
}, timeout);
});
window.KISS_GM = {
fetch: (input, init) => promiseGM(MSG_GM_xmlHttpRequest, { input, init }),
setValue: (key, val) => promiseGM(MSG_GM_setValue, { key, val }),
getValue: (key) => promiseGM(MSG_GM_getValue, { key }),
deleteValue: (key) => promiseGM(MSG_GM_deleteValue, { key }),
getInfo: async () => {
if (!window.GM_info) {
window.GM_info = await promiseGM(MSG_GM_info);
}
return window.GM_info;
},
};
};
/**
* 监听并回应页面对GM接口的请求
* @param {*} param0
*/
export const handlePing = async (e) => {
const { action, args, pong } = e.detail;
let res;
try {
switch (action) {
case MSG_GM_xmlHttpRequest:
const { input, init } = args;
res = await fetchGM(input, init);
break;
case MSG_GM_setValue:
const { key, val } = args;
await GM.setValue(key, val);
res = val;
break;
case MSG_GM_getValue:
res = await GM.getValue(args.key);
break;
case MSG_GM_deleteValue:
await GM.deleteValue(args.key);
res = "ok";
break;
case MSG_GM_info:
res = GM.info;
break;
default:
throw new Error(`message action is unavailable: ${action}`);
}
window.dispatchEvent(new CustomEvent(pong, { detail: { data: res } }));
} catch (err) {
window.dispatchEvent(
new CustomEvent(pong, { detail: { error: err.message } })
);
}
};

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

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

View File

@@ -1,99 +1,65 @@
import storage from "./storage";
import { import {
DEFAULT_SETTING, CACHE_NAME,
STOKEY_SETTING, OPT_TRANS_GOOGLE,
STOKEY_RULES, OPT_TRANS_MICROSOFT,
STOKEY_FAB, OPT_TRANS_BAIDU,
GLOBLA_RULE, OPT_TRANS_TENCENT,
GLOBAL_KEY,
BUILTIN_RULES,
} from "../config"; } from "../config";
import { browser } from "./browser"; import { browser } from "./browser";
import {
apiGoogleLangdetect,
apiMicrosoftLangdetect,
apiBaiduLangdetect,
apiTencentLangdetect,
} from "../apis";
import { kissLog } from "./log";
/** const langdetectMap = {
* 获取节点列表并转为数组 [OPT_TRANS_GOOGLE]: apiGoogleLangdetect,
* @param {*} selector [OPT_TRANS_MICROSOFT]: apiMicrosoftLangdetect,
* @param {*} el [OPT_TRANS_BAIDU]: apiBaiduLangdetect,
* @returns [OPT_TRANS_TENCENT]: apiTencentLangdetect,
*/
export const queryEls = (selector, el = document) =>
Array.from(el.querySelectorAll(selector));
/**
* 查询storage中的设置
* @returns
*/
export const getSetting = async () => ({
...DEFAULT_SETTING,
...((await storage.getObj(STOKEY_SETTING)) || {}),
});
/**
* 查询规则列表
* @returns
*/
export const getRules = async () => (await storage.getObj(STOKEY_RULES)) || [];
/**
* 查询fab位置信息
* @returns
*/
export const getFab = async () => (await storage.getObj(STOKEY_FAB)) || {};
/**
* 设置fab位置信息
* @returns
*/
export const setFab = async (obj) => await storage.setObj(STOKEY_FAB, obj);
/**
* 根据href匹配规则
* TODO: 支持通配符(*)匹配
* @param {*} rules
* @param {string} href
* @returns
*/
export const matchRule = (rules, href, { injectRules }) => {
if (injectRules) {
rules.splice(-1, 0, ...BUILTIN_RULES);
}
const rule = rules.find((rule) =>
rule.pattern.split(",").some((p) => href.includes(p.trim()))
);
const globalRule =
rules.find((rule) =>
rule.pattern.split(",").some((p) => p.trim() === "*")
) || GLOBLA_RULE;
if (!rule) {
return globalRule;
}
rule.selector =
rule?.selector?.trim() ||
globalRule?.selector?.trim() ||
GLOBLA_RULE.selector;
rule.bgColor = rule?.bgColor?.trim() || globalRule?.bgColor?.trim();
["translator", "fromLang", "toLang", "textStyle", "transOpen"].forEach(
(key) => {
if (rule[key] === GLOBAL_KEY) {
rule[key] = globalRule[key];
}
}
);
return rule;
}; };
/** /**
* 本地语言识别 * 清除缓存数据
*/
export const tryClearCaches = async () => {
try {
caches.delete(CACHE_NAME);
} catch (err) {
kissLog(err, "clean caches");
}
};
/**
* 语言识别
* @param {*} q * @param {*} q
* @returns * @returns
*/ */
export const detectLang = async (q) => { export const tryDetectLang = async (
const res = await browser?.i18n.detectLanguage(q); q,
return res?.languages?.[0]?.language; 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,13 +1,30 @@
import { browser } from "./browser"; import { browser } from "./browser";
/**
* 获取当前tab信息
* @returns
*/
export const getCurTab = async () => {
const [tab] = await browser.tabs.query({
active: true,
lastFocusedWindow: true,
});
return tab;
};
export const getCurTabId = async () => {
const tab = await getCurTab();
return tab.id;
};
/** /**
* 发送消息给background * 发送消息给background
* @param {*} action * @param {*} action
* @param {*} args * @param {*} args
* @returns * @returns
*/ */
export const sendMsg = (action, args) => export const sendBgMsg = (action, args) =>
browser?.runtime?.sendMessage({ action, args }); browser.runtime.sendMessage({ action, args });
/** /**
* 发送消息给当前页面 * 发送消息给当前页面
@@ -16,6 +33,6 @@ export const sendMsg = (action, args) =>
* @returns * @returns
*/ */
export const sendTabMsg = async (action, args) => { export const sendTabMsg = async (action, args) => {
const tabs = await browser?.tabs.query({ active: true, currentWindow: true }); const tabId = await getCurTabId();
return await browser?.tabs.sendMessage(tabs[0].id, { action, args }); return browser.tabs.sendMessage(tabId, { action, args });
}; };

View File

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

219
src/libs/rules.js Normal file
View File

@@ -0,0 +1,219 @@
import { matchValue, type, isMatch } from "./utils";
import {
GLOBAL_KEY,
REMAIN_KEY,
OPT_TRANS_ALL,
OPT_STYLE_ALL,
OPT_LANGS_FROM,
OPT_LANGS_TO,
OPT_TIMING_ALL,
GLOBLA_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匹配规则
* @param {*} rules
* @param {string} href
* @returns
*/
export const matchRule = async (
href,
{ injectRules, subrulesList, owSubrule }
) => {
const rules = await getRulesWithDefault();
if (injectRules) {
try {
const selectedSub = subrulesList.find((item) => item.selected);
if (selectedSub?.url) {
const mixRule = {};
Object.entries(owSubrule)
.filter(([key, val]) => {
if (
owSubrule.textStyle === REMAIN_KEY &&
(key === "bgColor" || key === "textDiyStyle")
) {
return false;
}
return val !== REMAIN_KEY;
})
.forEach(([key, val]) => {
mixRule[key] = val;
});
let subRules = await loadOrFetchSubRules(selectedSub.url);
subRules = subRules.map((item) => ({ ...item, ...mixRule }));
rules.splice(-1, 0, ...subRules);
}
} catch (err) {
kissLog(err, "load injectRules");
}
}
const rule = rules.find((r) =>
r.pattern.split(",").some((p) => isMatch(href, p.trim()))
);
const globalRule = {
...GLOBLA_RULE,
...(rules.find((r) => r.pattern === GLOBAL_KEY) || {}),
};
if (!rule) {
return globalRule;
}
[
"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;
rule.textDiyStyle = globalRule.textDiyStyle;
} else {
rule.bgColor = rule.bgColor?.trim() || globalRule.bgColor;
rule.textDiyStyle = rule.textDiyStyle?.trim() || globalRule.textDiyStyle;
}
return rule;
};
/**
* 检查过滤rules
* @param {*} rules
* @returns
*/
export const checkRules = (rules) => {
if (type(rules) === "string") {
rules = JSON.parse(rules);
}
if (type(rules) !== "array") {
throw new Error("data error");
}
const fromLangs = OPT_LANGS_FROM.map((item) => item[0]);
const toLangs = OPT_LANGS_TO.map((item) => item[0]);
const patternSet = new Set();
rules = rules
.filter((rule) => type(rule) === "object")
.filter(({ pattern }) => {
if (type(pattern) !== "string" || patternSet.has(pattern.trim())) {
return false;
}
patternSet.add(pattern.trim());
return true;
})
.map(
({
pattern,
selector,
keepSelector,
terms,
selectStyle,
parentStyle,
injectJs,
injectCss,
translator,
fromLang,
toLang,
textStyle,
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),
fromLang: matchValue([GLOBAL_KEY, ...fromLangs], fromLang),
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();
};

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

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

View File

@@ -1,28 +1,28 @@
import { browser, isExt, isGm } from "./browser"; import {
STOKEY_SETTING,
STOKEY_RULES,
STOKEY_WORDS,
STOKEY_FAB,
STOKEY_SYNC,
STOKEY_MSAUTH,
STOKEY_BDAUTH,
STOKEY_RULESCACHE_PREFIX,
DEFAULT_SETTING,
DEFAULT_RULES,
DEFAULT_SYNC,
BUILTIN_RULES,
} from "../config";
import { isExt, isGm } from "./client";
import { browser } from "./browser";
import { kissLog } from "./log";
async function set(key, val) { async function set(key, val) {
if (isExt) { if (isExt) {
await browser.storage.local.set({ [key]: val }); await browser.storage.local.set({ [key]: val });
} else if (isGm) { } else if (isGm) {
const oldValue = await GM.getValue(key); await (window.KISS_GM || GM).setValue(key, val);
await GM.setValue(key, val);
window.dispatchEvent(
new StorageEvent("storage", {
key,
oldValue,
newValue: val,
})
);
} else { } else {
const oldValue = window.localStorage.getItem(key);
window.localStorage.setItem(key, val); window.localStorage.setItem(key, val);
window.dispatchEvent(
new StorageEvent("storage", {
key,
oldValue,
newValue: val,
})
);
} }
} }
@@ -31,7 +31,7 @@ async function get(key) {
const val = await browser.storage.local.get([key]); const val = await browser.storage.local.get([key]);
return val[key]; return val[key];
} else if (isGm) { } else if (isGm) {
const val = await GM.getValue(key); const val = await (window.KISS_GM || GM).getValue(key);
return val; return val;
} }
return window.localStorage.getItem(key); return window.localStorage.getItem(key);
@@ -41,25 +41,9 @@ async function del(key) {
if (isExt) { if (isExt) {
await browser.storage.local.remove([key]); await browser.storage.local.remove([key]);
} else if (isGm) { } else if (isGm) {
const oldValue = await GM.getValue(key); await (window.KISS_GM || GM).deleteValue(key);
await GM.deleteValue(key);
window.dispatchEvent(
new StorageEvent("storage", {
key,
oldValue,
newValue: null,
})
);
} else { } else {
const oldValue = window.localStorage.getItem(key);
window.localStorage.removeItem(key); window.localStorage.removeItem(key);
window.dispatchEvent(
new StorageEvent("storage", {
key,
oldValue,
newValue: null,
})
);
} }
} }
@@ -83,22 +67,10 @@ async function putObj(key, obj) {
await setObj(key, { ...cur, ...obj }); await setObj(key, { ...cur, ...obj });
} }
/**
* 监听storage事件
* @param {*} handleChanged
*/
function onChanged(handleChanged) {
if (isExt) {
browser.storage.onChanged.addListener(handleChanged);
} else {
window.addEventListener("storage", handleChanged);
}
}
/** /**
* 对storage的封装 * 对storage的封装
*/ */
const storage = { export const storage = {
get, get,
set, set,
del, del,
@@ -106,7 +78,84 @@ const storage = {
trySetObj, trySetObj,
getObj, getObj,
putObj, putObj,
onChanged, // onChanged,
}; };
export default storage; /**
* 设置信息
*/
export const getSetting = () => getObj(STOKEY_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);
/**
* 规则列表
*/
export const getRules = () => getObj(STOKEY_RULES);
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);
/**
* 订阅规则
*/
export const getSubRules = (url) => getObj(STOKEY_RULESCACHE_PREFIX + url);
export const getSubRulesWithDefault = async () => (await getSubRules()) || [];
export const delSubRules = (url) => del(STOKEY_RULESCACHE_PREFIX + url);
export const setSubRules = (url, val) =>
setObj(STOKEY_RULESCACHE_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);
/**
* 数据同步
*/
export const getSync = () => getObj(STOKEY_SYNC);
export const getSyncWithDefault = async () => (await getSync()) || DEFAULT_SYNC;
export const updateSync = (obj) => putObj(STOKEY_SYNC, obj);
/**
* ms auth
*/
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);
/**
* 存入默认数据
*/
export const tryInitDefaultData = async () => {
try {
await trySetObj(STOKEY_SETTING, DEFAULT_SETTING);
await trySetObj(STOKEY_RULES, DEFAULT_RULES);
await trySetObj(STOKEY_SYNC, DEFAULT_SYNC);
await trySetObj(
`${STOKEY_RULESCACHE_PREFIX}${process.env.REACT_APP_RULESURL}`,
BUILTIN_RULES
);
} catch (err) {
kissLog(err, "init default");
}
};

87
src/libs/subRules.js Normal file
View File

@@ -0,0 +1,87 @@
import { GLOBAL_KEY } from "../config";
import {
getSyncWithDefault,
updateSync,
setSubRules,
getSubRules,
} 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) => {
const res = await apiFetch(url);
const rules = checkRules(res).filter(
({ pattern }) => !isAllchar(pattern, GLOBAL_KEY)
);
if (rules.length > 0) {
await setSubRules(url, rules);
}
return rules;
};
/**
* 同步所有订阅规则
* @param {*} url
* @returns
*/
export const syncAllSubRules = async (subrulesList) => {
for (const subrules of subrulesList) {
try {
await syncSubRules(subrules.url);
await updateSyncDataCache(subrules.url);
} catch (err) {
kissLog(err, `sync subrule error: ${subrules.url}`);
}
}
};
/**
* 根据时间同步所有订阅规则
* @param {*} url
* @returns
*/
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);
await updateSync({ subRulesSyncAt: now });
}
} catch (err) {
kissLog(err, "try sync all subrules");
}
};
/**
* 从缓存或远程加载订阅规则
* @param {*} url
* @returns
*/
export const loadOrFetchSubRules = async (url) => {
let rules = await getSubRules(url);
if (!rules || rules.length === 0) {
rules = await syncSubRules(url);
await updateSyncDataCache(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,78 +1,197 @@
import { import {
STOKEY_SYNC, APP_LCNAME,
DEFAULT_SYNC,
KV_SETTING_KEY, KV_SETTING_KEY,
KV_RULES_KEY, KV_RULES_KEY,
STOKEY_SETTING, KV_WORDS_KEY,
STOKEY_RULES, KV_RULES_SHARE_KEY,
KV_SALT_SHARE,
OPT_SYNCTYPE_WEBDAV,
} from "../config"; } from "../config";
import storage from "../libs/storage"; import {
import { getSetting, getRules } from "."; getSyncWithDefault,
updateSync,
getSettingWithDefault,
getRulesWithDefault,
getWordsWithDefault,
setSetting,
setRules,
setWords,
} from "./storage";
import { apiSyncData } from "../apis"; import { apiSyncData } from "../apis";
import { sha256, removeEndchar } from "./utils";
import { createClient, getPatcher } from "webdav";
import { fetchPatcher } from "./fetch";
import { kissLog } from "./log";
const loadOpt = async () => (await storage.getObj(STOKEY_SYNC)) || DEFAULT_SYNC; getPatcher().patch("request", (opts) => {
return fetchPatcher(opts.url, {
method: opts.method,
headers: opts.headers,
body: opts.data,
});
});
export const syncSetting = async () => { const syncByWebdav = async (data, { syncUrl, syncUser, syncKey }) => {
try { const client = createClient(syncUrl, {
const { syncUrl, syncKey, settingUpdateAt } = await loadOpt(); username: syncUser,
if (!syncUrl || !syncKey) { password: syncKey,
return; });
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;
} }
}
const setting = await getSetting(); await client.putFileContents(filename, JSON.stringify(data, null, 2));
const res = await apiSyncData(syncUrl, syncKey, { return data;
key: KV_SETTING_KEY, };
value: setting,
updateAt: settingUpdateAt,
});
if (res && res.updateAt > settingUpdateAt) { const syncByWorker = async (data, { syncUrl, syncKey }) => {
await storage.putObj(STOKEY_SYNC, { syncUrl = removeEndchar(syncUrl, "/");
settingUpdateAt: res.updateAt, return await apiSyncData(`${syncUrl}/sync`, syncKey, data);
settingSyncAt: res.updateAt, };
});
await storage.setObj(STOKEY_SETTING, res.value); const syncData = async (key, valueFn) => {
} else { const {
await storage.putObj(STOKEY_SYNC, { syncType,
settingSyncAt: res.updateAt, syncUrl,
}); syncUser,
} syncKey,
} catch (err) { syncMeta = {},
console.log("[sync setting]", err); } = 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 () => {
const res = await syncData(KV_SETTING_KEY, getSettingWithDefault);
if (res?.isNew) {
await setSetting(res.value);
} }
}; };
export const syncRules = async () => { export const trySyncSetting = async () => {
try { try {
const { syncUrl, syncKey, rulesUpdateAt } = await loadOpt(); await syncSetting();
if (!syncUrl || !syncKey) {
return;
}
const rules = await getRules();
const res = await apiSyncData(syncUrl, syncKey, {
key: KV_RULES_KEY,
value: rules,
updateAt: rulesUpdateAt,
});
if (res && res.updateAt > rulesUpdateAt) {
await storage.putObj(STOKEY_SYNC, {
rulesUpdateAt: res.updateAt,
rulesSyncAt: res.updateAt,
});
await storage.setObj(STOKEY_RULES, res.value);
} else {
await storage.putObj(STOKEY_SYNC, {
rulesSyncAt: res.updateAt,
});
}
} catch (err) { } catch (err) {
console.log("[sync rules]", err); kissLog(err, "sync setting");
} }
}; };
export const syncAll = async () => { /**
* 同步规则
* @returns
*/
const syncRules = async () => {
const res = await syncData(KV_RULES_KEY, getRulesWithDefault);
if (res?.isNew) {
await setRules(res.value);
}
};
export const trySyncRules = async () => {
try {
await syncRules();
} catch (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");
}
};
/**
* 同步分享规则
* @param {*} param0
* @returns
*/
export const syncShareRules = async ({ rules, syncUrl, syncKey }) => {
const data = {
key: KV_RULES_SHARE_KEY,
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}/rules?psk=${psk}`;
return shareUrl;
};
/**
* 同步个人设置和规则
* @returns
*/
export const syncSettingAndRules = async () => {
await syncSetting(); await syncSetting();
await syncRules(); await syncRules();
await syncWords();
};
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

@@ -3,26 +3,67 @@ import {
APP_LCNAME, APP_LCNAME,
TRANS_MIN_LENGTH, TRANS_MIN_LENGTH,
TRANS_MAX_LENGTH, TRANS_MAX_LENGTH,
EVENT_KISS,
MSG_TRANS_CURRULE, MSG_TRANS_CURRULE,
MSG_INJECT_JS,
MSG_INJECT_CSS,
OPT_STYLE_DASHLINE,
OPT_STYLE_FUZZY,
SHADOW_KEY,
OPT_TIMING_PAGESCROLL,
OPT_TIMING_PAGEOPEN,
OPT_TIMING_MOUSEOVER,
DEFAULT_TRANS_APIS,
DEFAULT_FETCH_LIMIT,
DEFAULT_FETCH_INTERVAL,
} from "../config"; } from "../config";
import { StoragesProvider } from "../hooks/Storage";
import { queryEls } from ".";
import Content from "../views/Content"; import Content from "../views/Content";
import { fetchUpdate, fetchClear } from "./fetch"; 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";
/** /**
* 翻译类 * 翻译类
*/ */
export class Translator { export class Translator {
_rule = {}; _rule = {};
_setting = {};
_rootNodes = new Set();
_tranNodes = new Map();
_skipNodeNames = [
APP_LCNAME,
"style",
"svg",
"img",
"audio",
"video",
"textarea",
"input",
"button",
"select",
"option",
"head",
"script",
"iframe",
];
_eventName = genEventName();
_mouseoverNode = null;
_keepSelector = [null, null];
_terms = [];
_docTitle = "";
// 显示
_interseObserver = new IntersectionObserver( _interseObserver = new IntersectionObserver(
(intersections) => { (entries, observer) => {
intersections.forEach((intersection) => { entries.forEach((entry) => {
if (intersection.isIntersecting) { if (entry.isIntersecting) {
this._render(intersection.target); observer.unobserve(entry.target);
this._interseObserver.unobserve(intersection.target); this._render(entry.target);
} }
}); });
}, },
@@ -31,28 +72,82 @@ export class Translator {
} }
); );
// 变化
_mutaObserver = new MutationObserver((mutations) => { _mutaObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => { mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => { if (
try { !this._skipNodeNames.includes(mutation.target.localName) &&
queryEls(this.rule.selector, node).forEach((el) => { mutation.addedNodes.length > 0
this._interseObserver.observe(el); ) {
}); const nodes = Array.from(mutation.addedNodes).filter((node) => {
} catch (err) { if (
// this._skipNodeNames.includes(node.localName) ||
node.id === APP_LCNAME
) {
return false;
}
return true;
});
if (nodes.length > 0) {
// const rootNode = mutation.target.getRootNode();
// todo
this._reTranslate();
} }
}); }
}); });
}); });
constructor(rule, { fetchInterval, fetchLimit }) { // 插入 shadowroot
fetchUpdate(fetchInterval, fetchLimit); _overrideAttachShadow = () => {
this.rule = rule; const _this = this;
const _attachShadow = HTMLElement.prototype.attachShadow;
HTMLElement.prototype.attachShadow = function () {
_this._reTranslate();
return _attachShadow.apply(this, arguments);
};
};
_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") { if (rule.transOpen === "true") {
this._register(); this._register();
} }
} }
get setting() {
return this._setting;
}
get eventName() {
return this._eventName;
}
get rule() { get rule() {
// console.log("get rule", this._rule); // console.log("get rule", this._rule);
return this._rule; return this._rule;
@@ -63,8 +158,9 @@ export class Translator {
this._rule = rule; this._rule = rule;
// 广播消息 // 广播消息
const eventName = this._eventName;
window.dispatchEvent( window.dispatchEvent(
new CustomEvent(EVENT_KISS, { new CustomEvent(eventName, {
detail: { detail: {
action: MSG_TRANS_CURRULE, action: MSG_TRANS_CURRULE,
args: rule, args: rule,
@@ -75,6 +171,7 @@ export class Translator {
updateRule = (obj) => { updateRule = (obj) => {
this.rule = { ...this.rule, ...obj }; this.rule = { ...this.rule, ...obj };
this._updatePool(obj.translator);
}; };
toggle = () => { toggle = () => {
@@ -87,67 +184,393 @@ export class Translator {
} }
}; };
_register = () => { toggleStyle = () => {
// 监听节点变化 const textStyle =
this._mutaObserver.observe(document, { this.rule.textStyle === OPT_STYLE_FUZZY
childList: true, ? OPT_STYLE_DASHLINE
subtree: true, : OPT_STYLE_FUZZY;
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) {
kissLog(selector, "querySelectorAll err");
}
return [];
};
_queryFilter = (selector, rootNode) => {
return this._querySelectorAll(selector, rootNode).filter(
(node) => this._queryFilter(selector, node).length === 0
);
};
_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("*"))
queryEls(this.rule.selector).forEach((el) => { .map((item) => item.shadowRoot)
this._interseObserver.observe(el); .filter(Boolean)
.forEach((item) => {
this._queryShadowNodes(selector, item);
});
};
_queryNodes = (rootNode = document) => {
// const childRoots = Array.from(rootNode.querySelectorAll("*"))
// .map((item) => item.shadowRoot)
// .filter(Boolean);
// const childNodes = childRoots.map((item) => this._queryNodes(item));
// const nodes = Array.from(rootNode.querySelectorAll(this.rule.selector));
// return nodes.concat(childNodes).flat();
this._rootNodes.add(rootNode);
this._rule.selector
.split(";")
.map((item) => item.trim())
.filter(Boolean)
.forEach((selector) => {
if (selector.includes(SHADOW_KEY)) {
const [outSelector, inSelector] = selector
.split(SHADOW_KEY)
.map((item) => item.trim());
if (outSelector && inSelector) {
const outNodes = this._querySelectorAll(outSelector, rootNode);
outNodes.forEach((outNode) => {
if (outNode.shadowRoot) {
// this._rootNodes.add(outNode.shadowRoot);
// this._queryFilter(inSelector, outNode.shadowRoot).forEach(
// (item) => {
// if (!this._tranNodes.has(item)) {
// this._tranNodes.set(item, "");
// }
// }
// );
this._queryShadowNodes(inSelector, outNode.shadowRoot);
}
});
}
} else {
this._queryFilter(selector, rootNode).forEach((item) => {
if (!this._tranNodes.has(item)) {
this._tranNodes.set(item, "");
}
});
}
});
};
_register = () => {
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();
this._rootNodes.forEach((node) => {
// 监听节点变化;
this._mutaObserver.observe(node, {
childList: true,
subtree: true,
// characterData: true,
});
}); });
if (
!this._rule.transTiming ||
this._rule.transTiming === OPT_TIMING_PAGESCROLL
) {
// 监听节点显示
this._tranNodes.forEach((_, node) => {
this._interseObserver.observe(node);
});
} 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) => {
// 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 = () => { _unRegister = () => {
// 恢复页面标题
if (this._docTitle) {
document.title = this._docTitle;
this._docTitle = "";
}
// 解除节点变化监听 // 解除节点变化监听
this._mutaObserver.disconnect(); this._mutaObserver.disconnect();
// 解除节点显示监听 // 解除节点显示监听
queryEls(this.rule.selector).forEach((el) => // this._interseObserver.disconnect();
this._interseObserver.unobserve(el)
);
// 移除已插入元素 // 移除键盘监听
queryEls(APP_LCNAME).forEach((el) => el.remove()); window.removeEventListener("keydown", this._handleKeydown);
const { transRemoveHook } = this._rule;
this._tranNodes.forEach((innerHTML, node) => {
if (
!this._rule.transTiming ||
this._rule.transTiming === OPT_TIMING_PAGESCROLL
) {
// 解除节点显示监听
this._interseObserver.unobserve(node);
} else if (this._rule.transTiming !== OPT_TIMING_PAGEOPEN) {
// 移除鼠标悬停监听
// node.style.pointerEvents = "none";
node.removeEventListener("mouseenter", this._handleMouseover);
node.removeEventListener("mouseleave", this._handleMouseout);
}
// 移除/恢复元素
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();
// 清空任务池 // 清空任务池
fetchClear(); clearFetchPool();
}; };
_render = (el) => { _removeInjector = () => {
// 含子元素 document
if (el.querySelector(this.rule.selector)) { .querySelectorAll(`[data-source^="KISS-Calendar"]`)
return; ?.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();
} }
}, 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 (el.querySelector(APP_LCNAME)) { if (traEl) {
return; if (this._rule.transOnly === "true") {
return;
}
const preText = this._tranNodes.get(el);
const curText = el.innerText.trim();
// const traText = traEl.innerText.trim();
// todo
// 1. traText when loading
// 2. replace startsWith
if (curText.startsWith(preText)) {
return;
}
traEl.remove();
}
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", " ");
}
}
} }
// 太长或太短 // 太长或太短
const q = el.innerText.trim(); if (this._invalidLength(q.replace(/\[(\d+)\]/g, "").trim())) {
if (!q || q.length < TRANS_MIN_LENGTH || q.length > TRANS_MAX_LENGTH) {
return; return;
} }
// console.log("---> ", q); // 专业术语
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 span = document.createElement(APP_LCNAME); // 附加样式
span.style.visibility = "visible"; const { selectStyle, parentStyle } = this._rule;
el.appendChild(span); el.style.cssText += selectStyle;
el.style.cssText += if (el.parentElement) {
"-webkit-line-clamp: unset; max-height: none; height: auto;"; el.parentElement.style.cssText += parentStyle;
el.parentElement.style.cssText += }
"-webkit-line-clamp: unset; max-height: none; height: auto;";
const root = createRoot(span); // 插入译文节点
root.render( traEl = document.createElement(APP_LCNAME);
<StoragesProvider> traEl.style.visibility = "visible";
<Content q={q} translator={this} /> // if (this._rule.transOnly === "true") {
</StoragesProvider> // el.innerHTML = "";
); // }
el.appendChild(traEl);
// 渲染译文节点
const root = createRoot(traEl);
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; return number;
}; };
export const limitFloat = (num, min = 0, max = 100) => {
const number = parseFloat(num);
if (Number.isNaN(number) || number < min) {
return min;
} else if (number > max) {
return max;
}
return number;
};
/** /**
* 匹配是否为数组中的值 * 匹配是否为数组中的值
* @param {*} arr * @param {*} arr
@@ -34,7 +44,12 @@ export const matchValue = (arr, val) => {
* @returns * @returns
*/ */
export const sleep = (delay) => export const sleep = (delay) =>
new Promise((resolve) => setTimeout(resolve, delay)); new Promise((resolve) => {
const timer = setTimeout(() => {
clearTimeout(timer);
resolve();
}, delay);
});
/** /**
* 防抖函数 * 防抖函数
@@ -43,11 +58,195 @@ export const sleep = (delay) =>
* @returns * @returns
*/ */
export const debounce = (func, delay = 200) => { export const debounce = (func, delay = 200) => {
let timer; let timer = null;
return (...args) => { return (...args) => {
timer && clearTimeout(timer); timer && clearTimeout(timer);
timer = setTimeout(() => { timer = setTimeout(() => {
func(...args); func(...args);
clearTimeout(timer);
timer = null;
}, delay); }, delay);
}; };
}; };
/**
* 节流函数
* @param {*} func
* @param {*} delay
* @returns
*/
export const throttle = (func, delay = 200) => {
let timer = null;
let cache = null;
return (...args) => {
if (!timer) {
func(...args);
cache = null;
timer = setTimeout(() => {
if (cache) {
func(...cache);
cache = null;
}
clearTimeout(timer);
timer = null;
}, delay);
} else {
cache = args;
}
};
};
/**
* 判断字符串全是某个字符
* @param {*} s
* @param {*} c
* @param {*} i
* @returns
*/
export const isAllchar = (s, c, i = 0) => {
while (i < s.length) {
if (s[i] !== c) {
return false;
}
i++;
}
return true;
};
/**
* 字符串通配符(*)匹配
* @param {*} s
* @param {*} p
* @returns
*/
export const isMatch = (s, p) => {
if (s.length === 0 || p.length === 0) {
return false;
}
p = "*" + p + "*";
let [sIndex, pIndex] = [0, 0];
let [sRecord, pRecord] = [-1, -1];
while (sIndex < s.length && pRecord < p.length) {
if (p[pIndex] === "*") {
pIndex++;
[sRecord, pRecord] = [sIndex, pIndex];
} else if (s[sIndex] === p[pIndex]) {
sIndex++;
pIndex++;
} else if (sRecord + 1 < s.length) {
sRecord++;
[sIndex, pIndex] = [sRecord, pRecord];
} else {
return false;
}
}
if (p.length === pIndex) {
return true;
}
return isAllchar(p, "*", pIndex);
};
/**
* 类型检查
* @param {*} o
* @returns
*/
export const type = (o) => {
const s = Object.prototype.toString.call(o);
return s.match(/\[object (.*?)\]/)[1].toLowerCase();
};
/**
* sha256
* @param {*} text
* @returns
*/
export const sha256 = async (text, salt) => {
const data = new TextEncoder().encode(text + salt);
const digest = await crypto.subtle.digest({ name: "SHA-256" }, data);
return [...new Uint8Array(digest)]
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
};
/**
* 生成随机事件名称
* @returns
*/
export const genEventName = () => btoa(Math.random()).slice(3, 11);
/**
* 判断两个 Set 是否相同
* @param {*} a
* @param {*} b
* @returns
*/
export const isSameSet = (a, b) => {
const s = new Set([...a, ...b]);
return s.size === a.size && s.size === b.size;
};
/**
* 去掉字符串末尾某个字符
* @param {*} s
* @param {*} c
* @param {*} count
* @returns
*/
export const removeEndchar = (s, c, count = 1) => {
let i = s.length;
while (i > s.length - count && s[i - 1] === c) {
i--;
}
return s.slice(0, i);
};
/**
* 匹配字符串及语言标识
* @param {*} str
* @param {*} sign
* @returns
*/
export const matchInputStr = (str, sign) => {
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);
});
};

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

@@ -0,0 +1,158 @@
/**
* 修复程序类型
*/
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";
export const FIXER_ALL = [
FIXER_NONE,
FIXER_BR,
FIXER_BN,
FIXER_BR_DIV,
FIXER_BN_DIV,
];
/**
* 修复过的标记
*/
const fixedSign = "kiss-fixed";
/**
* 采用 `br` 换行网站的修复函数
* 目标是将 `br` 替换成 `p`
* @param {*} node
* @returns
*/
function brFixer(node, tag = "p") {
if (node.hasAttribute(fixedSign)) {
return;
}
node.setAttribute(fixedSign, "true");
const gapTags = ["BR", "WBR"];
const newlineTags = [
"DIV",
"UL",
"OL",
"LI",
"H1",
"H2",
"H3",
"H4",
"H5",
"H6",
"P",
"HR",
"PRE",
"TABLE",
"BLOCKQUOTE",
];
let html = "";
node.childNodes.forEach(function (child, index) {
if (index === 0) {
html += `<${tag} class="kiss-p">`;
}
if (gapTags.indexOf(child.nodeName) !== -1) {
html += `</${tag}><${tag} class="kiss-p">`;
} else if (newlineTags.indexOf(child.nodeName) !== -1) {
html += `</${tag}>${child.outerHTML}<${tag} class="kiss-p">`;
} else if (child.outerHTML) {
html += child.outerHTML;
} else if (child.textContent) {
html += child.textContent;
}
if (index === node.childNodes.length - 1) {
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 = {
[FIXER_BR]: brFixer,
[FIXER_BN]: bnFixer,
[FIXER_BR_DIV]: brDivFixer,
[FIXER_BN_DIV]: bnDivFixer,
};
/**
* 执行fixer
* @param {*} param0
*/
export function runFixer(selector, fixer = "-", rootSelector) {
try {
if (Object.keys(fixerMap).includes(fixer)) {
run(selector, fixerMap[fixer], rootSelector);
}
} catch (err) {
console.error(`[kiss-webfix run]: ${err.message}`);
}
}

View File

@@ -1,16 +1,16 @@
import React from "react"; import React from "react";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import { StoragesProvider } from "./hooks/Storage"; import { SettingProvider } from "./hooks/Setting";
import ThemeProvider from "./hooks/Theme"; import ThemeProvider from "./hooks/Theme";
import Popup from "./views/Popup"; import Popup from "./views/Popup";
const root = ReactDOM.createRoot(document.getElementById("root")); const root = ReactDOM.createRoot(document.getElementById("root"));
root.render( root.render(
<React.StrictMode> <React.StrictMode>
<StoragesProvider> <SettingProvider>
<ThemeProvider> <ThemeProvider>
<Popup /> <Popup />
</ThemeProvider> </ThemeProvider>
</StoragesProvider> </SettingProvider>
</React.StrictMode> </React.StrictMode>
); );

28
src/rules.js Normal file
View File

@@ -0,0 +1,28 @@
import fs from "fs";
import path from "path";
import { BUILTIN_RULES } from "./config/rules";
(() => {
// rules
try {
const data = JSON.stringify(BUILTIN_RULES, null, 2);
const file = path.resolve(
__dirname,
"../build/web/kiss-translator-rules.json"
);
fs.writeFileSync(file, data);
console.info(`Built-in rules generated: ${file}`);
} catch (err) {
console.error(err);
}
// version
try {
var pjson = require("../package.json");
const file = path.resolve(__dirname, "../build/web/version.txt");
fs.writeFileSync(file, pjson.version);
console.info(`Version file generated: ${file}`);
} catch (err) {
console.error(err);
}
})();

View File

@@ -1,57 +1,3 @@
import React from "react"; import { run } from "./common";
import ReactDOM from "react-dom/client";
import Action from "./views/Action";
import createCache from "@emotion/cache";
import { CacheProvider } from "@emotion/react";
import { getSetting, getRules, matchRule, getFab } from "./libs";
import { Translator } from "./libs/translator";
/** run(true);
* 入口函数
*/
(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)
) {
unsafeWindow.GM = GM;
unsafeWindow.APP_NAME = process.env.REACT_APP_NAME;
return;
}
// skip iframe
if (window.self !== window.top) {
return;
}
// 翻译页面
const setting = await getSetting();
const rules = await getRules();
const rule = matchRule(rules, document.location.href, setting);
const translator = new Translator(rule, setting);
// 浮球按钮
const fab = await getFab();
const $action = document.createElement("div");
$action.setAttribute("id", "kiss-translator");
document.body.parentElement.appendChild($action);
const shadowContainer = $action.attachShadow({ mode: "open" });
const 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>
);
})();

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import { useMemo, useState, useEffect } from "react"; import { useState, useEffect, useMemo } from "react";
import LoadingIcon from "./LoadingIcon"; import LoadingIcon from "./LoadingIcon";
import { import {
OPT_STYLE_LINE, OPT_STYLE_LINE,
@@ -6,26 +6,97 @@ import {
OPT_STYLE_DASHLINE, OPT_STYLE_DASHLINE,
OPT_STYLE_WAVYLINE, OPT_STYLE_WAVYLINE,
OPT_STYLE_FUZZY, OPT_STYLE_FUZZY,
OPT_STYLE_HIGHTLIGHT, OPT_STYLE_HIGHLIGHT,
OPT_STYLE_BLOCKQUOTE,
OPT_STYLE_DIY,
DEFAULT_COLOR, DEFAULT_COLOR,
EVENT_KISS,
MSG_TRANS_CURRULE, MSG_TRANS_CURRULE,
} from "../../config"; } from "../../config";
import { useTranslate } from "../../hooks/Translate"; import { useTranslate } from "../../hooks/Translate";
import { styled, css } from "@mui/material/styles";
import { APP_LCNAME } from "../../config";
import interpreter from "../../libs/interpreter";
export default function Content({ q, translator }) { 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 ``;
}
}}
`;
export default function Content({ q, keeps, translator, $el }) {
const [rule, setRule] = useState(translator.rule); const [rule, setRule] = useState(translator.rule);
const [hover, setHover] = useState(false); const { text, sameLang, loading } = useTranslate(q, rule, translator.setting);
const { text, sameLang, loading } = useTranslate(q, rule); const {
const { textStyle, bgColor } = rule; transOpen,
textStyle,
bgColor,
textDiyStyle,
transOnly,
transTag,
transEndHook,
} = rule;
const handleMouseEnter = () => { const { newlineLength } = translator.setting;
setHover(true);
};
const handleMouseLeave = () => {
setHover(false);
};
const handleKissEvent = (e) => { const handleKissEvent = (e) => {
const { action, args } = e.detail; const { action, args } = e.detail;
@@ -34,80 +105,84 @@ export default function Content({ q, translator }) {
setRule(args); setRule(args);
break; break;
default: default:
// console.log(`[popup] kissEvent action skip: ${action}`);
} }
}; };
useEffect(() => { useEffect(() => {
window.addEventListener(EVENT_KISS, handleKissEvent); window.addEventListener(translator.eventName, handleKissEvent);
return () => { return () => {
window.removeEventListener(EVENT_KISS, handleKissEvent); window.removeEventListener(translator.eventName, handleKissEvent);
}; };
}, []); }, [translator.eventName]);
const style = useMemo(() => { useEffect(() => {
const lineColor = bgColor || ""; // 运行钩子函数
switch (textStyle) { if (text && transEndHook?.trim()) {
case OPT_STYLE_LINE: // 下划线 interpreter.run(`exports.transEndHook = ${transEndHook}`);
return { interpreter.exports.transEndHook($el, q, text, keeps);
opacity: hover ? 1 : 0.6,
textDecoration: `underline 2px ${lineColor}`,
textUnderlineOffset: "0.3em",
};
case OPT_STYLE_DOTLINE: // 点状线
return {
opacity: hover ? 1 : 0.6,
textDecoration: `dotted underline 2px ${lineColor}`,
textUnderlineOffset: "0.3em",
};
case OPT_STYLE_DASHLINE: // 虚线
return {
opacity: hover ? 1 : 0.6,
textDecoration: `dashed underline 2px ${lineColor}`,
textUnderlineOffset: "0.3em",
};
case OPT_STYLE_WAVYLINE: // 波浪线
return {
opacity: hover ? 1 : 0.6,
textDecoration: `wavy underline 2px ${lineColor}`,
textUnderlineOffset: "0.3em",
};
case OPT_STYLE_FUZZY: // 模糊
return {
filter: hover ? "none" : "blur(5px)",
transition: "filter 0.2s ease-in-out",
};
case OPT_STYLE_HIGHTLIGHT: // 高亮
return {
color: "#FFF",
backgroundColor: bgColor || DEFAULT_COLOR,
};
default:
return {};
} }
}, [textStyle, hover, bgColor]); }, [$el, q, text, keeps, transEndHook]);
const gap = useMemo(() => {
if (transOnly === "true") {
return "";
}
return q.length >= newlineLength ? <br /> : " ";
}, [q, transOnly, newlineLength]);
const styles = useMemo(
() => ({
textStyle,
textDiyStyle,
bgColor,
as: transTag,
}),
[textStyle, textDiyStyle, bgColor, transTag]
);
if (loading) { if (loading) {
return ( return (
<> <>
{q.length > 40 ? <br /> : " "} {gap}
<LoadingIcon /> <LoadingIcon />
</> </>
); );
} }
if (text && !sameLang) { if (!text || sameLang) {
return;
}
if (
transOnly === "true" &&
transOpen === "true" &&
$el.querySelector(APP_LCNAME)
) {
Array.from($el.childNodes).forEach((el) => {
if (el.localName !== APP_LCNAME) {
el.remove();
}
});
}
if (keeps.length > 0) {
return ( return (
<> <>
{q.length > 40 ? <br /> : " "} {gap}
<span <StyledSpan
style={style} {...styles}
onMouseEnter={handleMouseEnter} dangerouslySetInnerHTML={{
onMouseLeave={handleMouseLeave} __html: text.replace(/\[(\d+)\]/g, (_, p) => keeps[parseInt(p)]),
> }}
{text} />
</span>
</> </>
); );
} }
return (
<>
{gap}
<StyledSpan {...styles}>{text}</StyledSpan>
</>
);
} }

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,6 +10,10 @@ import InfoIcon from "@mui/icons-material/Info";
import DesignServicesIcon from "@mui/icons-material/DesignServices"; import DesignServicesIcon from "@mui/icons-material/DesignServices";
import { useI18n } from "../../hooks/I18n"; import { useI18n } from "../../hooks/I18n";
import SyncIcon from "@mui/icons-material/Sync"; import SyncIcon from "@mui/icons-material/Sync";
import ApiIcon from "@mui/icons-material/Api";
import InputIcon from "@mui/icons-material/Input";
import SelectAllIcon from "@mui/icons-material/SelectAll";
import EventNoteIcon from "@mui/icons-material/EventNote";
function LinkItem({ label, url, icon }) { function LinkItem({ label, url, icon }) {
const match = useMatch(url); const match = useMatch(url);
@@ -36,12 +40,36 @@ export default function Navigator(props) {
url: "/rules", url: "/rules",
icon: <DesignServicesIcon />, icon: <DesignServicesIcon />,
}, },
{
id: "input_translate",
label: i18n("input_translate"),
url: "/input",
icon: <InputIcon />,
},
{
id: "selection_translate",
label: i18n("selection_translate"),
url: "/tranbox",
icon: <SelectAllIcon />,
},
{
id: "apis_setting",
label: i18n("apis_setting"),
url: "/apis",
icon: <ApiIcon />,
},
{ {
id: "sync", id: "sync",
label: i18n("sync_setting"), label: i18n("sync_setting"),
url: "/sync", url: "/sync",
icon: <SyncIcon />, icon: <SyncIcon />,
}, },
{
id: "words",
label: i18n("favorite_words"),
url: "/words",
icon: <EventNoteIcon />,
},
{ id: "about", label: i18n("about"), url: "/about", icon: <InfoIcon /> }, { id: "about", label: i18n("about"), url: "/about", icon: <InfoIcon /> },
]; ];
return ( return (

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -5,57 +5,138 @@ import TextField from "@mui/material/TextField";
import MenuItem from "@mui/material/MenuItem"; import MenuItem from "@mui/material/MenuItem";
import FormControl from "@mui/material/FormControl"; import FormControl from "@mui/material/FormControl";
import Select from "@mui/material/Select"; import Select from "@mui/material/Select";
import { useSetting, useSettingUpdate } from "../../hooks/Setting"; import Link from "@mui/material/Link";
import { limitNumber, debounce } from "../../libs/utils"; import FormHelperText from "@mui/material/FormHelperText";
import { useSetting } from "../../hooks/Setting";
import { limitNumber } from "../../libs/utils";
import { useI18n } from "../../hooks/I18n"; import { useI18n } from "../../hooks/I18n";
import { UI_LANGS } from "../../config"; import { useAlert } from "../../hooks/Alert";
import { useMemo } from "react"; import { isExt } from "../../libs/client";
import Grid from "@mui/material/Grid";
import {
UI_LANGS,
TRANS_NEWLINE_LENGTH,
CACHE_NAME,
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 { useShortcut } from "../../hooks/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);
return (
<ShortcutInput value={shortcut} onChange={setShortcut} label={label} />
);
}
export default function Settings() { export default function Settings() {
const i18n = useI18n(); const i18n = useI18n();
const setting = useSetting(); const { setting, updateSetting } = useSetting();
const updateSetting = useSettingUpdate(); const alert = useAlert();
const { fab, updateFab } = useFab();
const handleChange = useMemo( const handleChange = (e) => {
() => e.preventDefault();
debounce((e) => { let { name, value } = e.target;
e.preventDefault(); switch (name) {
let { name, value } = e.target; case "fetchLimit":
switch (name) { value = limitNumber(value, 1, 100);
case "fetchLimit": break;
value = limitNumber(value, 1, 100); case "fetchInterval":
break; value = limitNumber(value, 0, 5000);
case "fetchInterval": break;
value = limitNumber(value, 0, 5000); case "transInterval":
break; value = limitNumber(value, 100, 5000);
default: break;
} case "minLength":
updateSetting({ value = limitNumber(value, 1, 100);
[name]: value, break;
}); case "maxLength":
}, 500), value = limitNumber(value, 100, 10000);
[updateSetting] break;
); 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({
[name]: value,
});
};
if (!setting) { const handleClearCache = () => {
return; try {
} caches.delete(CACHE_NAME);
alert.success(i18n("clear_success"));
} catch (err) {
kissLog(err, "clear cache");
}
};
const handleImport = async (data) => {
try {
await updateSetting(JSON.parse(data));
} catch (err) {
kissLog(err, "import setting");
}
};
const { const {
uiLang, uiLang,
googleUrl, minLength,
fetchLimit, maxLength,
fetchInterval,
openaiUrl,
openaiKey,
openaiModel,
openaiPrompt,
clearCache, clearCache,
newlineLength = TRANS_NEWLINE_LENGTH,
contextMenuType = 1,
touchTranslate = 2,
blacklist = DEFAULT_BLACKLIST.join(",\n"),
csplist = DEFAULT_CSPLIST.join(",\n"),
transInterval = 500,
langDetector = OPT_TRANS_MICROSOFT,
} = setting; } = setting;
const { isHide = false } = fab || {};
return ( return (
<Box> <Box>
<Stack spacing={3}> <Stack spacing={3}>
<Stack
direction="row"
alignItems="center"
spacing={2}
useFlexGap
flexWrap="wrap"
>
<UploadButton text={i18n("import")} handleImport={handleImport} />
<DownloadButton
handleData={() => JSON.stringify(setting, null, 2)}
text={i18n("export")}
fileName={`kiss-setting_${Date.now()}.json`}
/>
</Stack>
<FormControl size="small"> <FormControl size="small">
<InputLabel>{i18n("ui_lang")}</InputLabel> <InputLabel>{i18n("ui_lang")}</InputLabel>
<Select <Select
@@ -74,74 +155,174 @@ export default function Settings() {
<TextField <TextField
size="small" size="small"
label={i18n("fetch_limit")} label={i18n("min_translate_length")}
type="number" type="number"
name="fetchLimit" name="minLength"
defaultValue={fetchLimit} defaultValue={minLength}
onChange={handleChange} onChange={handleChange}
/> />
<TextField <TextField
size="small" size="small"
label={i18n("fetch_interval")} label={i18n("max_translate_length")}
type="number" type="number"
name="fetchInterval" name="maxLength"
defaultValue={fetchInterval} defaultValue={maxLength}
onChange={handleChange}
/>
<TextField
size="small"
label={i18n("num_of_newline_characters")}
type="number"
name="newlineLength"
defaultValue={newlineLength}
onChange={handleChange}
/>
<TextField
size="small"
label={i18n("translate_interval")}
type="number"
name="transInterval"
defaultValue={transInterval}
onChange={handleChange} onChange={handleChange}
/> />
<FormControl size="small"> <FormControl size="small">
<InputLabel>{i18n("clear_cache")}</InputLabel> <InputLabel>{i18n("touch_translate_shortcut")}</InputLabel>
<Select <Select
name="clearCache" name="touchTranslate"
value={clearCache} value={touchTranslate}
label={i18n("clear_cache")} label={i18n("touch_translate_shortcut")}
onChange={handleChange} onChange={handleChange}
> >
<MenuItem value={false}>{i18n("clear_cache_never")}</MenuItem> {[0, 2, 3, 4].map((item) => (
<MenuItem value={true}>{i18n("clear_cache_restart")}</MenuItem> <MenuItem key={item} value={item}>
{i18n(`touch_tap_${item}`)}
</MenuItem>
))}
</Select> </Select>
</FormControl> </FormControl>
<TextField <FormControl size="small">
size="small" <InputLabel>{i18n("hide_fab_button")}</InputLabel>
label={i18n("google_api")} <Select
name="googleUrl" name="isHide"
defaultValue={googleUrl} value={isHide}
onChange={handleChange} label={i18n("hide_fab_button")}
/> onChange={(e) => {
updateFab({ isHide: e.target.value });
}}
>
<MenuItem value={false}>{i18n("show")}</MenuItem>
<MenuItem value={true}>{i18n("hide")}</MenuItem>
</Select>
</FormControl>
<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>
<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>
</Box>
</>
)}
<TextField <TextField
size="small" size="small"
label={i18n("openai_api")} label={i18n("translate_blacklist")}
name="openaiUrl" helperText={i18n("pattern_helper")}
defaultValue={openaiUrl} name="blacklist"
onChange={handleChange} defaultValue={blacklist}
/>
<TextField
size="small"
type="password"
label={i18n("openai_key")}
name="openaiKey"
defaultValue={openaiKey}
onChange={handleChange}
/>
<TextField
size="small"
label={i18n("openai_model")}
name="openaiModel"
defaultValue={openaiModel}
onChange={handleChange}
/>
<TextField
size="small"
label={i18n("openai_prompt")}
name="openaiPrompt"
defaultValue={openaiPrompt}
onChange={handleChange} onChange={handleChange}
maxRows={10}
multiline multiline
/> />
</Stack> </Stack>

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,56 +5,134 @@ import { useI18n } from "../../hooks/I18n";
import { useSync } from "../../hooks/Sync"; import { useSync } from "../../hooks/Sync";
import Alert from "@mui/material/Alert"; import Alert from "@mui/material/Alert";
import Link from "@mui/material/Link"; import Link from "@mui/material/Link";
import { URL_KISS_WORKER } from "../../config"; import MenuItem from "@mui/material/MenuItem";
import { debounce } from "../../libs/utils"; import LoadingButton from "@mui/lab/LoadingButton";
import { useMemo } from "react"; 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 { useAlert } from "../../hooks/Alert";
import SyncIcon from "@mui/icons-material/Sync";
import { useSetting } from "../../hooks/Setting";
import { kissLog } from "../../libs/log";
export default function SyncSetting() { export default function SyncSetting() {
const i18n = useI18n(); const i18n = useI18n();
const sync = useSync(); const { sync, updateSync } = useSync();
const alert = useAlert();
const [loading, setLoading] = useState(false);
const { reloadSetting } = useSetting();
const handleChange = useMemo( const handleChange = async (e) => {
() => e.preventDefault();
debounce((e) => { const { name, value } = e.target;
e.preventDefault(); await updateSync({
const { name, value } = e.target; [name]: value,
sync.update({ });
[name]: value, };
});
}, 500),
[sync]
);
if (!sync.opt) { const handleSyncTest = async (e) => {
e.preventDefault();
try {
setLoading(true);
await syncSettingAndRules();
await reloadSetting();
alert.success(i18n("sync_success"));
} catch (err) {
kissLog(err, "sync all");
alert.error(i18n("sync_failed"));
} finally {
setLoading(false);
}
};
if (!sync) {
return; return;
} }
const { syncUrl, syncKey } = sync.opt; const {
syncType = OPT_SYNCTYPE_WORKER,
syncUrl = "",
syncUser = "",
syncKey = "",
} = sync;
return ( return (
<Box> <Box>
<Stack spacing={3}> <Stack spacing={3}>
<Alert severity="warning">{i18n("sync_warn")}</Alert> <Alert severity="warning">{i18n("sync_warn")}</Alert>
<TextField
select
size="small"
name="syncType"
value={syncType}
label={i18n("data_sync_type")}
onChange={handleChange}
>
{OPT_SYNCTYPE_ALL.map((item) => (
<MenuItem key={item} value={item}>
{item}
</MenuItem>
))}
</TextField>
<TextField <TextField
size="small" size="small"
label={i18n("data_sync_url")} label={i18n("data_sync_url")}
name="syncUrl" name="syncUrl"
defaultValue={syncUrl} value={syncUrl}
onChange={handleChange} onChange={handleChange}
helperText={ helperText={
<Link href={URL_KISS_WORKER}>{i18n("about_sync_api")}</Link> syncType === OPT_SYNCTYPE_WORKER && (
<Link href={URL_KISS_WORKER} target="_blank">
{i18n("about_sync_api")}
</Link>
)
} }
/> />
{syncType === OPT_SYNCTYPE_WEBDAV && (
<TextField
size="small"
label={i18n("data_sync_user")}
name="syncUser"
value={syncUser}
onChange={handleChange}
/>
)}
<TextField <TextField
size="small" size="small"
type="password" type="password"
label={i18n("data_sync_key")} label={i18n("data_sync_key")}
name="syncKey" name="syncKey"
defaultValue={syncKey} value={syncKey}
onChange={handleChange} onChange={handleChange}
/> />
<Stack
direction="row"
alignItems="center"
spacing={2}
useFlexGap
flexWrap="wrap"
>
<LoadingButton
size="small"
variant="contained"
disabled={!syncUrl || !syncKey || loading}
onClick={handleSyncTest}
startIcon={<SyncIcon />}
loading={loading}
>
{i18n("sync_now")}
</LoadingButton>
</Stack>
</Stack> </Stack>
</Box> </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

@@ -4,16 +4,26 @@ import Rules from "./Rules";
import Setting from "./Setting"; import Setting from "./Setting";
import Layout from "./Layout"; import Layout from "./Layout";
import SyncSetting from "./SyncSetting"; import SyncSetting from "./SyncSetting";
import { StoragesProvider } from "../../hooks/Storage"; import { SettingProvider } from "../../hooks/Setting";
import ThemeProvider from "../../hooks/Theme"; import ThemeProvider from "../../hooks/Theme";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { isGm } from "../../libs/browser"; import { isGm } from "../../libs/client";
import { sleep } from "../../libs/utils"; import { sleep } from "../../libs/utils";
import CircularProgress from "@mui/material/CircularProgress"; import CircularProgress from "@mui/material/CircularProgress";
import { syncAll } from "../../libs/sync"; import { trySyncSettingAndRules } from "../../libs/sync";
import { AlertProvider } from "../../hooks/Alert";
import Link from "@mui/material/Link";
import Divider from "@mui/material/Divider";
import Stack from "@mui/material/Stack";
import { adaptScript } from "../../libs/gm";
import Alert from "@mui/material/Alert";
import Apis from "./Apis";
import InputSetting from "./InputSetting";
import Tranbox from "./Tranbox";
import FavWords from "./FavWords";
export default function Options() { export default function Options() {
const [error, setError] = useState(false); const [error, setError] = useState("");
const [ready, setReady] = useState(false); const [ready, setReady] = useState(false);
useEffect(() => { useEffect(() => {
@@ -22,14 +32,30 @@ export default function Options() {
// 等待GM注入 // 等待GM注入
let i = 0; let i = 0;
for (;;) { for (;;) {
if (window.APP_NAME === process.env.REACT_APP_NAME) { if (window?.APP_INFO?.name === process.env.REACT_APP_NAME) {
setReady(true); const { version, eventName } = window.APP_INFO;
// 检查版本是否一致
if (version !== process.env.REACT_APP_VERSION) {
setError(
`The version of the local script(v${version}) is not the latest version(v${process.env.REACT_APP_VERSION}). 本地脚本之版本(v${version})非最新版(v${process.env.REACT_APP_VERSION})。`
);
return;
}
if (eventName) {
// 注入GM接口
adaptScript(eventName);
}
break; break;
} }
if (++i > 8) { if (++i > 8) {
setError(true); setError(
break; "Time out. Please confirm whether to install or enable KISS Translator GreaseMonkey script? 连接超时,请检查是否安装或启用简约翻译油猴脚本。"
);
return;
} }
await sleep(1000); await sleep(1000);
@@ -37,49 +63,65 @@ export default function Options() {
} }
// 同步数据 // 同步数据
syncAll(); await trySyncSettingAndRules();
setReady(true);
})(); })();
}, []); }, []);
if (error) { if (error) {
return ( return (
<center> <center>
<h2> <Divider>
Please confirm whether to install or enable{" "} <Link
<a href={process.env.REACT_APP_HOMEPAGE}>KISS Translator</a>{" "} href={process.env.REACT_APP_HOMEPAGE}
GreaseMonkey script? >{`KISS Translator v${process.env.REACT_APP_VERSION}`}</Link>
</h2> </Divider>
<h2> <Alert severity="error">{error}</Alert>
<a href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}>Click here</a>{" "} <Stack spacing={2}>
to install, or <a href={process.env.REACT_APP_HOMEPAGE}>click here</a>{" "} <Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}>
for help. Install/Update Userscript for Tampermonkey/Violentmonkey
</h2> </Link>
<Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL}>
Install/Update Userscript for iOS Safari
</Link>
</Stack>
</center> </center>
); );
} }
if (isGm && !ready) { if (!ready) {
return ( return (
<center> <center>
<Divider>
<Link
href={process.env.REACT_APP_HOMEPAGE}
>{`KISS Translator v${process.env.REACT_APP_VERSION}`}</Link>
</Divider>
<CircularProgress /> <CircularProgress />
</center> </center>
); );
} }
return ( return (
<StoragesProvider> <SettingProvider>
<ThemeProvider> <ThemeProvider>
<HashRouter> <AlertProvider>
<Routes> <HashRouter>
<Route path="/" element={<Layout />}> <Routes>
<Route index element={<Setting />} /> <Route path="/" element={<Layout />}>
<Route path="rules" element={<Rules />} /> <Route index element={<Setting />} />
<Route path="sync" element={<SyncSetting />} /> <Route path="rules" element={<Rules />} />
<Route path="about" element={<About />} /> <Route path="input" element={<InputSetting />} />
</Route> <Route path="tranbox" element={<Tranbox />} />
</Routes> <Route path="apis" element={<Apis />} />
</HashRouter> <Route path="sync" element={<SyncSetting />} />
<Route path="words" element={<FavWords />} />
<Route path="about" element={<About />} />
</Route>
</Routes>
</HashRouter>
</AlertProvider>
</ThemeProvider> </ThemeProvider>
</StoragesProvider> </SettingProvider>
); );
} }

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

@@ -0,0 +1,49 @@
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 = () => {
window.open(process.env.REACT_APP_HOMEPAGE, "_blank");
};
return (
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
spacing={2}
>
<Stack direction="row" justifyContent="flex-start" alignItems="center">
<IconButton onClick={handleHomepage}>
<HomeIcon />
</IconButton>
<Typography
component="div"
sx={{
userSelect: "none",
WebkitUserSelect: "none",
fontWeight: "bold",
}}
>
{`${process.env.REACT_APP_NAME} v${process.env.REACT_APP_VERSION}`}
</Typography>
</Stack>
{setShowPopup ? (
<IconButton
onClick={() => {
setShowPopup(false);
}}
>
<CloseIcon />
</IconButton>
) : (
<DarkModeButton />
)}
</Stack>
);
}

View File

@@ -5,27 +5,40 @@ import MenuItem from "@mui/material/MenuItem";
import FormControlLabel from "@mui/material/FormControlLabel"; import FormControlLabel from "@mui/material/FormControlLabel";
import Switch from "@mui/material/Switch"; import Switch from "@mui/material/Switch";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import { sendTabMsg } from "../../libs/msg"; import { sendBgMsg, sendTabMsg, getCurTab } from "../../libs/msg";
import { browser, isExt } from "../../libs/browser"; import { browser } from "../../libs/browser";
import { isExt } from "../../libs/client";
import { useI18n } from "../../hooks/I18n"; import { useI18n } from "../../hooks/I18n";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import Divider from "@mui/material/Divider";
import Header from "./Header";
import { import {
MSG_TRANS_TOGGLE, MSG_TRANS_TOGGLE,
MSG_TRANS_GETRULE, MSG_TRANS_GETRULE,
MSG_TRANS_PUTRULE, MSG_TRANS_PUTRULE,
MSG_OPEN_OPTIONS,
MSG_SAVE_RULE,
MSG_COMMAND_SHORTCUTS,
OPT_TRANS_ALL, OPT_TRANS_ALL,
OPT_LANGS_FROM, OPT_LANGS_FROM,
OPT_LANGS_TO, OPT_LANGS_TO,
OPT_STYLE_ALL, OPT_STYLE_ALL,
} from "../../config"; } 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 }) { export default function Popup({ setShowPopup, translator: tran }) {
const i18n = useI18n(); const i18n = useI18n();
const [rule, setRule] = useState(tran?.rule); const [rule, setRule] = useState(tran?.rule);
const [commands, setCommands] = useState({});
const handleOpenSetting = () => { const handleOpenSetting = () => {
if (isExt) { if (!tran) {
browser?.runtime.openOptionsPage(); browser?.runtime.openOptionsPage();
} else if (isExt) {
sendBgMsg(MSG_OPEN_OPTIONS);
} else { } else {
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank"); window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank");
} }
@@ -36,13 +49,14 @@ export default function Popup({ setShowPopup, translator: tran }) {
try { try {
setRule({ ...rule, transOpen: e.target.checked ? "true" : "false" }); setRule({ ...rule, transOpen: e.target.checked ? "true" : "false" });
if (isExt) { if (!tran) {
await sendTabMsg(MSG_TRANS_TOGGLE); await sendTabMsg(MSG_TRANS_TOGGLE);
} else { } else {
tran.toggle(); tran.toggle();
sendIframeMsg(MSG_TRANS_TOGGLE);
} }
} catch (err) { } catch (err) {
console.log("[toggle trans]", err); kissLog(err, "toggle trans");
} }
}; };
@@ -51,18 +65,41 @@ export default function Popup({ setShowPopup, translator: tran }) {
const { name, value } = e.target; const { name, value } = e.target;
setRule((pre) => ({ ...pre, [name]: value })); setRule((pre) => ({ ...pre, [name]: value }));
if (isExt) { if (!tran) {
await sendTabMsg(MSG_TRANS_PUTRULE, { [name]: value }); await sendTabMsg(MSG_TRANS_PUTRULE, { [name]: value });
} else { } else {
tran.updateRule({ [name]: value }); tran.updateRule({ [name]: value });
sendIframeMsg(MSG_TRANS_PUTRULE, { [name]: value });
} }
} catch (err) { } catch (err) {
console.log("[update rule]", err); kissLog(err, "update rule");
}
};
const handleClearCache = () => {
tryClearCaches();
};
const handleSaveRule = async () => {
try {
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) {
kissLog(err, "save rule");
} }
}; };
useEffect(() => { useEffect(() => {
if (!isExt) { if (tran) {
return; return;
} }
(async () => { (async () => {
@@ -72,15 +109,45 @@ export default function Popup({ setShowPopup, translator: tran }) {
setRule(res.data); setRule(res.data);
} }
} catch (err) { } catch (err) {
console.log("[query rule]", err); kissLog(err, "query rule");
} }
})(); })();
}, []); }, [tran]);
useEffect(() => {
(async () => {
try {
const commands = {};
if (isExt) {
const res = await sendBgMsg(MSG_COMMAND_SHORTCUTS);
res.forEach(({ name, shortcut }) => {
commands[name] = shortcut;
});
} else {
const shortcuts = tran.setting.shortcuts;
if (shortcuts) {
Object.entries(shortcuts).forEach(([key, val]) => {
commands[key] = val.join("+");
});
}
}
setCommands(commands);
} catch (err) {
kissLog(err, "query cmds");
}
})();
}, [tran]);
if (!rule) { if (!rule) {
return ( return (
<Box minWidth={300} sx={{ p: 2 }}> <Box minWidth={300}>
<Stack spacing={3}> {!tran && (
<>
<Header />
<Divider />
</>
)}
<Stack sx={{ p: 2 }} spacing={3}>
<Button variant="text" onClick={handleOpenSetting}> <Button variant="text" onClick={handleOpenSetting}>
{i18n("setting")} {i18n("setting")}
</Button> </Button>
@@ -89,20 +156,37 @@ export default function Popup({ setShowPopup, translator: tran }) {
); );
} }
const { transOpen, translator, fromLang, toLang, textStyle, bgColor } = rule; const { transOpen, translator, fromLang, toLang, textStyle } = rule;
return ( return (
<Box minWidth={300} sx={{ p: 2 }}> <Box minWidth={300}>
<Stack spacing={2}> {!tran && (
<FormControlLabel <>
control={ <Header />
<Switch <Divider />
checked={transOpen === "true"} </>
onChange={handleTransToggle} )}
/> <Stack sx={{ p: 2 }} spacing={2}>
} <Stack
label={i18n("translate")} direction="row"
/> justifyContent="space-between"
alignItems="center"
spacing={2}
>
<FormControlLabel
control={
<Switch
checked={transOpen === "true"}
onChange={handleTransToggle}
/>
}
label={
commands["toggleTranslate"]
? `${i18n("translate_alt")}(${commands["toggleTranslate"]})`
: i18n("translate_alt")
}
/>
</Stack>
<TextField <TextField
select select
@@ -158,7 +242,11 @@ export default function Popup({ setShowPopup, translator: tran }) {
size="small" size="small"
value={textStyle} value={textStyle}
name="textStyle" name="textStyle"
label={i18n("text_style")} label={
commands["toggleStyle"]
? `${i18n("text_style_alt")}(${commands["toggleStyle"]})`
: i18n("text_style_alt")
}
onChange={handleChange} onChange={handleChange}
> >
{OPT_STYLE_ALL.map((item) => ( {OPT_STYLE_ALL.map((item) => (
@@ -168,17 +256,34 @@ export default function Popup({ setShowPopup, translator: tran }) {
))} ))}
</TextField> </TextField>
<TextField {/* {OPT_STYLE_USE_COLOR.includes(textStyle) && (
size="small" <TextField
name="bgColor" size="small"
value={bgColor} name="bgColor"
label={i18n("bg_color")} value={bgColor}
onChange={handleChange} label={i18n("bg_color")}
/> onChange={handleChange}
/>
)} */}
<Button variant="text" onClick={handleOpenSetting}> <Stack
{i18n("setting")} direction="row"
</Button> justifyContent="space-between"
alignItems="center"
spacing={2}
>
<Button variant="text" onClick={handleSaveRule}>
{i18n("save_rule")}
</Button>
{!isExt && (
<Button variant="text" onClick={handleClearCache}>
{i18n("clear_cache")}
</Button>
)}
<Button variant="text" onClick={handleOpenSetting}>
{i18n("setting")}
</Button>
</Stack>
</Stack> </Stack>
</Box> </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>
);
}

Some files were not shown because too many files have changed in this diff Show More