Compare commits
691 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30c2cca2e1 | ||
|
|
af3241b773 | ||
|
|
eca0a63273 | ||
|
|
f15cdb38d6 | ||
|
|
5b8577aaa7 | ||
|
|
0a4a2b46c1 | ||
|
|
53d441b3f5 | ||
|
|
d4c346e40a | ||
|
|
2dcacf71e4 | ||
|
|
9ded6446a7 | ||
|
|
0a6f4a9f02 | ||
|
|
635e588bcc | ||
|
|
7343db78a5 | ||
|
|
ccd457c992 | ||
|
|
97676f114e | ||
|
|
e83c1eb017 | ||
|
|
e417c0106a | ||
|
|
3c09840d35 | ||
|
|
7361a94f8c | ||
|
|
a9bffe3913 | ||
|
|
5322555eba | ||
|
|
172dce2867 | ||
|
|
5735fee36e | ||
|
|
91642d8784 | ||
|
|
9d8f3f4211 | ||
|
|
66d39da80a | ||
|
|
fbd4a31a9c | ||
|
|
3d7e03ddaf | ||
|
|
1f213bf257 | ||
|
|
b38f079611 | ||
|
|
21e639cacd | ||
|
|
bdaf665b7c | ||
|
|
61a515c1d2 | ||
|
|
1b646df908 | ||
|
|
5550f939b2 | ||
|
|
b34fb5a600 | ||
|
|
c0dce5c0b1 | ||
|
|
d56bd2920f | ||
|
|
48ad100a64 | ||
|
|
ef07a172a9 | ||
|
|
f492d47719 | ||
|
|
ac8c07deb4 | ||
|
|
ca48ab639e | ||
|
|
7c5232c1a1 | ||
|
|
4fac7fdfe1 | ||
|
|
f7fc9560d5 | ||
|
|
f7ba744e7f | ||
|
|
315164f142 | ||
|
|
429cab5223 | ||
|
|
deecbc874b | ||
|
|
504f4cafa0 | ||
|
|
6d327d17af | ||
|
|
74290eb52b | ||
|
|
60b9653fd3 | ||
|
|
53e32d3031 | ||
|
|
ed279cf8a1 | ||
|
|
296b898bda | ||
|
|
2325155b1e | ||
|
|
b6ff4aae6a | ||
|
|
f04002fdb8 | ||
|
|
b8cb254f56 | ||
|
|
3983477904 | ||
|
|
f011f5ae38 | ||
|
|
18ecab18df | ||
|
|
793c481221 | ||
|
|
4fee3688ea | ||
|
|
b9693436bb | ||
|
|
6b18d8f934 | ||
|
|
65c325de9a | ||
|
|
8da5aaf259 | ||
|
|
00e8fdd3e6 | ||
|
|
b2eea5d0d7 | ||
|
|
9a8e24f590 | ||
|
|
32c6d45cb0 | ||
|
|
74ce6f2f1f | ||
|
|
573865cf10 | ||
|
|
56d4733e2a | ||
|
|
a8965a01e3 | ||
|
|
beef51ef38 | ||
|
|
b5b3ee8709 | ||
|
|
4f1e01dde0 | ||
|
|
d42ff51de5 | ||
|
|
c39861b7b7 | ||
|
|
d39b9fd73e | ||
|
|
ecab4ab634 | ||
|
|
5e67e15842 | ||
|
|
2af1a8b72c | ||
|
|
2510ed0ebb | ||
|
|
6827985289 | ||
|
|
a095a2c01c | ||
|
|
2033ff6777 | ||
|
|
0c22288833 | ||
|
|
0576150067 | ||
|
|
9cdcf616f7 | ||
|
|
2de10364f3 | ||
|
|
562559a1b0 | ||
|
|
0f0b7313bb | ||
|
|
412fc87d1e | ||
|
|
0a1abab475 | ||
|
|
b63ef8c1aa | ||
|
|
c38a3d439d | ||
|
|
dd395e668f | ||
|
|
8a5ef441f9 | ||
|
|
d9acbe865f | ||
|
|
dc35fef873 | ||
|
|
4e9293ae0f | ||
|
|
36973b8693 | ||
|
|
0ab734d1a5 | ||
|
|
bfce9b525a | ||
|
|
f19b6ef02f | ||
|
|
1992908a85 | ||
|
|
a6bcafa8f6 | ||
|
|
eabcb06eeb | ||
|
|
21dcbfa4c4 | ||
|
|
b6c074a242 | ||
|
|
f6d095d533 | ||
|
|
7a36251d3f | ||
|
|
3fda4d0da9 | ||
|
|
2fa8917d5e | ||
|
|
67149af64b | ||
|
|
0104cb9f29 | ||
|
|
1afe976777 | ||
|
|
d9b4399c57 | ||
|
|
ac7b3b9824 | ||
|
|
359206630d | ||
|
|
96dfee90ab | ||
|
|
9ace600fce | ||
|
|
549b945d0f | ||
|
|
4528a79c87 | ||
|
|
951ce985b5 | ||
|
|
001f04a9ee | ||
|
|
3844d2eb75 | ||
|
|
e593221e02 | ||
|
|
8cc3801dc5 | ||
|
|
251e57ec61 | ||
|
|
769a4f00aa | ||
|
|
9bafc937d5 | ||
|
|
2d0ea09e06 | ||
|
|
aeec5e361c | ||
|
|
71b2d62c9f | ||
|
|
40b3072e5f | ||
|
|
cae9338274 | ||
|
|
4c2781b3b6 | ||
|
|
b2b5bef9f5 | ||
|
|
df8c96569a | ||
|
|
e562f0b851 | ||
|
|
7b2b48f0d1 | ||
|
|
c353c88db8 | ||
|
|
171dbb7509 | ||
|
|
65e8fabe7d | ||
|
|
389f0b6f82 | ||
|
|
039566ded5 | ||
|
|
d18b31692b | ||
|
|
c993c15c92 | ||
|
|
3c5ffc045f | ||
|
|
261bb7aa6f | ||
|
|
96a7a41759 | ||
|
|
d563521eb1 | ||
|
|
a08c42db8b | ||
|
|
8e026238ae | ||
|
|
7412b3a5c8 | ||
|
|
b60b770ed6 | ||
|
|
20c4d6f6eb | ||
|
|
2437c75d75 | ||
|
|
867c2209b1 | ||
|
|
fffa448425 | ||
|
|
b1142b88f1 | ||
|
|
6d95e7debc | ||
|
|
6bafcb0ec0 | ||
|
|
4935abcf33 | ||
|
|
14f74b76bb | ||
|
|
6b9a1a49bb | ||
|
|
533a0e2d5b | ||
|
|
393f1a29d5 | ||
|
|
1dabbfc4de | ||
|
|
7665f8c260 | ||
|
|
eef5e25a00 | ||
|
|
261f29c185 | ||
|
|
2a46939aa5 | ||
|
|
779c9fc850 | ||
|
|
a20a06320d | ||
|
|
943a9e86f0 | ||
|
|
563242c5f1 | ||
|
|
d39a016d5f | ||
|
|
fa87d87011 | ||
|
|
7dc847fca2 | ||
|
|
343edcdbad | ||
|
|
b631703aa6 | ||
|
|
6dd6b73c2f | ||
|
|
2b496bda31 | ||
|
|
17c8d198c3 | ||
|
|
86f8d9694d | ||
|
|
3948cb74ca | ||
|
|
4ebced1e71 | ||
|
|
d4e58fc925 | ||
|
|
2bfb27f346 | ||
|
|
c4fba1c905 | ||
|
|
4a5e6c2a23 | ||
|
|
5fb7157f57 | ||
|
|
a8c38d2a00 | ||
|
|
cbc82fff64 | ||
|
|
5c44ba1da8 | ||
|
|
fd2f0e513b | ||
|
|
7fd2a0f187 | ||
|
|
fd355eeeab | ||
|
|
c93e370370 | ||
|
|
57d218a17f | ||
|
|
ffc43a67f2 | ||
|
|
7deb5b885a | ||
|
|
5d5e23482f | ||
|
|
36c1e40d64 | ||
|
|
c6f4fe2b7b | ||
|
|
1a23627193 | ||
|
|
d73b2377bf | ||
|
|
4559ab7ec2 | ||
|
|
1d9679e516 | ||
|
|
4c30f6b012 | ||
|
|
511210939f | ||
|
|
d2addf58cb | ||
|
|
f7db410235 | ||
|
|
dd18b04cea | ||
|
|
58d8009e91 | ||
|
|
663407b95d | ||
|
|
f78901603e | ||
|
|
5e1101baeb | ||
|
|
2ba2441900 | ||
|
|
4446ae3dbd | ||
|
|
72668f0386 | ||
|
|
a9b858ec6f | ||
|
|
e1f902c203 | ||
|
|
be6e34ba52 | ||
|
|
39b3b00117 | ||
|
|
763019f0c5 | ||
|
|
d743271be8 | ||
|
|
992dad26aa | ||
|
|
9bd0e67474 | ||
|
|
5767a4afb2 | ||
|
|
9e4c510684 | ||
|
|
16607fb069 | ||
|
|
e047a06432 | ||
|
|
9e09fd898a | ||
|
|
799c32a871 | ||
|
|
483f33b5c9 | ||
|
|
d444fd4fba | ||
|
|
0aae93ba2e | ||
|
|
9608bea3bf | ||
|
|
a038a1ecdc | ||
|
|
c82cdd7f8f | ||
|
|
0c6d5c3c61 | ||
|
|
900426f359 | ||
|
|
1d760fc93a | ||
|
|
61571e0f61 | ||
|
|
c9eb423c89 | ||
|
|
45b294a121 | ||
|
|
3a3f1fabe1 | ||
|
|
03177a09b3 | ||
|
|
cae391f62b | ||
|
|
2a5e9db079 | ||
|
|
650d6e8b41 | ||
|
|
1daf134b31 | ||
|
|
e1dfa35c6c | ||
|
|
73f80692d3 | ||
|
|
42c7dae495 | ||
|
|
b2a1309caa | ||
|
|
94bf5f9580 | ||
|
|
704ebdc9d7 | ||
|
|
165da4e559 | ||
|
|
d3e3b484bf | ||
|
|
192f8faa5b | ||
|
|
579d5cb0a3 | ||
|
|
07fca5b9af | ||
|
|
866a63ab6c | ||
|
|
97b4935bc4 | ||
|
|
30129abef3 | ||
|
|
24d904b32c | ||
|
|
5f0ce57ead | ||
|
|
51f58d095a | ||
|
|
d22e3838c4 | ||
|
|
adbb421b7b | ||
|
|
eaa47af269 | ||
|
|
a6cb5544f8 | ||
|
|
9e91faa660 | ||
|
|
8636fadc72 | ||
|
|
0621957592 | ||
|
|
8ec06b0c84 | ||
|
|
d47f8d7ee9 | ||
|
|
24f8959525 | ||
|
|
983740578b | ||
|
|
b5f79ed7cd | ||
|
|
bbb0e79d4e | ||
|
|
471dc05897 | ||
|
|
7a772d2459 | ||
|
|
1d92421960 | ||
|
|
aeaaf429d7 | ||
|
|
84432e98ae | ||
|
|
77c6102de7 | ||
|
|
ab5dd82169 | ||
|
|
23e7b69dc5 | ||
|
|
3dc8f393f2 | ||
|
|
8a2144f263 | ||
|
|
c1c59caa10 | ||
|
|
d27ebd90b6 | ||
|
|
467745c1e9 | ||
|
|
537378a038 | ||
|
|
4b5ed30e5b | ||
|
|
52b7f6a225 | ||
|
|
f31675d8a2 | ||
|
|
dd46a8450c | ||
|
|
b0843f7d66 | ||
|
|
daadc0195c | ||
|
|
298dec6957 | ||
|
|
bf39d85dfa | ||
|
|
30a9de25a8 | ||
|
|
af1ecf0bd4 | ||
|
|
fe55a2cd3c | ||
|
|
5a33d4e57e | ||
|
|
7f46a9023c | ||
|
|
dfd943b621 | ||
|
|
7007d0d922 | ||
|
|
601678500d | ||
|
|
9bfb504381 | ||
|
|
8a03b0cf15 | ||
|
|
11ba89de0a | ||
|
|
bac7f62eea | ||
|
|
eef90ea02b | ||
|
|
0650df534a | ||
|
|
9ef8c8b823 | ||
|
|
d7e08da0b2 | ||
|
|
ef361e0798 | ||
|
|
6855332092 | ||
|
|
121d523e02 | ||
|
|
42a375c4c7 | ||
|
|
a1dd705d97 | ||
|
|
71f90b36ca | ||
|
|
37facdc3c1 | ||
|
|
66b4f547ff | ||
|
|
d27b9c7f2d | ||
|
|
278ff9c6bc | ||
|
|
d6fe1ce9d7 | ||
|
|
0bfa5256b8 | ||
|
|
72ccfc8aec | ||
|
|
d117c5dc10 | ||
|
|
9312783f44 | ||
|
|
e5b16ebfd3 | ||
|
|
5d1d65c2d3 | ||
|
|
9ca1309cec | ||
|
|
a03afc05f5 | ||
|
|
0198963584 | ||
|
|
58e745d967 | ||
|
|
377e347d68 | ||
|
|
bac0704d3d | ||
|
|
d2ff46edf6 | ||
|
|
f908372b4e | ||
|
|
5d44ff4913 | ||
|
|
4c9aa66048 | ||
|
|
b6a09b99ab | ||
|
|
3a0dcb1a52 | ||
|
|
5015503b4c | ||
|
|
16423feea4 | ||
|
|
9703514698 | ||
|
|
de7a97fb76 | ||
|
|
319aaf8132 | ||
|
|
74bc58ba91 | ||
|
|
d622db0d7c | ||
|
|
de1ddf2362 | ||
|
|
32c0fc860b | ||
|
|
1938f432dd | ||
|
|
a5cfb0ca1d | ||
|
|
a172234fb0 | ||
|
|
63f989b31a | ||
|
|
2ae5d01d5c | ||
|
|
130f1deed1 | ||
|
|
5880d85b48 | ||
|
|
9455670e80 | ||
|
|
e369321c66 | ||
|
|
efc51b0d46 | ||
|
|
d6f3b23b88 | ||
|
|
0a4fa7b9f8 | ||
|
|
2b3e4a8d25 | ||
|
|
bf3a16f96d | ||
|
|
b416e72820 | ||
|
|
ca84bdb227 | ||
|
|
148a4e97a6 | ||
|
|
a13493ebc2 | ||
|
|
ce4ac79e5f | ||
|
|
8f76ea49e7 | ||
|
|
923d3293cd | ||
|
|
7379ff8d15 | ||
|
|
18ebec350d | ||
|
|
3b0cbc53aa | ||
|
|
f00e8ffa4d | ||
|
|
d6f7aad1c3 | ||
|
|
092ea6e836 | ||
|
|
d565e2464a | ||
|
|
2f5d875c47 | ||
|
|
fdb2ddc5f7 | ||
|
|
7a12c5315a | ||
|
|
60d788288d | ||
|
|
dc3c510d57 | ||
|
|
ec6a49f01e | ||
|
|
2b9bfbc20d | ||
|
|
06a51df834 | ||
|
|
6fa183dc56 | ||
|
|
b3cb4049ed | ||
|
|
602b51b1f5 | ||
|
|
a83039577c | ||
|
|
1c77a289a6 | ||
|
|
6278b9124d | ||
|
|
f94cafdbcc | ||
|
|
e13da2caba | ||
|
|
d833fa8dfd | ||
|
|
c921cc59b9 | ||
|
|
7a2c594324 | ||
|
|
0eeb9c2fbf | ||
|
|
ac921cd5a0 | ||
|
|
3ea14c1687 | ||
|
|
6e927473b9 | ||
|
|
1d9e9c1b7d | ||
|
|
54a6189b0c | ||
|
|
85aa9f39c2 | ||
|
|
bda83ce76e | ||
|
|
9ee4c20250 | ||
|
|
fbc70e43e3 | ||
|
|
bc4b4a2171 | ||
|
|
96f9bf6f6f | ||
|
|
9f0986536a | ||
|
|
f668aa7acd | ||
|
|
fc50f4784a | ||
|
|
1fa58cad31 | ||
|
|
469e62557c | ||
|
|
75830aaea7 | ||
|
|
61ef5df559 | ||
|
|
14b5ba9c4c | ||
|
|
9e9c56a3b4 | ||
|
|
3a79f55614 | ||
|
|
af25ee5c11 | ||
|
|
6dd581d5e2 | ||
|
|
746ec019c4 | ||
|
|
2b70f28b0b | ||
|
|
45127646e8 | ||
|
|
83e9c1dd97 | ||
|
|
2eabb7d5ac | ||
|
|
9d4c596b4b | ||
|
|
cc38ab6c45 | ||
|
|
586fa09c7b | ||
|
|
0c45bc5ea8 | ||
|
|
9d9c0633f0 | ||
|
|
47f9635b10 | ||
|
|
68088f5e17 | ||
|
|
77a37cc6df | ||
|
|
420e59bf81 | ||
|
|
dbc5135301 | ||
|
|
8c7d6bb552 | ||
|
|
2b5c1952c0 | ||
|
|
85a82618e5 | ||
|
|
0280ac34c3 | ||
|
|
439900154b | ||
|
|
4a6e902684 | ||
|
|
71bbd2e54a | ||
|
|
3083d8e147 | ||
|
|
e74883e9c2 | ||
|
|
0816a9d167 | ||
|
|
4b3e91fa84 | ||
|
|
0973a0b60e | ||
|
|
de5f61126d | ||
|
|
0c20ca761f | ||
|
|
4bce56207e | ||
|
|
dca54e0033 | ||
|
|
309646bf1d | ||
|
|
18b9961b39 | ||
|
|
1e51ff17f2 | ||
|
|
63b5f707e2 | ||
|
|
30efb6ee7a | ||
|
|
61b017618a | ||
|
|
1e0397adc9 | ||
|
|
48b34bf95f | ||
|
|
d5fc69e210 | ||
|
|
59f9dd697f | ||
|
|
c9d72323f1 | ||
|
|
e87f7f3abe | ||
|
|
82ebbcb6d6 | ||
|
|
2db11070c5 | ||
|
|
5efd2517e7 | ||
|
|
c0ba654678 | ||
|
|
546a5a549b | ||
|
|
cbf02c34e3 | ||
|
|
74a7258f10 | ||
|
|
1006c044bc | ||
|
|
ef4ea719f3 | ||
|
|
8b34afe69f | ||
|
|
01292af298 | ||
|
|
cff8b2fe39 | ||
|
|
2cb20b5cc0 | ||
|
|
8f2aed18fe | ||
|
|
d85831cc9a | ||
|
|
55dc3a5556 | ||
|
|
591afe08bd | ||
|
|
748f2002ab | ||
|
|
d2d18a2384 | ||
|
|
35f4fa6aa7 | ||
|
|
66fc2d22ed | ||
|
|
16cf9ee1ed | ||
|
|
d9d97bf14c | ||
|
|
dc811bd3c7 | ||
|
|
b939d1849a | ||
|
|
beca31f55d | ||
|
|
c7df103950 | ||
|
|
4bf7972ad5 | ||
|
|
534eaed1ed | ||
|
|
7e014e7385 | ||
|
|
34adb2660b | ||
|
|
b6bc165cf0 | ||
|
|
bdd5ed7fc7 | ||
|
|
95d19417c3 | ||
|
|
30ebebdd71 | ||
|
|
e9c557776d | ||
|
|
535a43b698 | ||
|
|
59752ed4aa | ||
|
|
b3e7b8f3f1 | ||
|
|
c4e9365512 | ||
|
|
7d3972d3a8 | ||
|
|
52ca4306fd | ||
|
|
da368ee612 | ||
|
|
22c50e7765 | ||
|
|
7bc39dd1bc | ||
|
|
c80ead6116 | ||
|
|
67e76e4009 | ||
|
|
b213218a30 | ||
|
|
c629a1252c | ||
|
|
64d2481e93 | ||
|
|
e7d6a6add8 | ||
|
|
edc25f7da4 | ||
|
|
5bff84ace1 | ||
|
|
f8bfcba317 | ||
|
|
013a05201b | ||
|
|
433e811821 | ||
|
|
df4cfc0fbc | ||
|
|
1bfb465fd6 | ||
|
|
d5d5ec3fef | ||
|
|
0a32f94d32 | ||
|
|
8067f34ce6 | ||
|
|
214c189a7c | ||
|
|
1f67afc8d8 | ||
|
|
7d4af27919 | ||
|
|
2d651abfdd | ||
|
|
6e06fe79cd | ||
|
|
6093577591 | ||
|
|
4b23ee733f | ||
|
|
46428b7c7f | ||
|
|
6805340a9a | ||
|
|
df36ca8d8b | ||
|
|
fe13de7c30 | ||
|
|
b00f636b72 | ||
|
|
8d074e63e1 | ||
|
|
37989b0089 | ||
|
|
477361eb40 | ||
|
|
94288b5dc3 | ||
|
|
84de1e0f12 | ||
|
|
06f93c1c10 | ||
|
|
450283b80a | ||
|
|
44aeed03a6 | ||
|
|
fa4569415d | ||
|
|
a341bf30ba | ||
|
|
34a7354c84 | ||
|
|
21b5dfbe98 | ||
|
|
c1920f5cdd | ||
|
|
3e24568df9 | ||
|
|
b785cfe854 | ||
|
|
15367bd117 | ||
|
|
d7eaac5aca | ||
|
|
d4526d605c | ||
|
|
52979356ca | ||
|
|
c6d3d6454f | ||
|
|
0d7112187d | ||
|
|
045ff3c3d9 | ||
|
|
dd68a73efd | ||
|
|
5947dc182e | ||
|
|
e185bbdb4d | ||
|
|
9368320c38 | ||
|
|
f65314bc2d | ||
|
|
1791e36038 | ||
|
|
8d93094af5 | ||
|
|
43f34fe6ed | ||
|
|
83911b2164 | ||
|
|
94c7494e90 | ||
|
|
65f2177299 | ||
|
|
f033b11e63 | ||
|
|
02f26af592 | ||
|
|
4125aba808 | ||
|
|
e89da9120c | ||
|
|
160fc218fc | ||
|
|
507d54dba0 | ||
|
|
f3029a0f76 | ||
|
|
53181588cf | ||
|
|
f88aa159fc | ||
|
|
fb2b517a67 | ||
|
|
6e952a9530 | ||
|
|
d9acef8d56 | ||
|
|
113a4d8eca | ||
|
|
6d5b93c01b | ||
|
|
5746911651 | ||
|
|
7173692db7 | ||
|
|
b13a63e568 | ||
|
|
791ec65579 | ||
|
|
dd99fddc07 | ||
|
|
5cd6977a6e | ||
|
|
5af66204c4 | ||
|
|
595efe808f | ||
|
|
7b4b3b020c | ||
|
|
3e96540b56 | ||
|
|
88b791bd73 | ||
|
|
7817019e70 | ||
|
|
6013bbd32c | ||
|
|
40e0b96f39 | ||
|
|
16560fbdf0 | ||
|
|
16fdd704aa | ||
|
|
44e84d9259 | ||
|
|
18d29461ce | ||
|
|
31fb749e93 | ||
|
|
e17931493b | ||
|
|
a395f0b31b | ||
|
|
c819896b43 | ||
|
|
733ec92c9c | ||
|
|
7c67bb7181 | ||
|
|
87f099dd7f | ||
|
|
5306d81284 | ||
|
|
471a4a3159 | ||
|
|
a2f99da3b4 | ||
|
|
accab22d56 | ||
|
|
6ea5228a5f | ||
|
|
a07d2cafb6 | ||
|
|
1b38f19cc1 | ||
|
|
aa5b286e0b | ||
|
|
6b6bbed330 | ||
|
|
489bc9534b | ||
|
|
01ebc184ad | ||
|
|
f591d66365 | ||
|
|
80782287d8 | ||
|
|
3494bb1297 | ||
|
|
92ffda5220 | ||
|
|
fbaeff6b7b | ||
|
|
248d3726dd | ||
|
|
1553559b1a | ||
|
|
8935ced75a | ||
|
|
a865d6d74f | ||
|
|
6d976554fd | ||
|
|
189b7f480a | ||
|
|
5e3aa7e2d1 | ||
|
|
730be678ef | ||
|
|
9293f422f3 | ||
|
|
6e8158bb34 | ||
|
|
3078d3ca91 | ||
|
|
947e1c7f08 | ||
|
|
938c123412 | ||
|
|
e7a57ad3b2 | ||
|
|
1e40f81bf7 | ||
|
|
72b2f44e32 | ||
|
|
76f54461e7 | ||
|
|
14ca13e31d | ||
|
|
556fd71275 | ||
|
|
a8002bba9f | ||
|
|
ddd9371fbd | ||
|
|
0ea97b73e3 | ||
|
|
f8c8a4ebeb | ||
|
|
5f613ab558 | ||
|
|
56281f9e82 | ||
|
|
5e8743dbb7 | ||
|
|
f4e4c84712 | ||
|
|
c57a0a11fa | ||
|
|
fa244b2097 | ||
|
|
79612f8a1b | ||
|
|
2bf79dbc51 | ||
|
|
c2658d5dd0 | ||
|
|
13684884c7 | ||
|
|
f216a9254e | ||
|
|
dbdbcbba2d | ||
|
|
2ee4609192 | ||
|
|
0d93cf78f7 | ||
|
|
3398ca0dd7 | ||
|
|
c1778fbcbb | ||
|
|
1ef9974c05 | ||
|
|
399c6b6fed | ||
|
|
62a60eee44 | ||
|
|
54339af885 | ||
|
|
06cfd33e60 | ||
|
|
08c9d78d2a | ||
|
|
e7a5e5dce1 | ||
|
|
3a59a127d1 | ||
|
|
26f213cad2 | ||
|
|
7b6148302d | ||
|
|
38c781b8f3 |
18
.env
18
.env
@@ -2,28 +2,18 @@ GENERATE_SOURCEMAP=false
|
||||
|
||||
REACT_APP_NAME=KISS Translator
|
||||
REACT_APP_NAME_CN=简约翻译
|
||||
REACT_APP_VERSION=1.6.9
|
||||
REACT_APP_VERSION=2.0.5
|
||||
|
||||
REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator
|
||||
|
||||
REACT_APP_OPTIONSPAGE=https://fishjar.github.io/kiss-translator/options.html
|
||||
REACT_APP_OPTIONSPAGE2=https://kiss-translator.rayjar.com/options
|
||||
REACT_APP_OPTIONSPAGE_DEV=http://localhost:3000/options.html
|
||||
|
||||
REACT_APP_LOGOURL=https://fishjar.github.io/kiss-translator/images/logo192.png
|
||||
REACT_APP_LOGOURL2=https://kiss-translator.rayjar.com/images/logo192.png
|
||||
|
||||
REACT_APP_RULESURL=https://fishjar.github.io/kiss-rules/kiss-rules.json
|
||||
REACT_APP_RULESURL_ON=https://fishjar.github.io/kiss-rules/kiss-rules-on.json
|
||||
REACT_APP_RULESURL_OFF=https://fishjar.github.io/kiss-rules/kiss-rules-off.json
|
||||
|
||||
REACT_APP_WEBFIXURL=https://fishjar.github.io/kiss-rules/kiss-webfix.json
|
||||
|
||||
REACT_APP_VERSIONFILE=https://fishjar.github.io/kiss-translator/version.txt
|
||||
REACT_APP_VERSIONFILE2=https://kiss-translator.rayjar.com/version.txt
|
||||
REACT_APP_RULESURL=https://fishjar.github.io/kiss-rules/kiss-rules_v2.json
|
||||
REACT_APP_RULESURL_ON=https://fishjar.github.io/kiss-rules/kiss-rules-on_v2.json
|
||||
REACT_APP_RULESURL_OFF=https://fishjar.github.io/kiss-rules/kiss-rules-off_v2.json
|
||||
|
||||
REACT_APP_USERSCRIPT_DOWNLOADURL=https://fishjar.github.io/kiss-translator/kiss-translator.user.js
|
||||
REACT_APP_USERSCRIPT_DOWNLOADURL2=https://kiss-translator.rayjar.com/kiss-translator.user.js
|
||||
|
||||
REACT_APP_USERSCRIPT_IOS_DOWNLOADURL=https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js
|
||||
REACT_APP_USERSCRIPT_IOS_DOWNLOADURL2=https://kiss-translator.rayjar.com/kiss-translator-ios-safari.user.js
|
||||
|
||||
40
.github/workflows/release.yml
vendored
40
.github/workflows/release.yml
vendored
@@ -7,25 +7,28 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
node-version: "18.17.0"
|
||||
cache: "yarn"
|
||||
- run: yarn install
|
||||
- run: yarn build
|
||||
- uses: actions/upload-artifact@v3
|
||||
version: latest
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: latest
|
||||
cache: "pnpm"
|
||||
- run: pnpm install
|
||||
- run: pnpm build+zip
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-artifacts
|
||||
path: build
|
||||
deploy-web:
|
||||
needs: build
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/download-artifact@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build-artifacts
|
||||
path: build
|
||||
@@ -34,7 +37,8 @@ jobs:
|
||||
with:
|
||||
folder: build/web
|
||||
create-release:
|
||||
runs-on: ubuntu-22.04
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
upload_url: ${{ steps.create-release.outputs.upload_url }}
|
||||
steps:
|
||||
@@ -51,18 +55,14 @@ jobs:
|
||||
needs: [build, create-release]
|
||||
strategy:
|
||||
matrix:
|
||||
client: ["chrome", "edge", "firefox", "userscript"]
|
||||
runs-on: ubuntu-22.04
|
||||
client: ["chrome", "edge", "firefox", "userscript", "thunderbird"]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/download-artifact@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build-artifacts
|
||||
path: build
|
||||
- name: Zip Release
|
||||
run: |
|
||||
cd build
|
||||
zip -r ${{ matrix.client }}.zip ${{ matrix.client }}
|
||||
- uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,6 +3,7 @@
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
/.obsidian
|
||||
.pnp.js
|
||||
.yarn
|
||||
|
||||
|
||||
4
.prettierignore
Normal file
4
.prettierignore
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
build
|
||||
public
|
||||
package.json
|
||||
24
.prettierrc
Normal file
24
.prettierrc
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"arrowParens": "always",
|
||||
"bracketSpacing": true,
|
||||
"endOfLine": "lf",
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"insertPragma": false,
|
||||
"singleAttributePerLine": false,
|
||||
"bracketSameLine": false,
|
||||
"jsxBracketSameLine": false,
|
||||
"jsxSingleQuote": false,
|
||||
"printWidth": 80,
|
||||
"proseWrap": "preserve",
|
||||
"quoteProps": "as-needed",
|
||||
"requirePragma": false,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5",
|
||||
"useTabs": false,
|
||||
"embeddedLanguageFormatting": "auto",
|
||||
"vueIndentScriptAndStyle": false,
|
||||
"experimentalTernaries": false,
|
||||
"parser": "babel"
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
nodeLinker: node-modules
|
||||
198
README.en.md
198
README.en.md
@@ -1,24 +1,93 @@
|
||||
# KISS Translator
|
||||
|
||||
A minimalist [bilingual translation Extension & Greasemonkey Script](https://github.com/fishjar/kiss-translator).
|
||||
English | [简体中文](README.md)
|
||||
|
||||
A simple, open source [bilingual translation extension & Greasemonkey script](https://github.com/fishjar/kiss-translator).
|
||||
|
||||
[kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f)
|
||||
|
||||
## Inspiration
|
||||
|
||||
The inspiration for this project comes from [Immersive Translate](https://github.com/immersive-translate/immersive-translate). After trying it out, I found that it can be used together with the [Webpage Word Translation Extension](https://github.com/fishjar/kiss-dictionary) developed by me earlier, which just forms a very good supplement.
|
||||
|
||||
But the function of this extension is a bit complicated for me, and only the compiled and obfuscated installation package is provided, and the source code is not provided, which cannot meet some of my personalized customization needs.
|
||||
|
||||
It just so happens that I am obsessed with translation tools. Based on the concept of "mainly for personal use, as long as you can use it", I made one. At present, the first version is completed, which basically meets the needs of personal use.
|
||||
|
||||
If you also like a little more simplicity, welcome to pick it up.
|
||||
|
||||
## Features
|
||||
|
||||
- Keep it simple, smart
|
||||
- [x] Keep it simple, smart
|
||||
- [x] Open source
|
||||
- [x] Adapt to common browsers
|
||||
- [x] Chrome/Edge
|
||||
- [x] Firefox
|
||||
- [x] Kiwi (Android)
|
||||
- [x] Orion (iOS)
|
||||
- [x] Safari
|
||||
- [x] Thunderbird
|
||||
- [x] Supports multiple translation services
|
||||
- [x] Google/Microsoft
|
||||
- [x] Tencent/Volcengine
|
||||
- [x] OpenAI/Gemini/Claude/Ollama/DeepSeek/OpenRouter
|
||||
- [x] DeepL/DeepLX/NiuTrans
|
||||
- [x] AzureAI / CloudflareAI
|
||||
- [x] Chrome built-in AI translation (BuiltinAI)
|
||||
- [x] Covers common translation scenarios
|
||||
- [x] Webpage bilingual translation
|
||||
- [x] Input-box translation
|
||||
- Instantly translate text in input fields into other languages via shortcut keys
|
||||
- [x] Text selection translation
|
||||
- [x] Open translation popup on any page, support multiple translation services for comparison
|
||||
- [x] English dictionary lookup
|
||||
- [x] Save vocabulary
|
||||
- [x] Hover translation
|
||||
- [x] YouTube subtitle translation
|
||||
- Support translating video subtitles with any translation service and display bilingually
|
||||
- Built-in basic subtitle merging and sentence-splitting algorithm to improve translation quality
|
||||
- Supports AI-powered sentence segmentation for even better translation
|
||||
- Custom subtitle style
|
||||
- [x] Supports diverse translation modes
|
||||
- [x] Supports both automatic text recognition and manual rule modes
|
||||
- Automatic text recognition mode allows most sites to be translated fully without writing rules
|
||||
- Manual rule mode enables extreme optimization for specific sites
|
||||
- [x] Custom translation styling
|
||||
- [x] Supports rich-text translation and rendering, preserving links and other text styles where possible
|
||||
- [x] Option to show only translation (hide original text)
|
||||
- [x] Advanced translation API features
|
||||
- [x] With custom API support, theoretically works with any translation service
|
||||
- [x] Batch aggregation of translation requests
|
||||
- [x] Supports AI conversation context memory to improve translation quality
|
||||
- [x] Custom AI terminology dictionary
|
||||
- [x] All APIs support hooks and custom parameters for advanced usage
|
||||
- [x] Cross-client data synchronization
|
||||
- [x] KISS-Worker(cloudflare/docker)
|
||||
- [x] WebDAV
|
||||
- [x] Custom translation rules
|
||||
- [x] Rule subscription/rule sharing
|
||||
- [x] Customized terminology
|
||||
- [x] Custom shortcut keys
|
||||
- `Alt+Q` Toggle Translation
|
||||
- `Alt+C` Toggle Styles
|
||||
- `Alt+K` Open Setting Popup
|
||||
- `Alt+S` Open Translate Popup / Translate Selected Text
|
||||
- `Alt+O` Open Options Page
|
||||
- `Alt+I` Input Box Translation
|
||||
|
||||
## Associated ProjectS
|
||||
## Install
|
||||
|
||||
> Note: For the following reasons, it is recommended to use browser extensions first
|
||||
>
|
||||
> - Browser extensions have more complete functions (local language recognition, context menu, etc.)
|
||||
> - Grease Monkey script will encounter more usage problems (cross domain issues, script conflicts, etc.)
|
||||
|
||||
- [x] Browser extension
|
||||
- [x] Chrome [Installation address](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof?hl=zh-CN)
|
||||
- [x] Kiwi (Android)
|
||||
- [x] Orion (iOS)
|
||||
- [x] Edge [Installation address](https://microsoftedge.microsoft.com/addons/detail/%E7%AE%80%E7%BA%A6%E7%BF%BB%E8%AF%91/jemckldkclkinpjighnoilpbldbdmmlh?hl=zh-CN)
|
||||
- [x] Firefox [Installation address](https://addons.mozilla.org/zh-CN/firefox/addon/kiss-translator/)
|
||||
- [ ] Safari
|
||||
- [ ] Safari (Mac)
|
||||
- [ ] Safari (iOS)
|
||||
- [x] Thunderbird [Download address](https://github.com/fishjar/kiss-translator/releases)
|
||||
- [x] GreaseMonkey Script
|
||||
- [x] Chrome/Edge/Firefox ([Tampermonkey](https://www.tampermonkey.net/)/[Violentmonkey](https://violentmonkey.github.io/)) [Installation link](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)
|
||||
- [Greasy Fork](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator)
|
||||
- [x] iOS Safari ([Userscripts Safari](https://github.com/quoid/userscripts)) [Installation link](https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js)
|
||||
|
||||
## Associated Projects
|
||||
|
||||
- Data synchronization service: [https://github.com/fishjar/kiss-worker](https://github.com/fishjar/kiss-worker)
|
||||
- Data synchronization service available for this project.
|
||||
@@ -27,61 +96,78 @@ If you also like a little more simplicity, welcome to pick it up.
|
||||
- Community subscription rules: [https://github.com/fishjar/kiss-rules](https://github.com/fishjar/kiss-rules)
|
||||
- Provides the latest and most complete list of subscription rules maintained by the community.
|
||||
- Help with rules-related issues.
|
||||
- Web page correction script: [https://github.com/fishjar/kiss-webfixer](https://github.com/fishjar/kiss-webfixer)
|
||||
- Fixed scripts for some special sites.
|
||||
- So that the translation software can get better display effect.
|
||||
- Translation interface agent: [https://github.com/fishjar/kiss-proxy](https://github.com/fishjar/kiss-proxy)
|
||||
- If you encounter network problems when accessing a certain translation interface, this proxy service may help you.
|
||||
- Deploy and manage by yourself.
|
||||
- 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.
|
||||
|
||||
## Description
|
||||
## Frequently Asked Questions
|
||||
|
||||
### Support shortcut keys
|
||||
### How to Set Keyboard Shortcuts
|
||||
|
||||
- `Alt+Q` Toggle Translation
|
||||
- `Alt+C` Toggle Styles
|
||||
- `Alt+K` Open Menu
|
||||
Set this in the extension management page, for example:
|
||||
|
||||
## Schedule
|
||||
- chrome [chrome://extensions/shortcuts](chrome://extensions/shortcuts)
|
||||
- firefox [about:addons](about:addons)
|
||||
|
||||
- [x] Provide trial installation package
|
||||
- [x] Adapt browser
|
||||
- [x] Chrome
|
||||
- [x] Edge
|
||||
- [x] Firefox
|
||||
- [ ] Safari
|
||||
- [x] Kiwi
|
||||
- [x] Support translation services
|
||||
- [x] Google
|
||||
- [x] Microsoft
|
||||
- [x] DeepL
|
||||
- [x] OpenAI
|
||||
- [x] Upload to app Store
|
||||
- [x] Chrome [Install Link](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof)
|
||||
- [x] Edge [Install Link](https://microsoftedge.microsoft.com/addons/detail/kiss-translator/jemckldkclkinpjighnoilpbldbdmmlh)
|
||||
- [x] Firefox [Install Link](https://addons.mozilla.org/en-US/firefox/addon/kiss-translator/)
|
||||
- [ ] Safari
|
||||
- [x] Greasy Fork [Install Link](https://greasyfork.org/en/scripts/472840-kiss-translator)
|
||||
- [x] Open source
|
||||
- [x] Data Synchronization Function
|
||||
- [x] Greasemonkey Script ([Setting Page 1](https://fishjar.github.io/kiss-translator/options.html)、[Setting Page 2](https://kiss-translator.rayjar.com/options))
|
||||
- [x] [Tampermonkey](https://www.tampermonkey.net/) (Chrome/Edge/Firefox) [Install Link 1](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)、[Install Link 2](https://kiss-translator.rayjar.com/kiss-translator.user.js)
|
||||
- [x] [Violentmonkey](https://violentmonkey.github.io/) (Chrome/Edge/Firefox) [Install Link 1](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)、[Install Link 2](https://kiss-translator.rayjar.com/kiss-translator.user.js)
|
||||
- [x] [Userscripts Safari](https://github.com/quoid/userscripts) (iOS Safari) [Install Link 1](https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js)、[Install Link 2](https://kiss-translator.rayjar.com/kiss-translator.user-ios-safari.js)
|
||||
### What is the priority order of rule settings?
|
||||
|
||||
## Guide
|
||||
Personal Rules > Subscription Rules > Global Rules
|
||||
|
||||
Among these, Global Rules have the lowest priority but are very important as they serve as the default rules.
|
||||
|
||||
### API (Ollama, etc.) Test Failure
|
||||
|
||||
Common reasons for API test failures include:
|
||||
|
||||
- Incorrect address:
|
||||
- For example, `Ollama` has a native API address and an `Openai`-compatible address. This plugin currently supports the `Openai`-compatible address and does not support the `Ollama` native API address.
|
||||
- Some AI models do not support batch translation:
|
||||
- In this case, you can choose to disable batch translation or use a custom API.
|
||||
- Alternatively, you can use a custom API. For details, please refer to: [Custom API Example Documentation](https://github.com/fishjar/kiss-translator/blob/master/custom-api_v2.md)
|
||||
- Some AI models have inconsistent parameters:
|
||||
- For example, the parameters of the `Gemini` native API are highly inconsistent. Some model versions do not support certain parameters, leading to errors.
|
||||
- In this case, you can modify the request body using a `Hook`, or replace it with `Gemini2` (an OpenAI-compatible address).
|
||||
- The server restricts cross-origin access, returning a 403 error:
|
||||
- For example, `Ollama` requires adding the environment variable `OLLAMA_ORIGINS=*` when starting. See: https://github.com/fishjar/kiss-translator/issues/174
|
||||
|
||||
### Custom API doesn't work in Tampermonkey scripts
|
||||
|
||||
Tampermonkey scripts require adding domains to the whitelist; otherwise, requests cannot be sent.
|
||||
|
||||
### How to set up a hook function for a custom API
|
||||
|
||||
Custom APIs are very powerful and flexible, and can theoretically connect to any translation API.
|
||||
|
||||
Example reference: [custom-api_v2.md](https://github.com/fishjar/kiss-translator/blob/master/custom-api_v2.md)
|
||||
|
||||
### How to directly access the Tampermonkey script settings page
|
||||
|
||||
Settings page address: https://fishjar.github.io/kiss-translator/options.html
|
||||
|
||||
## Future Plans
|
||||
|
||||
This is a side project with no strict timeline. Community contributions are welcome. The following are preliminary feature directions:
|
||||
|
||||
- [x] **Batch Text Requests**: Optimize request strategy to reduce translation API calls and improve performance.
|
||||
- [x] **Enhanced Rich Text Translation**: Support accurate translation of complex page structures and rich text content.
|
||||
- [x] **Advanced Custom/AI Interfaces**: Add support for context memory, multi-turn conversations, and other advanced AI features.
|
||||
- [x] **Fallback English Dictionary**: When translation services fail, fall back to a local dictionary lookup.
|
||||
- [x] **Improved YouTube Subtitle Support**: Enhance merging and translation experience for streaming subtitles, reducing sentence fragmentation.
|
||||
- [ ] **Upgraded Rule Collaboration System**: Introduce more flexible rule sharing, version management, and community review processes.
|
||||
|
||||
If you're interested in any of these directions, feel free to discuss in [Issues](https://github.com/fishjar/kiss-translator/issues) or submit a PR!
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
```sh
|
||||
git clone https://github.com/fishjar/kiss-translator.git
|
||||
cd kiss-translator
|
||||
yarn install
|
||||
yarn build
|
||||
git checkout dev # Submit a PR suggestion to push to the dev branch
|
||||
pnpm install
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## Discussion
|
||||
|
||||
- Join [Telegram Group](https://t.me/+RRCu_4oNwrM2NmFl)
|
||||
|
||||
## Appreciate
|
||||
|
||||

|
||||
|
||||
192
README.md
192
README.md
@@ -1,22 +1,91 @@
|
||||
# 简约翻译
|
||||
|
||||
一个简约的 [双语网页翻译扩展 & 油猴脚本](https://github.com/fishjar/kiss-translator)。
|
||||
[English](README.en.md) | 简体中文
|
||||
|
||||
一个简约、开源的 [双语对照翻译扩展 & 油猴脚本](https://github.com/fishjar/kiss-translator)。
|
||||
|
||||
[kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f)
|
||||
|
||||
## 缘由
|
||||
## 特性
|
||||
|
||||
本项目灵感来源于 [Immersive Translate](https://github.com/immersive-translate/immersive-translate),在试用了后,发现搭配本人早前开发的 [网页划词翻译扩展](https://github.com/fishjar/kiss-dictionary) 一起使用,刚好形成很好补充。
|
||||
- [x] 保持简约
|
||||
- [x] 开放源代码
|
||||
- [x] 适配常见浏览器
|
||||
- [x] Chrome/Edge
|
||||
- [x] Firefox
|
||||
- [x] Kiwi (Android)
|
||||
- [x] Orion (iOS)
|
||||
- [x] Safari
|
||||
- [x] Thunderbird
|
||||
- [x] 支持多种翻译服务
|
||||
- [x] Google/Microsoft
|
||||
- [x] Tencent/Volcengine
|
||||
- [x] OpenAI/Gemini/Claude/Ollama/DeepSeek/OpenRouter
|
||||
- [x] DeepL/DeepLX/NiuTrans
|
||||
- [x] AzureAI/CloudflareAI
|
||||
- [x] Chrome浏览器内置AI翻译(BuiltinAI)
|
||||
- [x] 覆盖常见翻译场景
|
||||
- [x] 网页双语对照翻译
|
||||
- [x] 输入框翻译
|
||||
- 通过快捷键立即将输入框内文本翻译成其他语言
|
||||
- [x] 划词翻译
|
||||
- [x] 任意页面打开翻译框,可用多种翻译服务对比翻译
|
||||
- [x] 英文词典翻译
|
||||
- [x] 收藏词汇
|
||||
- [x] 鼠标悬停翻译
|
||||
- [x] YouTube 字幕翻译
|
||||
- 支持任意翻译服务对视频字幕进行翻译并双语显示
|
||||
- 内置基础的字幕合并与断句算法,提升翻译效果
|
||||
- 支持AI断句功能,可进一步提升翻译质量
|
||||
- 自定义字幕样式
|
||||
- [x] 支持多样翻译效果
|
||||
- [x] 支持自动识别文本与手动规则两种模式
|
||||
- 自动识别文本模式使得绝大部分网站无需编写规则也能翻译完整
|
||||
- 手动规则模式,可以针对特定网站极致优化
|
||||
- [x] 自定义译文样式
|
||||
- [x] 支持富文本翻译及显示,能够尽量保留原文中的链接及其他文本样式
|
||||
- [x] 支持仅显示译文(隐藏原文)
|
||||
- [x] 翻译接口高级功能
|
||||
- [x] 通过自定义接口,理论上支持任何翻译接口
|
||||
- [x] 聚合批量发送翻译文本
|
||||
- [x] 支持AI上下文会话记忆功能,提升翻译效果
|
||||
- [x] 自定义AI术语词典
|
||||
- [x] 所有接口均支持Hook和自定义参数等高级功能
|
||||
- [x] 跨客户端数据同步
|
||||
- [x] KISS-Worker(cloudflare/docker)
|
||||
- [x] WebDAV
|
||||
- [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
|
||||
- [ ] Safari (Mac)
|
||||
- [ ] Safari (iOS)
|
||||
- [x] Thunderbird [下载地址](https://github.com/fishjar/kiss-translator/releases)
|
||||
- [x] 油猴脚本
|
||||
- [x] Chrome/Edge/Firefox ([Tampermonkey](https://www.tampermonkey.net/)/[Violentmonkey](https://violentmonkey.github.io/)) [安装链接](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)
|
||||
- [Greasy Fork](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator)
|
||||
- [x] iOS Safari ([Userscripts Safari](https://github.com/quoid/userscripts)) [安装链接](https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js)
|
||||
|
||||
## 关联项目
|
||||
|
||||
@@ -27,61 +96,78 @@
|
||||
- 社区订阅规则: [https://github.com/fishjar/kiss-rules](https://github.com/fishjar/kiss-rules)
|
||||
- 提供社区维护的,最新最全的订阅规则列表。
|
||||
- 求助规则相关的问题。
|
||||
- 网页修正脚本: [https://github.com/fishjar/kiss-webfixer](https://github.com/fishjar/kiss-webfixer)
|
||||
- 针对一些特殊网站的修正脚本。
|
||||
- 以便翻译软件得到更好的展示效果。
|
||||
- 翻译接口代理: [https://github.com/fishjar/kiss-proxy](https://github.com/fishjar/kiss-proxy)
|
||||
- 如果访问某个翻译接口遇到网络问题,这个代理服务也许可以帮你到你。
|
||||
- 自己部署,自己管理。
|
||||
- 简约词典插件: [https://github.com/fishjar/kiss-dictionary](https://github.com/fishjar/kiss-dictionary)
|
||||
- 搭配本项目一起使用的划词翻译插件。
|
||||
- 支持英文单词、句子、汉字的查询。
|
||||
- 支持历史记录、单词收藏。
|
||||
|
||||
## 简要说明
|
||||
## 常见问题
|
||||
|
||||
### 支持快捷键
|
||||
### 如何设置快捷键
|
||||
|
||||
- `Alt+Q` 开启翻译
|
||||
- `Alt+C` 切换样式
|
||||
- `Alt+K` 打开菜单
|
||||
在插件管理那里设置,例如:
|
||||
|
||||
## 进度
|
||||
- chrome [chrome://extensions/shortcuts](chrome://extensions/shortcuts)
|
||||
- firefox [about:addons](about:addons)
|
||||
|
||||
- [x] 提供试用安装包
|
||||
- [x] 适配浏览器
|
||||
- [x] Chrome
|
||||
- [x] Edge
|
||||
- [x] Firefox
|
||||
- [ ] Safari
|
||||
- [x] Kiwi
|
||||
- [x] 支持翻译服务
|
||||
- [x] Google
|
||||
- [x] Microsoft
|
||||
- [x] DeepL
|
||||
- [x] OpenAI
|
||||
- [x] 上架应用市场
|
||||
- [x] Chrome [安装地址](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof?hl=zh-CN)
|
||||
- [x] Edge [安装地址](https://microsoftedge.microsoft.com/addons/detail/%E7%AE%80%E7%BA%A6%E7%BF%BB%E8%AF%91/jemckldkclkinpjighnoilpbldbdmmlh?hl=zh-CN)
|
||||
- [x] Firefox [安装地址](https://addons.mozilla.org/zh-CN/firefox/addon/kiss-translator/)
|
||||
- [ ] Safari
|
||||
- [x] Greasy Fork [安装地址](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator)
|
||||
- [x] 开放源代码
|
||||
- [x] 数据同步功能
|
||||
- [x] 油猴脚本 ([设置页面 1](https://fishjar.github.io/kiss-translator/options.html)、[设置页面 2](https://kiss-translator.rayjar.com/options))
|
||||
- [x] [Tampermonkey](https://www.tampermonkey.net/) (Chrome/Edge/Firefox) [安装链接 1](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)、[安装链接 2](https://kiss-translator.rayjar.com/kiss-translator.user.js)
|
||||
- [x] [Violentmonkey](https://violentmonkey.github.io/) (Chrome/Edge/Firefox) [安装链接 1](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)、[安装链接 2](https://kiss-translator.rayjar.com/kiss-translator.user.js)
|
||||
- [x] [Userscripts Safari](https://github.com/quoid/userscripts) (iOS Safari) [安装链接 1](https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js)、[安装链接 2](https://kiss-translator.rayjar.com/kiss-translator.user-ios-safari.js)
|
||||
### 规则设置的优先级是如何的
|
||||
|
||||
## 指引
|
||||
个人规则 > 订阅规则 > 全局规则
|
||||
|
||||
其中全局规则优先级最低,但非常重要,相当于兜底规则。
|
||||
|
||||
### 接口(Ollama等)测试失败
|
||||
|
||||
一般接口测试失败常见有以下几种原因:
|
||||
|
||||
- 地址填错了:
|
||||
- 比如 `Ollama` 有原生接口地址和 `Openai` 兼容的地址,本插件目前统一支持 `Openai` 兼容的地址,不支持 `Ollama` 原生接口地址
|
||||
- 某些AI模型不支持聚合翻译:
|
||||
- 此种情况可以选择禁用聚合翻译或通过自定义接口的方式来使用。
|
||||
- 或通过自定义接口的方式来使用,详情参考: [自定义接口示例文档](https://github.com/fishjar/kiss-translator/blob/master/custom-api_v2.md)
|
||||
- 某些AI模型的参数不一致:
|
||||
- 比如 `Gemini` 原生接口参数非常不一致,部分版本的模型不支持某些参数会导致返回错误。
|
||||
- 此种情况可以通过 `Hook` 修改请求 `body` ,或者更换为 `Gemini2` (`Openai` 兼容的地址)
|
||||
- 服务器跨域限制访问,返回403错误:
|
||||
- 比如 `Ollama` 启动时须添加环境变量 `OLLAMA_ORIGINS=*`, 参考:https://github.com/fishjar/kiss-translator/issues/174
|
||||
|
||||
### 填写的接口在油猴脚本不能使用
|
||||
|
||||
油猴脚本需要增加域名白名单,否则不能发出请求。
|
||||
|
||||
### 如何设置自定义接口的hook函数
|
||||
|
||||
自定义接口功能非常强大、灵活,理论可以接入任何翻译接口。
|
||||
|
||||
示例参考: [custom-api_v2.md](https://github.com/fishjar/kiss-translator/blob/master/custom-api_v2.md)
|
||||
|
||||
### 如何直接进入油猴脚本设置页面
|
||||
|
||||
设置页面地址: https://fishjar.github.io/kiss-translator/options.html
|
||||
|
||||
## 未来规划
|
||||
|
||||
本项目为业余开发,无严格时间表,欢迎社区共建。以下为初步设想的功能方向:
|
||||
|
||||
- [x] **聚合发送文本**:优化请求策略,减少翻译接口调用次数,提升性能。
|
||||
- [x] **增强富文本翻译**:支持更复杂的页面结构和富文本内容的准确翻译。
|
||||
- [x] **强化自定义/AI 接口**:支持上下文记忆、多轮对话等高级 AI 功能。
|
||||
- [x] **英文词典备灾机制**:当翻译服务失效时,可切换其他词典或 fallback 到本地词典查询。
|
||||
- [x] **优化 YouTube 字幕支持**:改进流式字幕的合并与翻译体验,减少断句。
|
||||
- [ ] **规则共建机制升级**:引入更灵活的规则分享、版本管理与社区评审流程。
|
||||
|
||||
如果你对某个方向感兴趣,欢迎在 [Issues](https://github.com/fishjar/kiss-translator/issues) 中讨论或提交 PR!
|
||||
|
||||
## 开发指引
|
||||
|
||||
```sh
|
||||
git clone https://github.com/fishjar/kiss-translator.git
|
||||
cd kiss-translator
|
||||
yarn install
|
||||
yarn build
|
||||
git checkout dev # 提交PR建议推送到dev分支
|
||||
pnpm install
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## 交流
|
||||
|
||||
- 加入 [Telegram 群](https://t.me/+RRCu_4oNwrM2NmFl)
|
||||
|
||||
## 赞赏
|
||||
|
||||

|
||||
|
||||
@@ -32,6 +32,8 @@ const extWebpack = (config, env) => {
|
||||
options: paths.appSrc + "/options.js",
|
||||
background: paths.appSrc + "/background.js",
|
||||
content: paths.appSrc + "/content.js",
|
||||
"injector-subtitle": paths.appSrc + "/injector-subtitle.js",
|
||||
"injector-shadowroot": paths.appSrc + "/injector-shadowroot.js",
|
||||
};
|
||||
|
||||
config.output.filename = "[name].js";
|
||||
@@ -75,7 +77,7 @@ const userscriptWebpack = (config, env) => {
|
||||
// @name ${process.env.REACT_APP_NAME}
|
||||
// @namespace ${process.env.REACT_APP_HOMEPAGE}
|
||||
// @version ${process.env.REACT_APP_VERSION}
|
||||
// @description A minimalist bilingual translation Extension & Greasemonkey Script (一个简约的双语网页翻译扩展 & 油猴脚本)
|
||||
// @description A simple bilingual translation extension & Greasemonkey script (一个简约的双语对照翻译扩展 & 油猴脚本)
|
||||
// @author Gabe<yugang2002@gmail.com>
|
||||
// @homepageURL ${process.env.REACT_APP_HOMEPAGE}
|
||||
// @license GPL-3.0
|
||||
@@ -85,24 +87,41 @@ const userscriptWebpack = (config, env) => {
|
||||
// @updateURL ${process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}
|
||||
// @grant GM.xmlHttpRequest
|
||||
// @grant GM.registerMenuCommand
|
||||
// @grant GM.unregisterMenuCommand
|
||||
// @grant GM.setValue
|
||||
// @grant GM.getValue
|
||||
// @grant GM.deleteValue
|
||||
// @grant GM.info
|
||||
// @grant unsafeWindow
|
||||
// @connect translate.googleapis.com
|
||||
// @connect translate-pa.googleapis.com
|
||||
// @connect generativelanguage.googleapis.com
|
||||
// @connect api-edge.cognitive.microsofttranslator.com
|
||||
// @connect edge.microsoft.com
|
||||
// @connect bing.com
|
||||
// @connect api-free.deepl.com
|
||||
// @connect api.deepl.com
|
||||
// @connect www2.deepl.com
|
||||
// @connect api.openai.com
|
||||
// @connect generativelanguage.googleapis.com
|
||||
// @connect openai.azure.com
|
||||
// @connect workers.dev
|
||||
// @connect github.io
|
||||
// @connect github.com
|
||||
// @connect githubusercontent.com
|
||||
// @connect kiss-translator.rayjar.com
|
||||
// @connect ghproxy.com
|
||||
// @connect localhost:3000
|
||||
// @connect dav.jianguoyun.com
|
||||
// @connect fanyi.baidu.com
|
||||
// @connect transmart.qq.com
|
||||
// @connect niutrans.com
|
||||
// @connect translate.volcengine.com
|
||||
// @connect dict.youdao.com
|
||||
// @connect api.anthropic.com
|
||||
// @connect api.cloudflare.com
|
||||
// @connect openrouter.ai
|
||||
// @connect localhost
|
||||
// @connect 127.0.0.1
|
||||
// @run-at document-end
|
||||
// ==/UserScript==
|
||||
|
||||
|
||||
348
custom-api.md
Normal file
348
custom-api.md
Normal file
@@ -0,0 +1,348 @@
|
||||
# 自定义接口示例(本文档已过期,新版不再适用)
|
||||
|
||||
V2版的示例请查看这里:[custom-api_v2.md](https://github.com/fishjar/kiss-translator/blob/master/custom-api_v2.md)
|
||||
|
||||
以下示例为网友提供,仅供学习参考。
|
||||
|
||||
## 本地运行 Seed-X-PPO-7B 量化模型
|
||||
|
||||
> 由网友 emptyghost6 提供,来源:https://linux.do/t/topic/828257
|
||||
|
||||
URL
|
||||
|
||||
```sh
|
||||
http://localhost:8000/v1/completions
|
||||
```
|
||||
|
||||
Request Hook
|
||||
|
||||
```js
|
||||
(text, from, to, url, key) => {
|
||||
// 模型支持的语言代码到完整名称的映射
|
||||
const langFullNameMap = {
|
||||
ar: 'Arabic', fr: 'French', ms: 'Malay', ru: 'Russian',
|
||||
cs: 'Czech', hr: 'Croatian', nb: 'Norwegian Bokmal', sv: 'Swedish',
|
||||
da: 'Danish', hu: 'Hungarian', nl: 'Dutch', th: 'Thai',
|
||||
de: 'German', id: 'Indonesian', no: 'Norwegian', tr: 'Turkish',
|
||||
en: 'English', it: 'Italian', pl: 'Polish', uk: 'Ukrainian',
|
||||
es: 'Spanish', ja: 'Japanese', pt: 'Portuguese', vi: 'Vietnamese',
|
||||
fi: 'Finnish', ko: 'Korean', ro: 'Romanian', zh: 'Chinese'
|
||||
};
|
||||
|
||||
// 将 Hook 系统的语言代码转换为模型 API 支持的代码
|
||||
const getModelLangCode = (lang) => {
|
||||
if (lang === 'zh-CN' || lang === 'zh-TW') return 'zh';
|
||||
return lang;
|
||||
};
|
||||
|
||||
const sourceLangCode = getModelLangCode(from);
|
||||
const targetLangCode = getModelLangCode(to);
|
||||
|
||||
const sourceLangName = langFullNameMap[sourceLangCode] || from;
|
||||
const targetLangName = langFullNameMap[targetLangCode] || to;
|
||||
|
||||
const prompt = `Translate it to ${targetLangName}:\n${text} <${targetLangCode}>`;
|
||||
|
||||
// 构建请求体对象
|
||||
const bodyObject = {
|
||||
model: "./ByteDance-Seed/Seed-X-PPO-7B-AWQ-Int4",
|
||||
prompt: prompt,
|
||||
max_tokens: 2048,
|
||||
temperature: 0.0,
|
||||
};
|
||||
|
||||
// 返回最终的请求配置
|
||||
return [url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
// 关键改动:将 JavaScript 对象转换为 JSON 字符串
|
||||
body: JSON.stringify(bodyObject),
|
||||
}];
|
||||
}
|
||||
```
|
||||
|
||||
Response Hook
|
||||
|
||||
```js
|
||||
(res, text, from, to) => {
|
||||
// 检查返回是否有效
|
||||
if (res && res.choices && res.choices.length > 0 && res.choices[0].text) {
|
||||
|
||||
// 提取译文并去除可能存在的前后空格
|
||||
const translatedText = res.choices[0].text.trim();
|
||||
|
||||
// 比较原文与译文,相同为 true,否则为 false。
|
||||
const areTextsIdentical = text.trim() === translatedText;
|
||||
|
||||
// 返回数组:[翻译后的文本, 是否与原文相同]
|
||||
return [translatedText, areTextsIdentical];
|
||||
}
|
||||
// 如果响应格式不正确或没有结果,则抛出错误
|
||||
throw new Error("Invalid API response format or no translation found.");
|
||||
}
|
||||
```
|
||||
|
||||
## 接入 openrouter
|
||||
|
||||
> 由网友 Rick Sanchez 提供
|
||||
|
||||
URL
|
||||
|
||||
```sh
|
||||
https://openrouter.ai/api/v1/chat/completions
|
||||
```
|
||||
|
||||
Request Hook
|
||||
|
||||
```js
|
||||
(text, from, to, url, key) => [url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": `Bearer ${key}`,
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"model": "deepseek/deepseek-chat-v3-0324:free", //可自定义你的模型
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": //可自定义你的提示词
|
||||
`You are a professional ${to} native translator. Your task is to produce a fluent, natural, and culturally appropriate translation of the following text from ${from} to ${to}, fully conveying the meaning, tone, and nuance of the original.
|
||||
|
||||
## Translation Rules
|
||||
1. Output only the final polished translation — no explanations, intermediate drafts, or notes.
|
||||
2. Translate in a way that reads naturally to a native ${to} audience, adapting idioms, cultural references, and tone when necessary.
|
||||
3. Preserve proper nouns, technical terms, brand names, and URLs exactly as in the original text unless a widely accepted ${to} equivalent exists.
|
||||
4. Keep any formatting (Markdown, HTML tags, bullet points, numbering) intact and positioned naturally within the translation.
|
||||
5. Adapt humor, metaphors, and figurative language to culturally relevant forms in ${to} while keeping the original intent.
|
||||
6. Maintain the same level of formality or informality as the original.
|
||||
|
||||
Source Text: ${text}
|
||||
|
||||
Translated Text:`
|
||||
}
|
||||
]
|
||||
})
|
||||
}]
|
||||
```
|
||||
|
||||
Response Hook
|
||||
|
||||
```js
|
||||
(res, text, from, to) => [
|
||||
res.choices?.[0]?.message?.content ?? "",
|
||||
false
|
||||
]
|
||||
```
|
||||
|
||||
## 接入 gemini-2.5-flash, 关闭思考模式, 去审查
|
||||
|
||||
> 由网友 Rick Sanchez 提供
|
||||
|
||||
URL
|
||||
|
||||
```sh
|
||||
https://generativelanguage.googleapis.com/v1beta/models
|
||||
```
|
||||
|
||||
Request Hook
|
||||
|
||||
```js
|
||||
(text, from, to, url, key) => [`${url}/gemini-2.5-flash:generateContent?key=${key}`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
"generationConfig": {
|
||||
"temperature": 0.8,
|
||||
"thinkingConfig": {
|
||||
"thinkingBudget": 0, //gemini-2.5-flash设为0关闭思考模式
|
||||
},
|
||||
},
|
||||
"safetySettings": [
|
||||
{
|
||||
"category": "HARM_CATEGORY_HARASSMENT",
|
||||
"threshold": "BLOCK_NONE",
|
||||
},
|
||||
{
|
||||
"category": "HARM_CATEGORY_HATE_SPEECH",
|
||||
"threshold": "BLOCK_NONE",
|
||||
},
|
||||
{
|
||||
"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
||||
"threshold": "BLOCK_NONE",
|
||||
},
|
||||
{
|
||||
"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
|
||||
"threshold": "BLOCK_NONE",
|
||||
}
|
||||
],
|
||||
"contents": [{
|
||||
"parts": [{
|
||||
"text": `自定义提示词`
|
||||
}]
|
||||
}],
|
||||
}),
|
||||
}]
|
||||
```
|
||||
|
||||
Response Hook
|
||||
|
||||
```js
|
||||
(res, text, from, to) => [
|
||||
res.candidates?.[0]?.content?.parts?.[0]?.text ?? "",
|
||||
false
|
||||
]
|
||||
```
|
||||
|
||||
## 接入 Qwen-MT
|
||||
|
||||
> 由网友 atom 提供
|
||||
|
||||
URL
|
||||
|
||||
```sh
|
||||
https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions
|
||||
```
|
||||
|
||||
Request Hook
|
||||
|
||||
```js
|
||||
(text, from, to, url, key) => {
|
||||
const mapLanguageCode = (lang) => ({
|
||||
'zh-CN': 'zh',
|
||||
'zh-TW': 'zh_tw',
|
||||
})[lang] || lang;
|
||||
|
||||
const targetLang = mapLanguageCode(to);
|
||||
|
||||
return [
|
||||
url,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${key}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"model": "qwen-mt-turbo",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": text
|
||||
}
|
||||
],
|
||||
"translation_options": {
|
||||
"source_lang": "auto",
|
||||
"target_lang": targetLang
|
||||
}
|
||||
})
|
||||
}
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
Response Hook
|
||||
|
||||
```js
|
||||
(res, text, from, to) => [res.choices?.[0]?.message?.content ?? "", false]
|
||||
```
|
||||
|
||||
|
||||
## 接入 deepl 接口
|
||||
|
||||
> 来源: https://github.com/fishjar/kiss-translator/issues/101#issuecomment-2123786236
|
||||
|
||||
Request Hook
|
||||
|
||||
```js
|
||||
(text, from, to, url, key) => [
|
||||
url,
|
||||
{
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
text,
|
||||
target_lang: "ZH",
|
||||
source_lang: "auto",
|
||||
}),
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
Response Hook
|
||||
|
||||
```js
|
||||
(res, text, from, to) => [res.data, "ZH" === res.source_lang]
|
||||
```
|
||||
|
||||
## 接入智谱AI大模型
|
||||
|
||||
> 来源: https://github.com/fishjar/kiss-translator/issues/205#issuecomment-2642422679
|
||||
|
||||
Request Hook
|
||||
|
||||
```js
|
||||
(text, from, to, url, key) => [url, {
|
||||
"method": "POST",
|
||||
"headers": {
|
||||
"Content-type": "application/json",
|
||||
"Authorization": key
|
||||
},
|
||||
"body": JSON.stringify({
|
||||
"model": "glm-4-flash",
|
||||
"messages": [
|
||||
{
|
||||
"role":"system",
|
||||
"content": "You are a professional, authentic machine translation engine. You only return the translated text, without any explanations."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": `Translate the following text into ${to}. If translation is unnecessary (e.g. proper nouns, codes, etc.), return the original text. NO explanations. NO notes:\n\n ${text} `
|
||||
}
|
||||
]
|
||||
})
|
||||
}]
|
||||
```
|
||||
|
||||
## 接入谷歌新接口
|
||||
|
||||
> 由网友 Bush2021 提供,来源:https://github.com/fishjar/kiss-translator/issues/225#issuecomment-2810950717
|
||||
|
||||
URL
|
||||
|
||||
```sh
|
||||
https://translate-pa.googleapis.com/v1/translateHtml
|
||||
```
|
||||
|
||||
KEY
|
||||
|
||||
```sh
|
||||
AIzaSyATBXajvzQLTDHEQbcpq0Ihe0vWDHmO520
|
||||
```
|
||||
|
||||
Request Hook
|
||||
|
||||
```js
|
||||
(text, from, to, url, key) => [url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json+protobuf",
|
||||
"X-Goog-API-Key": key
|
||||
},
|
||||
body: JSON.stringify([[[text], from || "auto", to], "wt_lib"])
|
||||
}]
|
||||
```
|
||||
|
||||
Response Hook
|
||||
|
||||
```js
|
||||
(res, text, from, to) => [res?.[0]?.join(" ") || "Translation unavailable", to === res?.[1]?.[0]]
|
||||
```
|
||||
|
||||
|
||||
295
custom-api_v2.md
Normal file
295
custom-api_v2.md
Normal file
@@ -0,0 +1,295 @@
|
||||
# 自定义接口示例
|
||||
|
||||
## 默认接口规范
|
||||
|
||||
如果接口的请求数据和返回数据符合以下规范,
|
||||
则无需填写 `Request Hook` 或 `Response Hook`。
|
||||
|
||||
Request body
|
||||
|
||||
```json
|
||||
{
|
||||
"texts": ["hello"], // 需要翻译的文本列表
|
||||
"from":"auto", // 原文语言
|
||||
"to": "zh-CN" // 目标语言
|
||||
}
|
||||
```
|
||||
|
||||
Response
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"text": "你好", // 译文
|
||||
"src": "en" // 原文语言
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
v2.0.4版后亦支持以下 Response 格式
|
||||
|
||||
```json
|
||||
{
|
||||
"translations": [ // 译文列表
|
||||
{
|
||||
"text": "你好", // 译文
|
||||
"src": "en" // 原文语言
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 谷歌翻译接口
|
||||
|
||||
> 此接口不支持聚合
|
||||
|
||||
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
|
||||
|
||||
```js
|
||||
async (args) => {
|
||||
const url = args.url.replace("{{text}}", args.texts[0]);
|
||||
const method = "GET";
|
||||
return { url, method };
|
||||
};
|
||||
```
|
||||
|
||||
Response Hook
|
||||
|
||||
```js
|
||||
async ({ res }) => {
|
||||
return { translations: [[res?.sentences?.[0]?.trans || "", res?.src]] };
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
## Ollama
|
||||
|
||||
> 此示例为支持聚合的模型类(要支持上下文,需进一步改动)
|
||||
|
||||
* 注意 ollama 启动参数需要添加环境变量 `OLLAMA_ORIGINS=*`
|
||||
* 检查环境变量生效命令:`systemctl show ollama | grep OLLAMA_ORIGINS`
|
||||
|
||||
URL
|
||||
|
||||
```
|
||||
http://localhost:11434/v1/chat/completions
|
||||
```
|
||||
|
||||
Request Hook
|
||||
|
||||
```js
|
||||
async (args) => {
|
||||
const url = args.url;
|
||||
const method = "POST";
|
||||
const headers = { "Content-type": "application/json" };
|
||||
const body = {
|
||||
model: "gemma3",
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content:
|
||||
'Act as a translation API. Output a single raw JSON object only. No extra text or fences.\n\nInput:\n{"targetLanguage":"<lang>","title":"<context>","description":"<context>","segments":[{"id":1,"text":"..."}],"glossary":{"sourceTerm":"targetTerm"},"tone":"<formal|casual>"}\n\nOutput:\n{"translations":[{"id":1,"text":"...","sourceLanguage":"<detected>"}]}\n\nRules:\n1. Use title/description for context only; do not output them.\n2. Keep id, order, and count of segments.\n3. Preserve whitespace, HTML entities, and all HTML-like tags (e.g., <i1>, <a1>). Translate inner text only.\n4. Highest priority: Follow \'glossary\'. Use value for translation; if value is "", keep the key.\n5. Do not translate: content in <code>, <pre>, text enclosed in backticks, or placeholders like {1}, {{1}}, [1], [[1]].\n6. Apply the specified tone to the translation.\n7. Detect sourceLanguage for each segment.\n8. Return empty or unchanged inputs as is.\n\nExample:\nInput: {"targetLanguage":"zh-CN","segments":[{"id":1,"text":"A <b>React</b> component."}],"glossary":{"component":"组件","React":""}}\nOutput: {"translations":[{"id":1,"text":"一个<b>React</b>组件","sourceLanguage":"en"}]}\n\nFail-safe: On any error, return {"translations":[]}.',
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: JSON.stringify({
|
||||
targetLanguage: args.to,
|
||||
segments: args.texts.map((text, id) => ({ id, text })),
|
||||
glossary: {},
|
||||
}),
|
||||
},
|
||||
],
|
||||
temperature: 0,
|
||||
max_tokens: 20480,
|
||||
think: false,
|
||||
stream: false,
|
||||
};
|
||||
|
||||
return { url, body, headers, method };
|
||||
};
|
||||
```
|
||||
|
||||
v2.0.2 Request Hook 可以简化为:
|
||||
|
||||
```js
|
||||
async (args) => {
|
||||
const url = args.url;
|
||||
const method = "POST";
|
||||
const headers = { "Content-type": "application/json" };
|
||||
const body = {
|
||||
model: "gemma3", // v2.0.2 版后此处可填 args.model
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: args.defaultSystemPrompt, // 或者 args.systemPrompt
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: JSON.stringify({
|
||||
targetLanguage: args.to,
|
||||
segments: args.texts.map((text, id) => ({ id, text })),
|
||||
glossary: {},
|
||||
}),
|
||||
},
|
||||
],
|
||||
temperature: 0,
|
||||
max_tokens: 20480,
|
||||
think: false,
|
||||
stream: false,
|
||||
};
|
||||
|
||||
return { url, body, headers, method };
|
||||
};
|
||||
```
|
||||
|
||||
Response Hook
|
||||
|
||||
```js
|
||||
async ({ res }) => {
|
||||
const extractJson = (raw) => {
|
||||
const jsonRegex = /({.*}|\[.*\])/s;
|
||||
const match = raw.match(jsonRegex);
|
||||
return match ? match[0] : null;
|
||||
};
|
||||
|
||||
const parseAIRes = (raw) => {
|
||||
if (!raw) return [];
|
||||
|
||||
try {
|
||||
const jsonString = extractJson(raw);
|
||||
if (!jsonString) return [];
|
||||
|
||||
const data = JSON.parse(jsonString);
|
||||
if (Array.isArray(data.translations)) {
|
||||
return data.translations.map((item) => [
|
||||
item?.text ?? "",
|
||||
item?.sourceLanguage ?? "",
|
||||
]);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("parseAIRes", err);
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
const translations = parseAIRes(res?.choices?.[0]?.message?.content);
|
||||
|
||||
return { translations };
|
||||
};
|
||||
```
|
||||
|
||||
v2.0.2 版后内置`parseAIRes`函数,Response Hook 可以简化为:
|
||||
|
||||
```js
|
||||
async ({ res, parseAIRes }) => {
|
||||
const translations = parseAIRes(res?.choices?.[0]?.message?.content);
|
||||
return { translations };
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
## 硅基流动
|
||||
|
||||
> 此示例为不支持聚合模型类,支持聚合的模型类参考上面 Ollama 的写法
|
||||
|
||||
URL
|
||||
|
||||
```
|
||||
https://api.siliconflow.cn/v1/chat/completions
|
||||
```
|
||||
|
||||
Request Hook
|
||||
|
||||
```js
|
||||
async (args) => {
|
||||
const url = args.url;
|
||||
const method = "POST";
|
||||
const headers = {
|
||||
"Content-type": "application/json",
|
||||
Authorization: `Bearer ${args.key}`,
|
||||
};
|
||||
const body = {
|
||||
model: "tencent/Hunyuan-MT-7B", // v2.0.2 版后此处可填 args.model
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content:
|
||||
"You are a professional, authentic machine translation engine.",
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: `Translate the following source text to ${args.to}. Output translation directly without any additional text.\n\nSource Text: ${args.texts[0]}\n\nTranslated Text:`,
|
||||
},
|
||||
],
|
||||
temperature: 0,
|
||||
max_tokens: 20480,
|
||||
};
|
||||
|
||||
return { url, body, headers, method };
|
||||
};
|
||||
```
|
||||
|
||||
Response Hook
|
||||
|
||||
```js
|
||||
async ({ res }) => {
|
||||
return { translations: [[res?.choices?.[0]?.message?.content || ""]] };
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
## 语言代码表及说明
|
||||
|
||||
Hook参数里面的语言含义说明:
|
||||
|
||||
- `toLang`, `fromLang` 是本插件支持的标准语言代码
|
||||
- `to`, `from` 是转换后的适用于特定接口的语言代码
|
||||
|
||||
如果你的自定义接口与下面的标准语言代码不匹配,需要自行映射转换。
|
||||
|
||||
```
|
||||
["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"],
|
||||
```
|
||||
45
package.json
45
package.json
@@ -1,34 +1,44 @@
|
||||
{
|
||||
"name": "kiss-translator",
|
||||
"description": "A minimalist bilingual translation Extension & Greasemonkey Script",
|
||||
"version": "1.6.9",
|
||||
"version": "2.0.5",
|
||||
"author": "Gabe<yugang2002@gmail.com>",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@emotion/cache": "^11.11.0",
|
||||
"@emotion/css": "^11.13.5",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.10.8",
|
||||
"@mui/icons-material": "^5.11.11",
|
||||
"@mui/material": "^5.11.12",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@mui/icons-material": "^5.15.15",
|
||||
"@mui/lab": "5.0.0-alpha.170",
|
||||
"@mui/material": "^5.15.15",
|
||||
"query-string": "^8.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-router-dom": "^6.10.0",
|
||||
"react-router-dom": "^6.16.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"sval": "^0.5.2",
|
||||
"webdav": "^5.3.0",
|
||||
"webextension-polyfill": "^0.10.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "REACT_APP_CLIENT=web react-app-rewired start",
|
||||
"start:userscript": "REACT_APP_CLIENT=userscript react-app-rewired start",
|
||||
"build:chrome": "rm -rf build/chrome && BUILD_PATH=./build/chrome REACT_APP_CLIENT=chrome react-app-rewired build",
|
||||
"build:chrome": "rm -rf build/chrome && BUILD_PATH=./build/chrome REACT_APP_CLIENT=chrome react-app-rewired build && rm build/chrome/content.html",
|
||||
"build:safari-output": "rm -rf build/safari && BUILD_PATH=./build/safari REACT_APP_CLIENT=safari react-app-rewired build && rm build/safari/content.html",
|
||||
"build:safari": "node src/scripts/build-safari.js",
|
||||
"build:edge": "rm -rf build/edge && cp -r build/chrome build/edge",
|
||||
"build:firefox": "rm -rf build/firefox && cp -r build/chrome build/firefox && cat ./build/firefox/manifest.firefox.json > ./build/firefox/manifest.json",
|
||||
"build:thunderbird": "rm -rf build/thunderbird && BUILD_PATH=./build/thunderbird REACT_APP_CLIENT=thunderbird react-app-rewired build && rm build/thunderbird/content.html && cp ./build/thunderbird/manifest.thunderbird.json ./build/thunderbird/manifest.json && rm build/*/manifest.thunderbird.json",
|
||||
"build:firefox": "rm -rf build/firefox && BUILD_PATH=./build/firefox REACT_APP_CLIENT=firefox react-app-rewired build && rm build/firefox/content.html && cat ./build/firefox/manifest.firefox.json > ./build/firefox/manifest.json && rm build/*/manifest.firefox.json",
|
||||
"build:web": "rm -rf build/web && BUILD_PATH=./build/web REACT_APP_CLIENT=userscript react-app-rewired build",
|
||||
"build:userscript-ios": "file1=build/web/kiss-translator.user.js file2=build/web/kiss-translator-ios-safari.user.js && cp $file1 $file2 && sed -i 's|// @grant unsafeWindow|// @inject-into content|g' $file2",
|
||||
"build:userscript": "rm -rf build/userscript && mkdir build/userscript && cp build/web/*.user.js build/userscript/",
|
||||
"build:rules": "babel-node src/rules.js",
|
||||
"build": "yarn build:chrome && yarn build:edge && yarn build:firefox && yarn build:web && yarn build:userscript-ios && yarn build:userscript && yarn build:rules",
|
||||
"deploy:web": "wrangler pages deploy ./build/web --project-name kiss-translator",
|
||||
"build": "pnpm format && pnpm build:chrome && pnpm build:edge && pnpm build:thunderbird && pnpm build:firefox && pnpm build:web && pnpm build:userscript-ios && pnpm build:userscript && pnpm build:rules",
|
||||
"zip": "cd build && rm -f *.zip && zip -r chrome.zip chrome && zip -r edge.zip edge && zip -r userscript.zip userscript && (cd firefox && zip -r ../firefox.zip .) && (cd thunderbird && zip -r ../thunderbird.zip .)",
|
||||
"build+zip": "pnpm build && pnpm zip",
|
||||
"format": "prettier --write \"**/*.{js,json,html}\"",
|
||||
"test": "react-app-rewired test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
@@ -39,7 +49,11 @@
|
||||
],
|
||||
"globals": {
|
||||
"GM": true,
|
||||
"unsafeWindow": true
|
||||
"unsafeWindow": true,
|
||||
"globalThis": true,
|
||||
"messenger": true,
|
||||
"LanguageDetector": true,
|
||||
"Translator": true
|
||||
}
|
||||
},
|
||||
"browserslist": {
|
||||
@@ -55,11 +69,14 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.22.10",
|
||||
"@babel/node": "^7.22.10",
|
||||
"@babel/core": "^7.22.20",
|
||||
"@babel/node": "^7.22.19",
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@babel/preset-env": "^7.22.10",
|
||||
"@babel/preset-env": "^7.22.20",
|
||||
"dotenv": "^17.2.1",
|
||||
"find-up": "^7.0.0",
|
||||
"prettier": "3.6.2",
|
||||
"react-app-rewired": "^2.2.1",
|
||||
"wrangler": "^3.4.0"
|
||||
"zx": "^8.8.1"
|
||||
}
|
||||
}
|
||||
|
||||
13177
pnpm-lock.yaml
generated
Normal file
13177
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,12 +3,18 @@
|
||||
"message": "KISS Translator"
|
||||
},
|
||||
"app_description": {
|
||||
"message": "A minimalist bilingual translation Extension & Greasemonkey Script"
|
||||
"message": "A simple bilingual translation extension & Greasemonkey script"
|
||||
},
|
||||
"toggle_translate": {
|
||||
"message": "Toggle Translate"
|
||||
},
|
||||
"toggle_style": {
|
||||
"message": "Toggle Style"
|
||||
},
|
||||
"open_options": {
|
||||
"message": "Open Options"
|
||||
},
|
||||
"open_tranbox": {
|
||||
"message": "Translate Popup/Selected"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,18 @@
|
||||
"message": "简约翻译"
|
||||
},
|
||||
"app_description": {
|
||||
"message": "一个简约的双语网页翻译扩展 & 油猴脚本"
|
||||
"message": "一个简约的双语对照翻译扩展 & 油猴脚本"
|
||||
},
|
||||
"toggle_translate": {
|
||||
"message": "切换翻译"
|
||||
"message": "开启翻译"
|
||||
},
|
||||
"toggle_style": {
|
||||
"message": "切换样式"
|
||||
},
|
||||
"open_options": {
|
||||
"message": "打开设置"
|
||||
},
|
||||
"open_tranbox": {
|
||||
"message": "翻译弹窗/选中文字"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
@@ -16,7 +16,7 @@
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// (() => {
|
||||
// var shadow = document.querySelector("#shadow1");
|
||||
// var root = shadow.attachShadow({ mode: "open" });
|
||||
@@ -54,8 +54,8 @@
|
||||
// }, 1000);
|
||||
|
||||
setTimeout(function () {
|
||||
var el = document.querySelector("h2>p>span");
|
||||
el.innerText = "hello world";
|
||||
var el = document.querySelector('h2>p>span');
|
||||
el.innerText = 'hello world';
|
||||
}, 1000);
|
||||
});
|
||||
</script>
|
||||
@@ -64,26 +64,68 @@
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root">
|
||||
<p>You need to enable <code>JavaScript</code> to run <span>this app.</span></p>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<div id="content">
|
||||
<p>You need to enable JavaScript to run <span>this app.</span></p>
|
||||
The <span>embargo</span> has just lifted to confirm that AmpereOne is
|
||||
coming to Google Cloud with the C3A instances.
|
||||
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.
|
||||
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
|
||||
>
|
||||
<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>
|
||||
@@ -122,15 +164,47 @@
|
||||
<br />
|
||||
<br />
|
||||
<h2>
|
||||
React Server Components (or RSC) is a new application architecture
|
||||
designed by the React team.
|
||||
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>
|
||||
src="http://localhost:3000/index.html"></iframe>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<h2>We’ve first shared our research on RSC in an introductory talk and an RFC.</h2>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
@@ -164,52 +238,10 @@
|
||||
<br />
|
||||
<br />
|
||||
<h2>
|
||||
We’ve first shared our research on RSC in an introductory talk and an
|
||||
RFC.
|
||||
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>
|
||||
<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>
|
||||
<iframe id="iframe2" width="800px" height="600px" src="https://react.dev/"></iframe>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
@@ -244,15 +276,14 @@
|
||||
<br />
|
||||
<div class="cont cont1">
|
||||
<h2>
|
||||
Server Components can run during the build, letting you read from the
|
||||
filesystem or fetch static content.
|
||||
Server Components can run during the build, letting you read from the filesystem
|
||||
or fetch static content.
|
||||
</h2>
|
||||
<ul>
|
||||
<li>
|
||||
They can also run on the server, letting you access your data layer
|
||||
without having to build an API. You can pass data by props from
|
||||
Server Components to the interactive Client Components in the
|
||||
browser.
|
||||
They can also run on the server, letting you access your data layer without
|
||||
having to build an API. You can pass data by props from Server Components to
|
||||
the interactive Client Components in the browser.
|
||||
</li>
|
||||
<li>以声明式编写 UI,可以让你的代码更加可靠,且方便调试。</li>
|
||||
</ul>
|
||||
@@ -271,14 +302,14 @@
|
||||
<br />
|
||||
<div class="cont cont2">
|
||||
<h2>
|
||||
Since our last update, we have merged the React Server Components RFC
|
||||
to ratify the proposal.
|
||||
Since our last update, we have merged the React Server Components RFC to ratify
|
||||
the proposal.
|
||||
</h2>
|
||||
<ul>
|
||||
<li>
|
||||
RSC combines the simple “request/response” mental model of
|
||||
server-centric Multi-Page Apps with the seamless interactivity of
|
||||
client-centric Single-Page Apps, giving you the best of both worlds.
|
||||
RSC combines the simple “request/response” mental model of server-centric
|
||||
Multi-Page Apps with the seamless interactivity of client-centric Single-Page
|
||||
Apps, giving you the best of both worlds.
|
||||
</li>
|
||||
<li>
|
||||
React 使创建交互式 UI
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 2,
|
||||
"name": "__MSG_app_name__",
|
||||
"description": "__MSG_app_description__",
|
||||
"version": "1.6.9",
|
||||
"version": "2.0.5",
|
||||
"default_locale": "en",
|
||||
"author": "Gabe<yugang2002@gmail.com>",
|
||||
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
||||
@@ -12,10 +12,14 @@
|
||||
"content_scripts": [
|
||||
{
|
||||
"js": ["content.js"],
|
||||
"matches": ["<all_urls>"],
|
||||
"matches": ["<all_urls>", "file://*/*"],
|
||||
"all_frames": true
|
||||
}
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
"injector-subtitle.js",
|
||||
"injector-shadowroot.js"
|
||||
],
|
||||
"commands": {
|
||||
"_execute_browser_action": {
|
||||
"suggested_key": {
|
||||
@@ -28,14 +32,29 @@
|
||||
},
|
||||
"description": "__MSG_toggle_translate__"
|
||||
},
|
||||
"openTranbox": {
|
||||
"suggested_key": {
|
||||
"default": "Alt+S"
|
||||
},
|
||||
"description": "__MSG_open_tranbox__"
|
||||
},
|
||||
"toggleStyle": {
|
||||
"suggested_key": {
|
||||
"default": "Alt+C"
|
||||
},
|
||||
"description": "__MSG_toggle_style__"
|
||||
},
|
||||
"openOptions": {
|
||||
"description": "__MSG_open_options__"
|
||||
}
|
||||
},
|
||||
"permissions": ["<all_urls>", "storage"],
|
||||
"permissions": [
|
||||
"<all_urls>",
|
||||
"storage",
|
||||
"contextMenus",
|
||||
"scripting",
|
||||
"declarativeNetRequest"
|
||||
],
|
||||
"icons": {
|
||||
"16": "images/logo16.png",
|
||||
"32": "images/logo32.png",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 3,
|
||||
"name": "__MSG_app_name__",
|
||||
"description": "__MSG_app_description__",
|
||||
"version": "1.6.9",
|
||||
"version": "2.0.5",
|
||||
"default_locale": "en",
|
||||
"author": "Gabe<yugang2002@gmail.com>",
|
||||
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
||||
@@ -13,10 +13,20 @@
|
||||
"content_scripts": [
|
||||
{
|
||||
"js": ["content.js"],
|
||||
"matches": ["<all_urls>"],
|
||||
"matches": ["<all_urls>", "file://*/*"],
|
||||
"all_frames": true
|
||||
}
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": ["injector-subtitle.js"],
|
||||
"matches": ["https://www.youtube.com/*"]
|
||||
},
|
||||
{
|
||||
"resources": ["injector-shadowroot.js"],
|
||||
"matches": ["<all_urls>"]
|
||||
}
|
||||
],
|
||||
"commands": {
|
||||
"_execute_action": {
|
||||
"suggested_key": {
|
||||
@@ -29,14 +39,23 @@
|
||||
},
|
||||
"description": "__MSG_toggle_translate__"
|
||||
},
|
||||
"openTranbox": {
|
||||
"suggested_key": {
|
||||
"default": "Alt+S"
|
||||
},
|
||||
"description": "__MSG_open_tranbox__"
|
||||
},
|
||||
"toggleStyle": {
|
||||
"suggested_key": {
|
||||
"default": "Alt+C"
|
||||
},
|
||||
"description": "__MSG_toggle_style__"
|
||||
},
|
||||
"openOptions": {
|
||||
"description": "__MSG_open_options__"
|
||||
}
|
||||
},
|
||||
"permissions": ["storage"],
|
||||
"permissions": ["storage", "contextMenus", "scripting", "declarativeNetRequest", "declarativeNetRequestWithHostAccess"],
|
||||
"host_permissions": ["<all_urls>"],
|
||||
"icons": {
|
||||
"16": "images/logo16.png",
|
||||
|
||||
82
public/manifest.thunderbird.json
Normal file
82
public/manifest.thunderbird.json
Normal file
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "__MSG_app_name__",
|
||||
"description": "__MSG_app_description__",
|
||||
"version": "2.0.5",
|
||||
"default_locale": "en",
|
||||
"author": "Gabe<yugang2002@gmail.com>",
|
||||
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "yugang2002@gmail.com",
|
||||
"strict_min_version": "78.0"
|
||||
}
|
||||
},
|
||||
"background": {
|
||||
"scripts": ["background.js"]
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"js": ["content.js"],
|
||||
"matches": ["<all_urls>", "file://*/*"],
|
||||
"all_frames": true
|
||||
}
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
"injector-subtitle.js",
|
||||
"injector-shadowroot.js"
|
||||
],
|
||||
"commands": {
|
||||
"_execute_browser_action": {
|
||||
"suggested_key": {
|
||||
"default": "Alt+K"
|
||||
}
|
||||
},
|
||||
"toggleTranslate": {
|
||||
"suggested_key": {
|
||||
"default": "Alt+Q"
|
||||
},
|
||||
"description": "__MSG_toggle_translate__"
|
||||
},
|
||||
"openTranbox": {
|
||||
"suggested_key": {
|
||||
"default": "Alt+S"
|
||||
},
|
||||
"description": "__MSG_open_tranbox__"
|
||||
},
|
||||
"toggleStyle": {
|
||||
"suggested_key": {
|
||||
"default": "Alt+C"
|
||||
},
|
||||
"description": "__MSG_toggle_style__"
|
||||
},
|
||||
"openOptions": {
|
||||
"description": "__MSG_open_options__"
|
||||
}
|
||||
},
|
||||
"permissions": [
|
||||
"<all_urls>",
|
||||
"storage",
|
||||
"menus",
|
||||
"messagesModify",
|
||||
"scripting",
|
||||
"declarativeNetRequest"
|
||||
],
|
||||
"icons": {
|
||||
"16": "images/logo16.png",
|
||||
"32": "images/logo32.png",
|
||||
"48": "images/logo48.png",
|
||||
"128": "images/logo128.png"
|
||||
},
|
||||
"browser_action": {
|
||||
"default_icon": {
|
||||
"128": "images/logo128.png"
|
||||
},
|
||||
"default_title": "__MSG_app_name__",
|
||||
"default_popup": "popup.html"
|
||||
},
|
||||
"options_ui": {
|
||||
"page": "options.html",
|
||||
"open_in_tab": true
|
||||
}
|
||||
}
|
||||
19
src/apis/baidu.js
Normal file
19
src/apis/baidu.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { DEFAULT_USER_AGENT } from "../config";
|
||||
|
||||
export const genBaidu = ({ texts, from, to }) => {
|
||||
const body = {
|
||||
from,
|
||||
to,
|
||||
query: texts.join(" "),
|
||||
source: "txt",
|
||||
};
|
||||
|
||||
const url = "https://fanyi.baidu.com/transapi";
|
||||
const headers = {
|
||||
// Origin: "https://fanyi.baidu.com",
|
||||
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
"User-Agent": DEFAULT_USER_AGENT,
|
||||
};
|
||||
|
||||
return { url, body, headers };
|
||||
};
|
||||
50
src/apis/deepl.js
Normal file
50
src/apis/deepl.js
Normal file
@@ -0,0 +1,50 @@
|
||||
let id = 1e4 * Math.round(1e4 * Math.random());
|
||||
|
||||
export const genDeeplFree = ({ texts, from, to }) => {
|
||||
const text = texts.join(" ");
|
||||
const iCount = (text.match(/[i]/g) || []).length + 1;
|
||||
let timestamp = Date.now();
|
||||
timestamp = timestamp + (iCount - (timestamp % iCount));
|
||||
id++;
|
||||
|
||||
const url = "https://www2.deepl.com/jsonrpc";
|
||||
|
||||
const body = {
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const 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",
|
||||
};
|
||||
|
||||
return { url, body, headers };
|
||||
};
|
||||
39
src/apis/history.js
Normal file
39
src/apis/history.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import { DEFAULT_CONTEXT_SIZE } from "../config";
|
||||
|
||||
const historyMap = new Map();
|
||||
|
||||
const MsgHistory = (maxSize = DEFAULT_CONTEXT_SIZE) => {
|
||||
const messages = [];
|
||||
|
||||
const add = (...msgs) => {
|
||||
messages.push(...msgs.filter(Boolean));
|
||||
const extra = messages.length - maxSize;
|
||||
if (extra > 0) {
|
||||
messages.splice(0, extra);
|
||||
}
|
||||
};
|
||||
|
||||
const getAll = () => {
|
||||
return [...messages];
|
||||
};
|
||||
|
||||
const clear = () => {
|
||||
messages.length = 0;
|
||||
};
|
||||
|
||||
return {
|
||||
add,
|
||||
getAll,
|
||||
clear,
|
||||
};
|
||||
};
|
||||
|
||||
export const getMsgHistory = (apiSlug, maxSize) => {
|
||||
if (historyMap.has(apiSlug)) {
|
||||
return historyMap.get(apiSlug);
|
||||
}
|
||||
|
||||
const msgHistory = MsgHistory(maxSize);
|
||||
historyMap.set(apiSlug, msgHistory);
|
||||
return msgHistory;
|
||||
};
|
||||
@@ -1,18 +1,33 @@
|
||||
import queryString from "query-string";
|
||||
import { fetchPolyfill } from "../libs/fetch";
|
||||
import { fetchData } from "../libs/fetch";
|
||||
import {
|
||||
OPT_TRANS_GOOGLE,
|
||||
OPT_TRANS_MICROSOFT,
|
||||
OPT_TRANS_DEEPL,
|
||||
OPT_TRANS_OPENAI,
|
||||
OPT_TRANS_CUSTOMIZE,
|
||||
OPT_LANGS_SPECIAL,
|
||||
PROMPT_PLACE_FROM,
|
||||
PROMPT_PLACE_TO,
|
||||
URL_CACHE_TRAN,
|
||||
URL_CACHE_DELANG,
|
||||
URL_CACHE_BINGDICT,
|
||||
KV_SALT_SYNC,
|
||||
OPT_LANGS_TO_SPEC,
|
||||
OPT_LANGS_SPEC_DEFAULT,
|
||||
API_SPE_TYPES,
|
||||
DEFAULT_API_SETTING,
|
||||
OPT_TRANS_MICROSOFT,
|
||||
MSG_BUILTINAI_DETECT,
|
||||
MSG_BUILTINAI_TRANSLATE,
|
||||
OPT_TRANS_BUILTINAI,
|
||||
URL_CACHE_SUBTITLE,
|
||||
OPT_LANGS_TO_CODE,
|
||||
} from "../config";
|
||||
import { tryDetectLang } from "../libs";
|
||||
import { sha256 } from "../libs/utils";
|
||||
import { sha256, withTimeout } from "../libs/utils";
|
||||
import {
|
||||
handleTranslate,
|
||||
handleSubtitle,
|
||||
handleMicrosoftLangdetect,
|
||||
} from "./trans";
|
||||
import { getHttpCachePolyfill, putHttpCachePolyfill } from "../libs/cache";
|
||||
import { getBatchQueue } from "../libs/batchQueue";
|
||||
import { isBuiltinAIAvailable } from "../libs/browser";
|
||||
import { chromeDetect, chromeTranslate } from "../libs/builtinAI";
|
||||
import { fnPolyfill } from "../libs/fetch";
|
||||
import { getFetchPool } from "../libs/pool";
|
||||
|
||||
/**
|
||||
* 同步数据
|
||||
@@ -21,222 +36,376 @@ import { sha256 } from "../libs/utils";
|
||||
* @param {*} data
|
||||
* @returns
|
||||
*/
|
||||
export const apiSyncData = async (url, key, data, isBg = false) =>
|
||||
fetchPolyfill(url, {
|
||||
export const apiSyncData = async (url, key, data) =>
|
||||
fetchData(url, {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
Authorization: `Bearer ${await sha256(key, KV_SALT_SYNC)}`,
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
isBg,
|
||||
});
|
||||
|
||||
/**
|
||||
* 下载数据
|
||||
* @param {*} url
|
||||
* @param {*} isBg
|
||||
* @returns
|
||||
*/
|
||||
export const apiFetch = (url, isBg = false) => fetchPolyfill(url, { isBg });
|
||||
export const apiFetch = (url) => fetchData(url);
|
||||
|
||||
/**
|
||||
* 谷歌翻译
|
||||
* @param {*} text
|
||||
* @param {*} to
|
||||
* @param {*} from
|
||||
* Microsoft token
|
||||
* @returns
|
||||
*/
|
||||
const apiGoogleTranslate = async (
|
||||
translator,
|
||||
text,
|
||||
to,
|
||||
from,
|
||||
{ url, key, useCache = true }
|
||||
) => {
|
||||
export const apiMsAuth = async () =>
|
||||
fetchData("https://edge.microsoft.com/translate/auth");
|
||||
|
||||
/**
|
||||
* Google语言识别
|
||||
* @param {*} text
|
||||
* @returns
|
||||
*/
|
||||
export const apiGoogleLangdetect = async (text) => {
|
||||
const params = {
|
||||
client: "gtx",
|
||||
dt: "t",
|
||||
dj: 1,
|
||||
ie: "UTF-8",
|
||||
sl: from,
|
||||
tl: to,
|
||||
sl: "auto",
|
||||
tl: "zh-CN",
|
||||
q: text,
|
||||
};
|
||||
const input = `${url}?${queryString.stringify(params)}`;
|
||||
const res = await fetchPolyfill(input, {
|
||||
const input = `https://translate.googleapis.com/translate_a/single?${queryString.stringify(params)}`;
|
||||
const init = {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
useCache,
|
||||
usePool: true,
|
||||
translator,
|
||||
token: key,
|
||||
});
|
||||
const trText = res.sentences.map((item) => item.trans).join(" ");
|
||||
const isSame = to === res.src;
|
||||
|
||||
return [trText, isSame];
|
||||
};
|
||||
|
||||
/**
|
||||
* 微软翻译
|
||||
* @param {*} text
|
||||
* @param {*} to
|
||||
* @param {*} from
|
||||
* @returns
|
||||
*/
|
||||
const apiMicrosoftTranslate = async (
|
||||
translator,
|
||||
text,
|
||||
to,
|
||||
from,
|
||||
{ url, useCache = true }
|
||||
) => {
|
||||
const params = {
|
||||
from,
|
||||
to,
|
||||
"api-version": "3.0",
|
||||
};
|
||||
const input = `${url}?${queryString.stringify(params)}`;
|
||||
const res = await fetchPolyfill(input, {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify([{ Text: text }]),
|
||||
useCache,
|
||||
usePool: true,
|
||||
translator,
|
||||
});
|
||||
const trText = res[0].translations[0].text;
|
||||
const isSame = to === res[0].detectedLanguage?.language;
|
||||
const res = await fetchData(input, init, { useCache: true });
|
||||
|
||||
return [trText, isSame];
|
||||
};
|
||||
|
||||
/**
|
||||
* DeepL翻译
|
||||
* @param {*} text
|
||||
* @param {*} to
|
||||
* @param {*} from
|
||||
* @returns
|
||||
*/
|
||||
const apiDeepLTranslate = async (
|
||||
translator,
|
||||
text,
|
||||
to,
|
||||
from,
|
||||
{ url, key, useCache = true }
|
||||
) => {
|
||||
const data = {
|
||||
text: [text],
|
||||
target_lang: to,
|
||||
split_sentences: "0",
|
||||
};
|
||||
if (from) {
|
||||
data.source_lang = from;
|
||||
if (res?.src) {
|
||||
await putHttpCachePolyfill(input, init, res);
|
||||
return res.src;
|
||||
}
|
||||
const res = await fetchPolyfill(url, {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
useCache,
|
||||
usePool: true,
|
||||
translator,
|
||||
token: key,
|
||||
});
|
||||
const trText = res.translations.map((item) => item.text).join(" ");
|
||||
const isSame = to === res.translations[0].detected_source_language;
|
||||
|
||||
return [trText, isSame];
|
||||
return "";
|
||||
};
|
||||
|
||||
/**
|
||||
* OpenAI 翻译
|
||||
* Microsoft语言识别
|
||||
* @param {*} text
|
||||
* @param {*} to
|
||||
* @param {*} from
|
||||
* @returns
|
||||
*/
|
||||
const apiOpenaiTranslate = async (
|
||||
translator,
|
||||
text,
|
||||
to,
|
||||
from,
|
||||
{ url, key, model, prompt, useCache = true }
|
||||
) => {
|
||||
prompt = prompt
|
||||
.replaceAll(PROMPT_PLACE_FROM, from)
|
||||
.replaceAll(PROMPT_PLACE_TO, to);
|
||||
const res = await fetchPolyfill(url, {
|
||||
export const apiMicrosoftLangdetect = async (text) => {
|
||||
const cacheOpts = { text, detector: OPT_TRANS_MICROSOFT };
|
||||
const cacheInput = `${URL_CACHE_DELANG}?${queryString.stringify(cacheOpts)}`;
|
||||
const cache = await getHttpCachePolyfill(cacheInput);
|
||||
if (cache) {
|
||||
return cache;
|
||||
}
|
||||
|
||||
const key = `${URL_CACHE_DELANG}_${OPT_TRANS_MICROSOFT}`;
|
||||
const queue = getBatchQueue(key, handleMicrosoftLangdetect, {
|
||||
batchInterval: 200,
|
||||
batchSize: 20,
|
||||
batchLength: 100000,
|
||||
});
|
||||
const lang = await queue.addTask(text);
|
||||
|
||||
if (lang) {
|
||||
putHttpCachePolyfill(cacheInput, null, lang);
|
||||
return lang;
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
/**
|
||||
* Microsoft词典
|
||||
* @param {*} text
|
||||
* @returns
|
||||
*/
|
||||
export const apiMicrosoftDict = async (text) => {
|
||||
const cacheOpts = { text };
|
||||
const cacheInput = `${URL_CACHE_BINGDICT}?${queryString.stringify(cacheOpts)}`;
|
||||
const cache = await getHttpCachePolyfill(cacheInput);
|
||||
if (cache) {
|
||||
return cache;
|
||||
}
|
||||
|
||||
const host = "https://www.bing.com";
|
||||
const url = `${host}/dict/search?q=${text}&FORM=BDVSP6&cc=cn`;
|
||||
const str = await fetchData(
|
||||
url,
|
||||
{ credentials: "include" },
|
||||
{ useCache: false }
|
||||
);
|
||||
if (!str) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(str, "text/html");
|
||||
|
||||
const word = doc.querySelector("#headword > h1")?.textContent.trim();
|
||||
if (!word) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const trs = [];
|
||||
doc.querySelectorAll("div.qdef > ul > li").forEach(($li) => {
|
||||
const pos = $li.querySelector(".pos")?.textContent?.trim();
|
||||
const def = $li.querySelector(".def")?.textContent?.trim();
|
||||
trs.push({ pos, def });
|
||||
});
|
||||
|
||||
const aus = [];
|
||||
const $audioUK = doc.querySelector("#bigaud_uk");
|
||||
const $audioUS = doc.querySelector("#bigaud_us");
|
||||
if ($audioUK) {
|
||||
const audioUK = host + $audioUK?.dataset?.mp3link;
|
||||
const $phoneticUK = $audioUK.parentElement?.previousElementSibling;
|
||||
const phoneticUK = $phoneticUK?.textContent?.trim();
|
||||
aus.push({ key: "UK", audio: audioUK, phonetic: phoneticUK });
|
||||
}
|
||||
if ($audioUS) {
|
||||
const audioUS = host + $audioUS?.dataset?.mp3link;
|
||||
const $phoneticUS = $audioUS.parentElement?.previousElementSibling;
|
||||
const phoneticUS = $phoneticUS?.textContent?.trim();
|
||||
aus.push({ key: "US", audio: audioUS, phonetic: phoneticUS });
|
||||
}
|
||||
|
||||
const res = { word, trs, aus };
|
||||
putHttpCachePolyfill(cacheInput, null, res);
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
/**
|
||||
* 百度语言识别
|
||||
* @param {*} text
|
||||
* @returns
|
||||
*/
|
||||
export const apiBaiduLangdetect = async (text) => {
|
||||
const input = "https://fanyi.baidu.com/langdetect";
|
||||
const init = {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
model: model,
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: prompt,
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: text,
|
||||
},
|
||||
],
|
||||
temperature: 0,
|
||||
max_tokens: 256,
|
||||
query: text,
|
||||
}),
|
||||
useCache,
|
||||
usePool: true,
|
||||
translator,
|
||||
token: key,
|
||||
});
|
||||
const trText = res?.choices?.[0].message.content;
|
||||
const sLang = await tryDetectLang(text);
|
||||
const tLang = await tryDetectLang(trText);
|
||||
const isSame = text === trText || (sLang && tLang && sLang === tLang);
|
||||
};
|
||||
const res = await fetchData(input, init, { useCache: true });
|
||||
|
||||
return [trText, isSame];
|
||||
if (res?.error === 0) {
|
||||
await putHttpCachePolyfill(input, init, res);
|
||||
return res.lan;
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
/**
|
||||
* 自定义接口 翻译
|
||||
* 百度翻译建议
|
||||
* @param {*} text
|
||||
* @param {*} to
|
||||
* @param {*} from
|
||||
* @returns
|
||||
*/
|
||||
const apiCustomTranslate = async (
|
||||
translator,
|
||||
text,
|
||||
to,
|
||||
from,
|
||||
{ url, key, useCache = true }
|
||||
) => {
|
||||
const res = await fetchPolyfill(url, {
|
||||
export const apiBaiduSuggest = async (text) => {
|
||||
const input = "https://fanyi.baidu.com/sug";
|
||||
const init = {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
kw: text,
|
||||
}),
|
||||
};
|
||||
const res = await fetchData(input, init, { useCache: true });
|
||||
|
||||
if (res?.errno === 0) {
|
||||
await putHttpCachePolyfill(input, init, res);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
/**
|
||||
* 有道翻译建议
|
||||
* @param {*} text
|
||||
* @returns
|
||||
*/
|
||||
export const apiYoudaoSuggest = async (text) => {
|
||||
const params = {
|
||||
num: 5,
|
||||
ver: 3.0,
|
||||
doctype: "json",
|
||||
cache: false,
|
||||
le: "en",
|
||||
q: text,
|
||||
};
|
||||
const input = `https://dict.youdao.com/suggest?${queryString.stringify(params)}`;
|
||||
const init = {
|
||||
headers: {
|
||||
accept: "application/json, text/plain, */*",
|
||||
"accept-language": "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7,ja;q=0.6",
|
||||
"content-type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
method: "GET",
|
||||
};
|
||||
const res = await fetchData(input, init, { useCache: true });
|
||||
|
||||
if (res?.result?.code === 200) {
|
||||
await putHttpCachePolyfill(input, init, res);
|
||||
return res.data.entries;
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
/**
|
||||
* 有道词典
|
||||
* @param {*} text
|
||||
* @returns
|
||||
*/
|
||||
export const apiYoudaoDict = async (text) => {
|
||||
const params = {
|
||||
doctype: "json",
|
||||
jsonversion: 4,
|
||||
};
|
||||
const input = `https://dict.youdao.com/jsonapi_s?${queryString.stringify(params)}`;
|
||||
const body = queryString.stringify({
|
||||
q: text,
|
||||
le: "en",
|
||||
t: 3,
|
||||
client: "web",
|
||||
// sign: "",
|
||||
keyfrom: "webdict",
|
||||
});
|
||||
const init = {
|
||||
headers: {
|
||||
accept: "application/json, text/plain, */*",
|
||||
"accept-language": "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7,ja;q=0.6",
|
||||
"content-type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
method: "POST",
|
||||
body,
|
||||
};
|
||||
const res = await fetchData(input, init, { useCache: true });
|
||||
|
||||
if (res) {
|
||||
await putHttpCachePolyfill(input, init, res);
|
||||
return res;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* 百度语音
|
||||
* @param {*} text
|
||||
* @param {*} lan
|
||||
* @param {*} spd
|
||||
* @returns
|
||||
*/
|
||||
export const apiBaiduTTS = (text, lan = "uk", spd = 3) => {
|
||||
const input = `https://fanyi.baidu.com/gettts?${queryString.stringify({ lan, text, spd })}`;
|
||||
return fetchData(input);
|
||||
};
|
||||
|
||||
/**
|
||||
* 腾讯语言识别
|
||||
* @param {*} text
|
||||
* @returns
|
||||
*/
|
||||
export const apiTencentLangdetect = async (text) => {
|
||||
const input = "https://transmart.qq.com/api/imt";
|
||||
const body = JSON.stringify({
|
||||
header: {
|
||||
fn: "text_analysis",
|
||||
client_key:
|
||||
"browser-chrome-110.0.0-Mac OS-df4bd4c5-a65d-44b2-a40f-42f34f3535f2-1677486696487",
|
||||
},
|
||||
text,
|
||||
});
|
||||
const init = {
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
"user-agent":
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36",
|
||||
referer: "https://transmart.qq.com/zh-CN/index",
|
||||
},
|
||||
method: "POST",
|
||||
body,
|
||||
};
|
||||
const res = await fetchData(input, init, { useCache: true });
|
||||
|
||||
if (res?.language) {
|
||||
await putHttpCachePolyfill(input, init, res);
|
||||
return res.language;
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
/**
|
||||
* 浏览器内置AI语言识别
|
||||
* @param {*} text
|
||||
* @returns
|
||||
*/
|
||||
export const apiBuiltinAIDetect = async (text) => {
|
||||
if (!isBuiltinAIAvailable) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const [lang, error] = await fnPolyfill({
|
||||
fn: chromeDetect,
|
||||
msg: MSG_BUILTINAI_DETECT,
|
||||
text,
|
||||
});
|
||||
if (!error) {
|
||||
return lang;
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
/**
|
||||
* 浏览器内置AI翻译
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
const apiBuiltinAITranslate = async ({ text, from, to, apiSetting }) => {
|
||||
if (!isBuiltinAIAvailable) {
|
||||
return ["", true];
|
||||
}
|
||||
|
||||
const { fetchInterval, fetchLimit, httpTimeout } = apiSetting;
|
||||
const fetchPool = getFetchPool(fetchInterval, fetchLimit);
|
||||
const result = await withTimeout(
|
||||
fetchPool.push(fnPolyfill, {
|
||||
fn: chromeTranslate,
|
||||
msg: MSG_BUILTINAI_TRANSLATE,
|
||||
text,
|
||||
from,
|
||||
to,
|
||||
}),
|
||||
useCache,
|
||||
usePool: true,
|
||||
translator,
|
||||
token: key,
|
||||
});
|
||||
const trText = res.text;
|
||||
const isSame = to === res.from;
|
||||
httpTimeout
|
||||
);
|
||||
|
||||
return [trText, isSame];
|
||||
if (!result) {
|
||||
throw new Error("apiBuiltinAITranslate got null reault");
|
||||
}
|
||||
|
||||
const [trText, srLang, error] = result;
|
||||
if (error) {
|
||||
throw new Error("apiBuiltinAITranslate got error", error);
|
||||
}
|
||||
|
||||
return [trText, srLang];
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -244,29 +413,147 @@ const apiCustomTranslate = async (
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
export const apiTranslate = ({
|
||||
translator,
|
||||
export const apiTranslate = async ({
|
||||
text,
|
||||
fromLang,
|
||||
fromLang = "auto",
|
||||
toLang,
|
||||
apiSetting = DEFAULT_API_SETTING,
|
||||
docInfo = {},
|
||||
glossary = {},
|
||||
useCache = true,
|
||||
usePool = true,
|
||||
}) => {
|
||||
if (!text) {
|
||||
throw new Error("The text cannot be empty.");
|
||||
}
|
||||
|
||||
const { apiType, apiSlug, useBatchFetch } = apiSetting;
|
||||
const langMap = OPT_LANGS_TO_SPEC[apiType] || OPT_LANGS_SPEC_DEFAULT;
|
||||
const from = langMap.get(fromLang);
|
||||
const to = langMap.get(toLang);
|
||||
if (!to) {
|
||||
throw new Error(`The target lang: ${toLang} not support`);
|
||||
}
|
||||
|
||||
// todo: 优化缓存失效因素
|
||||
const [v1, v2] = process.env.REACT_APP_VERSION.split(".");
|
||||
const cacheOpts = {
|
||||
apiSlug,
|
||||
text,
|
||||
fromLang,
|
||||
toLang,
|
||||
version: [v1, v2].join("."),
|
||||
};
|
||||
const cacheInput = `${URL_CACHE_TRAN}?${queryString.stringify(cacheOpts)}`;
|
||||
|
||||
// 查询缓存数据
|
||||
if (useCache) {
|
||||
const cache = await getHttpCachePolyfill(cacheInput);
|
||||
if (cache?.trText) {
|
||||
return cache;
|
||||
}
|
||||
}
|
||||
|
||||
// 请求接口数据
|
||||
let tranlation = [];
|
||||
if (apiType === OPT_TRANS_BUILTINAI) {
|
||||
tranlation = await apiBuiltinAITranslate({
|
||||
text,
|
||||
from,
|
||||
to,
|
||||
apiSetting,
|
||||
});
|
||||
} else if (useBatchFetch && API_SPE_TYPES.batch.has(apiType)) {
|
||||
const { apiSlug, batchInterval, batchSize, batchLength } = apiSetting;
|
||||
const key = `${apiSlug}_${fromLang}_${toLang}`;
|
||||
const queue = getBatchQueue(key, handleTranslate, {
|
||||
batchInterval,
|
||||
batchSize,
|
||||
batchLength,
|
||||
});
|
||||
tranlation = await queue.addTask(text, {
|
||||
from,
|
||||
to,
|
||||
fromLang,
|
||||
toLang,
|
||||
langMap,
|
||||
docInfo,
|
||||
glossary,
|
||||
apiSetting,
|
||||
usePool,
|
||||
});
|
||||
} else {
|
||||
[tranlation] = await handleTranslate([text], {
|
||||
from,
|
||||
to,
|
||||
fromLang,
|
||||
toLang,
|
||||
langMap,
|
||||
docInfo,
|
||||
glossary,
|
||||
apiSetting,
|
||||
usePool,
|
||||
});
|
||||
}
|
||||
|
||||
let trText = "";
|
||||
let srLang = "";
|
||||
let srCode = "";
|
||||
if (Array.isArray(tranlation)) {
|
||||
[trText, srLang = ""] = tranlation;
|
||||
if (srLang) {
|
||||
srCode = OPT_LANGS_TO_CODE[apiType].get(srLang) || "";
|
||||
}
|
||||
} else if (typeof tranlation === "string") {
|
||||
trText = tranlation;
|
||||
}
|
||||
|
||||
if (!trText) {
|
||||
throw new Error("tanslate api got empty trtext");
|
||||
}
|
||||
|
||||
const isSame = fromLang === "auto" && srLang === to;
|
||||
|
||||
// 插入缓存
|
||||
if (useCache) {
|
||||
putHttpCachePolyfill(cacheInput, null, { trText, isSame, srLang, srCode });
|
||||
}
|
||||
|
||||
return { trText, srLang, srCode, isSame };
|
||||
};
|
||||
|
||||
// 字幕处理/翻译
|
||||
export const apiSubtitle = async ({
|
||||
videoId,
|
||||
chunkSign,
|
||||
fromLang = "auto",
|
||||
toLang,
|
||||
events = [],
|
||||
apiSetting,
|
||||
}) => {
|
||||
const from = OPT_LANGS_SPECIAL[translator]?.get(fromLang) ?? fromLang;
|
||||
const to = OPT_LANGS_SPECIAL[translator]?.get(toLang) ?? toLang;
|
||||
const callApi = (api) => api(translator, text, to, from, apiSetting);
|
||||
|
||||
switch (translator) {
|
||||
case OPT_TRANS_GOOGLE:
|
||||
return callApi(apiGoogleTranslate);
|
||||
case OPT_TRANS_MICROSOFT:
|
||||
return callApi(apiMicrosoftTranslate);
|
||||
case OPT_TRANS_DEEPL:
|
||||
return callApi(apiDeepLTranslate);
|
||||
case OPT_TRANS_OPENAI:
|
||||
return callApi(apiOpenaiTranslate);
|
||||
case OPT_TRANS_CUSTOMIZE:
|
||||
return callApi(apiCustomTranslate);
|
||||
default:
|
||||
return ["", false];
|
||||
const cacheOpts = {
|
||||
apiSlug: apiSetting.apiSlug,
|
||||
videoId,
|
||||
chunkSign,
|
||||
fromLang,
|
||||
toLang,
|
||||
};
|
||||
const cacheInput = `${URL_CACHE_SUBTITLE}?${queryString.stringify(cacheOpts)}`;
|
||||
const cache = await getHttpCachePolyfill(cacheInput);
|
||||
if (cache) {
|
||||
return cache;
|
||||
}
|
||||
|
||||
const subtitles = await handleSubtitle({
|
||||
events,
|
||||
from: fromLang,
|
||||
to: toLang,
|
||||
apiSetting,
|
||||
});
|
||||
if (subtitles?.length) {
|
||||
putHttpCachePolyfill(cacheInput, null, subtitles);
|
||||
return subtitles;
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
1030
src/apis/trans.js
Normal file
1030
src/apis/trans.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,77 +1,296 @@
|
||||
import browser from "webextension-polyfill";
|
||||
import {
|
||||
MSG_FETCH,
|
||||
MSG_FETCH_LIMIT,
|
||||
MSG_FETCH_CLEAR,
|
||||
MSG_GET_HTTPCACHE,
|
||||
MSG_PUT_HTTPCACHE,
|
||||
MSG_TRANS_TOGGLE,
|
||||
MSG_OPEN_OPTIONS,
|
||||
MSG_SAVE_RULE,
|
||||
MSG_TRANS_TOGGLE_STYLE,
|
||||
MSG_OPEN_TRANBOX,
|
||||
MSG_CONTEXT_MENUS,
|
||||
MSG_COMMAND_SHORTCUTS,
|
||||
MSG_INJECT_JS,
|
||||
MSG_INJECT_CSS,
|
||||
MSG_UPDATE_CSP,
|
||||
MSG_BUILTINAI_DETECT,
|
||||
MSG_BUILTINAI_TRANSLATE,
|
||||
DEFAULT_CSPLIST,
|
||||
DEFAULT_ORILIST,
|
||||
CMD_TOGGLE_TRANSLATE,
|
||||
CMD_TOGGLE_STYLE,
|
||||
CMD_OPEN_OPTIONS,
|
||||
CMD_OPEN_TRANBOX,
|
||||
CLIENT_THUNDERBIRD,
|
||||
MSG_SET_LOGLEVEL,
|
||||
MSG_CLEAR_CACHES,
|
||||
} from "./config";
|
||||
import { getSettingWithDefault, tryInitDefaultData } from "./libs/storage";
|
||||
import { trySyncSettingAndRules } from "./libs/sync";
|
||||
import { fetchData, fetchPool } from "./libs/fetch";
|
||||
import { fetchHandle } from "./libs/fetch";
|
||||
import { tryClearCaches, getHttpCache, putHttpCache } from "./libs/cache";
|
||||
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 { injectInlineJsBg, injectInternalCss } from "./libs/injector";
|
||||
import { kissLog, logger } from "./libs/log";
|
||||
import { chromeDetect, chromeTranslate } from "./libs/builtinAI";
|
||||
|
||||
globalThis.ContextType = "BACKGROUND";
|
||||
|
||||
const CSP_RULE_START_ID = 1;
|
||||
const ORI_RULE_START_ID = 10000;
|
||||
const CSP_REMOVE_HEADERS = [
|
||||
`content-security-policy`,
|
||||
`content-security-policy-report-only`,
|
||||
`x-webkit-csp`,
|
||||
`x-content-security-policy`,
|
||||
];
|
||||
|
||||
/**
|
||||
* 添加右键菜单
|
||||
*/
|
||||
async function addContextMenus(contextMenuType = 1) {
|
||||
// 添加前先删除,避免重复ID的错误
|
||||
try {
|
||||
await browser.contextMenus.removeAll();
|
||||
} catch (err) {
|
||||
kissLog("remove contextMenus", 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, orilist }) {
|
||||
try {
|
||||
const oldRules = await browser.declarativeNetRequest.getDynamicRules();
|
||||
|
||||
const rulesToAdd = [];
|
||||
const idsToRemove = [];
|
||||
|
||||
if (csplist !== undefined) {
|
||||
let processedCspList = csplist;
|
||||
if (typeof processedCspList === "string") {
|
||||
processedCspList = processedCspList
|
||||
.split(/\n|,/)
|
||||
.map((url) => url.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
const oldCspRuleIds = oldRules
|
||||
.filter(
|
||||
(rule) => rule.id >= CSP_RULE_START_ID && rule.id < ORI_RULE_START_ID
|
||||
)
|
||||
.map((rule) => rule.id);
|
||||
idsToRemove.push(...oldCspRuleIds);
|
||||
|
||||
const newCspRules = processedCspList.map((url, index) => ({
|
||||
id: CSP_RULE_START_ID + index,
|
||||
action: {
|
||||
type: "modifyHeaders",
|
||||
responseHeaders: CSP_REMOVE_HEADERS.map((header) => ({
|
||||
operation: "remove",
|
||||
header,
|
||||
})),
|
||||
},
|
||||
condition: {
|
||||
urlFilter: url,
|
||||
resourceTypes: ["main_frame", "sub_frame"],
|
||||
},
|
||||
}));
|
||||
rulesToAdd.push(...newCspRules);
|
||||
}
|
||||
|
||||
if (orilist !== undefined) {
|
||||
let processedOriList = orilist;
|
||||
if (typeof processedOriList === "string") {
|
||||
processedOriList = processedOriList
|
||||
.split(/\n|,/)
|
||||
.map((url) => url.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
const oldOriRuleIds = oldRules
|
||||
.filter((rule) => rule.id >= ORI_RULE_START_ID)
|
||||
.map((rule) => rule.id);
|
||||
idsToRemove.push(...oldOriRuleIds);
|
||||
|
||||
const newOriRules = processedOriList.map((url, index) => ({
|
||||
id: ORI_RULE_START_ID + index,
|
||||
action: {
|
||||
type: "modifyHeaders",
|
||||
requestHeaders: [{ header: "Origin", operation: "set", value: url }],
|
||||
},
|
||||
condition: {
|
||||
urlFilter: url,
|
||||
resourceTypes: ["xmlhttprequest"],
|
||||
},
|
||||
}));
|
||||
rulesToAdd.push(...newOriRules);
|
||||
}
|
||||
|
||||
if (idsToRemove.length > 0 || rulesToAdd.length > 0) {
|
||||
await browser.declarativeNetRequest.updateDynamicRules({
|
||||
removeRuleIds: idsToRemove,
|
||||
addRules: rulesToAdd,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
kissLog("update csp rules", err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册邮件显示脚本
|
||||
*/
|
||||
async function registerMsgDisplayScript() {
|
||||
await messenger.messageDisplayScripts.register({
|
||||
js: [{ file: "/content.js" }],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件安装
|
||||
*/
|
||||
browser.runtime.onInstalled.addListener(() => {
|
||||
tryInitDefaultData();
|
||||
|
||||
//在thunderbird中注册脚本
|
||||
if (process.env.REACT_APP_CLIENT === CLIENT_THUNDERBIRD) {
|
||||
registerMsgDisplayScript();
|
||||
}
|
||||
|
||||
// 右键菜单
|
||||
addContextMenus();
|
||||
|
||||
// 禁用CSP
|
||||
updateCspRules({ csplist: DEFAULT_CSPLIST, orilist: DEFAULT_ORILIST });
|
||||
});
|
||||
|
||||
/**
|
||||
* 浏览器启动
|
||||
*/
|
||||
browser.runtime.onStartup.addListener(async () => {
|
||||
console.log("browser onStartup");
|
||||
const {
|
||||
clearCache,
|
||||
contextMenuType,
|
||||
subrulesList,
|
||||
csplist,
|
||||
orilist,
|
||||
logLevel,
|
||||
} = await getSettingWithDefault();
|
||||
|
||||
// 同步数据
|
||||
await trySyncSettingAndRules(true);
|
||||
// 设置日志
|
||||
logger.setLevel(logLevel);
|
||||
|
||||
// 清除缓存
|
||||
const setting = await getSettingWithDefault();
|
||||
if (setting.clearCache) {
|
||||
if (clearCache) {
|
||||
tryClearCaches();
|
||||
}
|
||||
|
||||
//在thunderbird中注册脚本
|
||||
if (process.env.REACT_APP_CLIENT === CLIENT_THUNDERBIRD) {
|
||||
registerMsgDisplayScript();
|
||||
}
|
||||
|
||||
// 右键菜单
|
||||
// firefox重启后菜单会消失,故重复添加
|
||||
addContextMenus(contextMenuType);
|
||||
|
||||
// 禁用CSP
|
||||
updateCspRules({ csplist, orilist });
|
||||
|
||||
// 同步数据
|
||||
trySyncSettingAndRules();
|
||||
|
||||
// 同步订阅规则
|
||||
trySyncAllSubRules(setting, true);
|
||||
trySyncAllSubRules({ subrulesList });
|
||||
});
|
||||
|
||||
/**
|
||||
* 监听消息
|
||||
* 向当前活动标签页注入脚本或CSS
|
||||
*/
|
||||
browser.runtime.onMessage.addListener(
|
||||
({ action, args }, sender, sendResponse) => {
|
||||
switch (action) {
|
||||
case MSG_FETCH:
|
||||
const { input, opts } = args;
|
||||
fetchData(input, opts)
|
||||
.then((data) => {
|
||||
sendResponse({ data });
|
||||
})
|
||||
.catch((error) => {
|
||||
sendResponse({ error: error.message });
|
||||
});
|
||||
break;
|
||||
case MSG_FETCH_LIMIT:
|
||||
const { interval, limit } = args;
|
||||
fetchPool.update(interval, limit);
|
||||
sendResponse({ data: "ok" });
|
||||
break;
|
||||
case MSG_FETCH_CLEAR:
|
||||
fetchPool.clear();
|
||||
sendResponse({ data: "ok" });
|
||||
break;
|
||||
default:
|
||||
sendResponse({ error: `message action is unavailable: ${action}` });
|
||||
}
|
||||
return true;
|
||||
const injectToCurrentTab = async (func, args) => {
|
||||
const tabId = await getCurTabId();
|
||||
return browser.scripting.executeScript({
|
||||
target: { tabId, allFrames: true },
|
||||
func: func,
|
||||
args: [args],
|
||||
world: "MAIN",
|
||||
});
|
||||
};
|
||||
|
||||
// 动作处理器映射表
|
||||
const messageHandlers = {
|
||||
[MSG_FETCH]: (args) => fetchHandle(args),
|
||||
[MSG_GET_HTTPCACHE]: (args) => getHttpCache(args),
|
||||
[MSG_PUT_HTTPCACHE]: (args) => putHttpCache(args),
|
||||
[MSG_OPEN_OPTIONS]: () => browser.runtime.openOptionsPage(),
|
||||
[MSG_SAVE_RULE]: (args) => saveRule(args),
|
||||
[MSG_INJECT_JS]: (args) => injectToCurrentTab(injectInlineJsBg, args),
|
||||
[MSG_INJECT_CSS]: (args) => injectToCurrentTab(injectInternalCss, args),
|
||||
[MSG_UPDATE_CSP]: (args) => updateCspRules(args),
|
||||
[MSG_CONTEXT_MENUS]: (args) => addContextMenus(args),
|
||||
[MSG_COMMAND_SHORTCUTS]: () => browser.commands.getAll(),
|
||||
[MSG_BUILTINAI_DETECT]: (args) => chromeDetect(args),
|
||||
[MSG_BUILTINAI_TRANSLATE]: (args) => chromeTranslate(args),
|
||||
[MSG_SET_LOGLEVEL]: (args) => logger.setLevel(args),
|
||||
[MSG_CLEAR_CACHES]: () => tryClearCaches(),
|
||||
};
|
||||
|
||||
/**
|
||||
* 监听消息
|
||||
* todo: 返回含错误的结构化信息
|
||||
*/
|
||||
browser.runtime.onMessage.addListener(async ({ action, args }) => {
|
||||
const handler = messageHandlers[action];
|
||||
if (!handler) {
|
||||
throw new Error(`Message action is unavailable: ${action}`);
|
||||
}
|
||||
);
|
||||
|
||||
return handler(args);
|
||||
});
|
||||
|
||||
/**
|
||||
* 监听快捷键
|
||||
@@ -82,9 +301,36 @@ browser.commands.onCommand.addListener((command) => {
|
||||
case CMD_TOGGLE_TRANSLATE:
|
||||
sendTabMsg(MSG_TRANS_TOGGLE);
|
||||
break;
|
||||
case CMD_OPEN_TRANBOX:
|
||||
sendTabMsg(MSG_OPEN_TRANBOX);
|
||||
break;
|
||||
case CMD_TOGGLE_STYLE:
|
||||
sendTabMsg(MSG_TRANS_TOGGLE_STYLE);
|
||||
break;
|
||||
case CMD_OPEN_OPTIONS:
|
||||
browser.runtime.openOptionsPage();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 监听右键菜单
|
||||
*/
|
||||
browser.contextMenus.onClicked.addListener(({ menuItemId }) => {
|
||||
switch (menuItemId) {
|
||||
case CMD_TOGGLE_TRANSLATE:
|
||||
sendTabMsg(MSG_TRANS_TOGGLE);
|
||||
break;
|
||||
case CMD_TOGGLE_STYLE:
|
||||
sendTabMsg(MSG_TRANS_TOGGLE_STYLE);
|
||||
break;
|
||||
case CMD_OPEN_TRANBOX:
|
||||
sendTabMsg(MSG_OPEN_TRANBOX);
|
||||
break;
|
||||
case CMD_OPEN_OPTIONS:
|
||||
browser.runtime.openOptionsPage();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
|
||||
172
src/common.js
Normal file
172
src/common.js
Normal file
@@ -0,0 +1,172 @@
|
||||
import { OPT_HIGHLIGHT_WORDS_DISABLE } from "./config";
|
||||
import {
|
||||
getFabWithDefault,
|
||||
getSettingWithDefault,
|
||||
getWordsWithDefault,
|
||||
} from "./libs/storage";
|
||||
import { isIframe } from "./libs/iframe";
|
||||
import { genEventName } from "./libs/utils";
|
||||
import { handlePing, injectScript } from "./libs/gm";
|
||||
import { matchRule } from "./libs/rules";
|
||||
import { trySyncAllSubRules } from "./libs/subRules";
|
||||
import { isInBlacklist } from "./libs/blacklist";
|
||||
import { runSubtitle } from "./subtitle/subtitle";
|
||||
import { logger } from "./libs/log";
|
||||
import { injectInlineJs } from "./libs/injector";
|
||||
import TranslatorManager from "./libs/translatorManager";
|
||||
|
||||
/**
|
||||
* 油猴脚本设置页面
|
||||
*/
|
||||
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
|
||||
injectInlineJs(
|
||||
`(${injectScript})("${ping}")`,
|
||||
"kiss-translator-options-injector"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示错误信息到页面顶部
|
||||
* @param {*} message
|
||||
*/
|
||||
function showErr(message) {
|
||||
const bannerId = "KISS-Translator-Message";
|
||||
const existingBanner = document.getElementById(bannerId);
|
||||
if (existingBanner) {
|
||||
existingBanner.remove();
|
||||
}
|
||||
|
||||
const banner = document.createElement("div");
|
||||
banner.id = bannerId;
|
||||
|
||||
Object.assign(banner.style, {
|
||||
position: "fixed",
|
||||
top: "0",
|
||||
left: "0",
|
||||
width: "100%",
|
||||
backgroundColor: "#f44336",
|
||||
color: "white",
|
||||
textAlign: "center",
|
||||
padding: "8px 16px",
|
||||
zIndex: "1001",
|
||||
boxSizing: "border-box",
|
||||
fontSize: "16px",
|
||||
boxShadow: "0 2px 5px rgba(0,0,0,0.2)",
|
||||
});
|
||||
|
||||
const closeButton = document.createElement("span");
|
||||
closeButton.textContent = "×";
|
||||
|
||||
Object.assign(closeButton.style, {
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
right: "20px",
|
||||
transform: "translateY(-50%)",
|
||||
cursor: "pointer",
|
||||
fontSize: "22px",
|
||||
fontWeight: "bold",
|
||||
});
|
||||
|
||||
const messageText = document.createTextNode(`KISS-Translator: ${message}`);
|
||||
banner.appendChild(messageText);
|
||||
banner.appendChild(closeButton);
|
||||
|
||||
document.body.appendChild(banner);
|
||||
|
||||
const removeBanner = () => {
|
||||
banner.style.transition = "opacity 0.5s ease";
|
||||
banner.style.opacity = "0";
|
||||
setTimeout(() => {
|
||||
if (banner && banner.parentNode) {
|
||||
banner.parentNode.removeChild(banner);
|
||||
}
|
||||
}, 500);
|
||||
};
|
||||
|
||||
closeButton.onclick = removeBanner;
|
||||
setTimeout(removeBanner, 10000);
|
||||
}
|
||||
|
||||
async function getFavWords(rule) {
|
||||
if (
|
||||
rule.highlightWords &&
|
||||
rule.highlightWords !== OPT_HIGHLIGHT_WORDS_DISABLE
|
||||
) {
|
||||
try {
|
||||
return Object.keys(await getWordsWithDefault());
|
||||
} catch (err) {
|
||||
logger.info("get fav words", err);
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 入口函数
|
||||
*/
|
||||
export async function run(isUserscript = false) {
|
||||
try {
|
||||
// 读取设置信息
|
||||
const setting = await getSettingWithDefault();
|
||||
|
||||
// 日志
|
||||
logger.setLevel(setting.logLevel);
|
||||
|
||||
const href = document.location.href;
|
||||
|
||||
// 设置页面
|
||||
if (
|
||||
isUserscript &&
|
||||
(href.includes(process.env.REACT_APP_OPTIONSPAGE_DEV) ||
|
||||
href.includes(process.env.REACT_APP_OPTIONSPAGE))
|
||||
) {
|
||||
runSettingPage();
|
||||
return;
|
||||
}
|
||||
|
||||
// 黑名单
|
||||
if (isInBlacklist(href, setting)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 翻译网页
|
||||
const rule = await matchRule(href, setting);
|
||||
const favWords = await getFavWords(rule);
|
||||
const fabConfig = await getFabWithDefault();
|
||||
const translatorManager = new TranslatorManager({
|
||||
setting,
|
||||
rule,
|
||||
fabConfig,
|
||||
favWords,
|
||||
isIframe,
|
||||
isUserscript,
|
||||
});
|
||||
translatorManager.start();
|
||||
|
||||
if (isIframe) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 字幕翻译
|
||||
runSubtitle({ href, setting, rule, isUserscript });
|
||||
|
||||
if (isUserscript) {
|
||||
trySyncAllSubRules(setting);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[KISS-Translator]", err);
|
||||
showErr(err.message);
|
||||
}
|
||||
}
|
||||
573
src/config/api.js
Normal file
573
src/config/api.js
Normal file
@@ -0,0 +1,573 @@
|
||||
export const DEFAULT_HTTP_TIMEOUT = 10000; // 调用超时时间
|
||||
export const DEFAULT_FETCH_LIMIT = 10; // 默认最大任务数量
|
||||
export const DEFAULT_FETCH_INTERVAL = 100; // 默认任务间隔时间
|
||||
export const DEFAULT_BATCH_INTERVAL = 400; // 批处理请求间隔时间
|
||||
export const DEFAULT_BATCH_SIZE = 10; // 每次最多发送段落数量
|
||||
export const DEFAULT_BATCH_LENGTH = 10000; // 每次发送最大文字数量
|
||||
export const DEFAULT_CONTEXT_SIZE = 3; // 上下文会话数量
|
||||
|
||||
export const INPUT_PLACE_URL = "{{url}}"; // 占位符
|
||||
export const INPUT_PLACE_FROM = "{{from}}"; // 占位符
|
||||
export const INPUT_PLACE_TO = "{{to}}"; // 占位符
|
||||
export const INPUT_PLACE_TEXT = "{{text}}"; // 占位符
|
||||
export const INPUT_PLACE_KEY = "{{key}}"; // 占位符
|
||||
export const INPUT_PLACE_MODEL = "{{model}}"; // 占位符
|
||||
|
||||
// export const OPT_DICT_BAIDU = "Baidu";
|
||||
export const OPT_DICT_BING = "Bing";
|
||||
export const OPT_DICT_YOUDAO = "Youdao";
|
||||
export const OPT_DICT_ALL = [OPT_DICT_BING, OPT_DICT_YOUDAO];
|
||||
export const OPT_DICT_MAP = new Set(OPT_DICT_ALL);
|
||||
|
||||
export const OPT_SUG_BAIDU = "Baidu";
|
||||
export const OPT_SUG_YOUDAO = "Youdao";
|
||||
export const OPT_SUG_ALL = [OPT_SUG_BAIDU, OPT_SUG_YOUDAO];
|
||||
export const OPT_SUG_MAP = new Set(OPT_SUG_ALL);
|
||||
|
||||
export const OPT_TRANS_BUILTINAI = "BuiltinAI";
|
||||
export const OPT_TRANS_GOOGLE = "Google";
|
||||
export const OPT_TRANS_GOOGLE_2 = "Google2";
|
||||
export const OPT_TRANS_MICROSOFT = "Microsoft";
|
||||
export const OPT_TRANS_AZUREAI = "AzureAI";
|
||||
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_VOLCENGINE = "Volcengine";
|
||||
export const OPT_TRANS_OPENAI = "OpenAI";
|
||||
export const OPT_TRANS_GEMINI = "Gemini";
|
||||
export const OPT_TRANS_GEMINI_2 = "Gemini2";
|
||||
export const OPT_TRANS_CLAUDE = "Claude";
|
||||
export const OPT_TRANS_CLOUDFLAREAI = "CloudflareAI";
|
||||
export const OPT_TRANS_OLLAMA = "Ollama";
|
||||
export const OPT_TRANS_OPENROUTER = "OpenRouter";
|
||||
export const OPT_TRANS_CUSTOMIZE = "Custom";
|
||||
|
||||
// 内置支持的翻译引擎
|
||||
export const OPT_ALL_TRANS_TYPES = [
|
||||
OPT_TRANS_BUILTINAI,
|
||||
OPT_TRANS_GOOGLE,
|
||||
OPT_TRANS_GOOGLE_2,
|
||||
OPT_TRANS_MICROSOFT,
|
||||
OPT_TRANS_AZUREAI,
|
||||
// OPT_TRANS_BAIDU,
|
||||
OPT_TRANS_TENCENT,
|
||||
OPT_TRANS_VOLCENGINE,
|
||||
OPT_TRANS_DEEPL,
|
||||
OPT_TRANS_DEEPLFREE,
|
||||
OPT_TRANS_DEEPLX,
|
||||
OPT_TRANS_NIUTRANS,
|
||||
OPT_TRANS_OPENAI,
|
||||
OPT_TRANS_GEMINI,
|
||||
OPT_TRANS_GEMINI_2,
|
||||
OPT_TRANS_CLAUDE,
|
||||
OPT_TRANS_CLOUDFLAREAI,
|
||||
OPT_TRANS_OLLAMA,
|
||||
OPT_TRANS_OPENROUTER,
|
||||
OPT_TRANS_CUSTOMIZE,
|
||||
];
|
||||
|
||||
export const OPT_LANGDETECTOR_ALL = [
|
||||
OPT_TRANS_BUILTINAI,
|
||||
OPT_TRANS_GOOGLE,
|
||||
OPT_TRANS_MICROSOFT,
|
||||
OPT_TRANS_BAIDU,
|
||||
OPT_TRANS_TENCENT,
|
||||
];
|
||||
|
||||
export const OPT_LANGDETECTOR_MAP = new Set(OPT_LANGDETECTOR_ALL);
|
||||
|
||||
// 翻译引擎特殊集合
|
||||
export const API_SPE_TYPES = {
|
||||
// 内置翻译
|
||||
builtin: new Set(OPT_ALL_TRANS_TYPES),
|
||||
// 机器翻译
|
||||
machine: new Set([
|
||||
OPT_TRANS_MICROSOFT,
|
||||
OPT_TRANS_DEEPLFREE,
|
||||
OPT_TRANS_BAIDU,
|
||||
OPT_TRANS_TENCENT,
|
||||
OPT_TRANS_VOLCENGINE,
|
||||
]),
|
||||
// AI翻译
|
||||
ai: new Set([
|
||||
OPT_TRANS_OPENAI,
|
||||
OPT_TRANS_GEMINI,
|
||||
OPT_TRANS_GEMINI_2,
|
||||
OPT_TRANS_CLAUDE,
|
||||
OPT_TRANS_OLLAMA,
|
||||
OPT_TRANS_OPENROUTER,
|
||||
]),
|
||||
// 支持多key
|
||||
mulkeys: new Set([
|
||||
OPT_TRANS_AZUREAI,
|
||||
OPT_TRANS_DEEPL,
|
||||
OPT_TRANS_OPENAI,
|
||||
OPT_TRANS_GEMINI,
|
||||
OPT_TRANS_GEMINI_2,
|
||||
OPT_TRANS_CLAUDE,
|
||||
OPT_TRANS_CLOUDFLAREAI,
|
||||
OPT_TRANS_OLLAMA,
|
||||
OPT_TRANS_OPENROUTER,
|
||||
OPT_TRANS_NIUTRANS,
|
||||
OPT_TRANS_CUSTOMIZE,
|
||||
]),
|
||||
// 支持批处理
|
||||
batch: new Set([
|
||||
OPT_TRANS_AZUREAI,
|
||||
OPT_TRANS_GOOGLE_2,
|
||||
OPT_TRANS_MICROSOFT,
|
||||
OPT_TRANS_TENCENT,
|
||||
OPT_TRANS_DEEPL,
|
||||
OPT_TRANS_OPENAI,
|
||||
OPT_TRANS_GEMINI,
|
||||
OPT_TRANS_GEMINI_2,
|
||||
OPT_TRANS_CLAUDE,
|
||||
OPT_TRANS_OLLAMA,
|
||||
OPT_TRANS_OPENROUTER,
|
||||
OPT_TRANS_CUSTOMIZE,
|
||||
]),
|
||||
// 支持上下文
|
||||
context: new Set([
|
||||
OPT_TRANS_OPENAI,
|
||||
OPT_TRANS_GEMINI,
|
||||
OPT_TRANS_GEMINI_2,
|
||||
OPT_TRANS_CLAUDE,
|
||||
OPT_TRANS_OLLAMA,
|
||||
OPT_TRANS_OPENROUTER,
|
||||
OPT_TRANS_CUSTOMIZE,
|
||||
]),
|
||||
};
|
||||
|
||||
export const BUILTIN_STONES = [
|
||||
"formal", // 正式风格
|
||||
"casual", // 口语风格
|
||||
"neutral", // 中性风格
|
||||
"technical", // 技术风格
|
||||
"marketing", // 营销风格
|
||||
"Literary", // 文学风格
|
||||
"academic", // 学术风格
|
||||
"legal", // 法律风格
|
||||
"literal", // 直译风格
|
||||
"ldiomatic", // 意译风格
|
||||
"transcreation", // 创译风格
|
||||
"machine-like", // 机器风格
|
||||
"concise", // 简明风格
|
||||
];
|
||||
export const BUILTIN_PLACEHOLDERS = ["{ }", "{{ }}", "[ ]", "[[ ]]"];
|
||||
export const BUILTIN_PLACETAGS = ["i", "a", "b", "x"];
|
||||
|
||||
export const OPT_LANGS_TO = [
|
||||
["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"],
|
||||
];
|
||||
export const OPT_LANGS_LIST = OPT_LANGS_TO.map(([lang]) => lang);
|
||||
export const OPT_LANGS_FROM = [["auto", "Auto-detect"], ...OPT_LANGS_TO];
|
||||
export const OPT_LANGS_MAP = new Map(OPT_LANGS_TO);
|
||||
|
||||
// CODE->名称
|
||||
export const OPT_LANGS_SPEC_NAME = new Map(
|
||||
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
||||
);
|
||||
export const OPT_LANGS_SPEC_DEFAULT = new Map(
|
||||
OPT_LANGS_FROM.map(([key]) => [key, key])
|
||||
);
|
||||
export const OPT_LANGS_SPEC_DEFAULT_UC = new Map(
|
||||
OPT_LANGS_FROM.map(([key]) => [key, key.toUpperCase()])
|
||||
);
|
||||
export const OPT_LANGS_TO_SPEC = {
|
||||
[OPT_TRANS_BUILTINAI]: new Map([
|
||||
...OPT_LANGS_SPEC_DEFAULT,
|
||||
["zh-CN", "zh"],
|
||||
["zh-TW", "zh"],
|
||||
]),
|
||||
[OPT_TRANS_GOOGLE]: OPT_LANGS_SPEC_DEFAULT,
|
||||
[OPT_TRANS_GOOGLE_2]: OPT_LANGS_SPEC_DEFAULT,
|
||||
[OPT_TRANS_MICROSOFT]: new Map([
|
||||
...OPT_LANGS_SPEC_DEFAULT,
|
||||
["auto", ""],
|
||||
["zh-CN", "zh-Hans"],
|
||||
["zh-TW", "zh-Hant"],
|
||||
]),
|
||||
[OPT_TRANS_AZUREAI]: new Map([
|
||||
...OPT_LANGS_SPEC_DEFAULT,
|
||||
["auto", ""],
|
||||
["zh-CN", "zh-Hans"],
|
||||
["zh-TW", "zh-Hant"],
|
||||
]),
|
||||
[OPT_TRANS_DEEPL]: new Map([
|
||||
...OPT_LANGS_SPEC_DEFAULT_UC,
|
||||
["auto", ""],
|
||||
["zh-CN", "ZH"],
|
||||
["zh-TW", "ZH"],
|
||||
]),
|
||||
[OPT_TRANS_DEEPLFREE]: new Map([
|
||||
...OPT_LANGS_SPEC_DEFAULT_UC,
|
||||
["auto", "auto"],
|
||||
["zh-CN", "ZH"],
|
||||
["zh-TW", "ZH"],
|
||||
]),
|
||||
[OPT_TRANS_DEEPLX]: new Map([
|
||||
...OPT_LANGS_SPEC_DEFAULT_UC,
|
||||
["auto", "auto"],
|
||||
["zh-CN", "ZH"],
|
||||
["zh-TW", "ZH"],
|
||||
]),
|
||||
[OPT_TRANS_NIUTRANS]: new Map([
|
||||
...OPT_LANGS_SPEC_DEFAULT,
|
||||
["auto", "auto"],
|
||||
["zh-CN", "zh"],
|
||||
["zh-TW", "cht"],
|
||||
]),
|
||||
[OPT_TRANS_VOLCENGINE]: new Map([
|
||||
...OPT_LANGS_SPEC_DEFAULT,
|
||||
["auto", "auto"],
|
||||
["zh-CN", "zh"],
|
||||
["zh-TW", "zh-Hant"],
|
||||
]),
|
||||
[OPT_TRANS_BAIDU]: new Map([
|
||||
...OPT_LANGS_SPEC_DEFAULT,
|
||||
["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]: OPT_LANGS_SPEC_DEFAULT,
|
||||
[OPT_TRANS_GEMINI]: OPT_LANGS_SPEC_DEFAULT,
|
||||
[OPT_TRANS_GEMINI_2]: OPT_LANGS_SPEC_DEFAULT,
|
||||
[OPT_TRANS_CLAUDE]: OPT_LANGS_SPEC_DEFAULT,
|
||||
[OPT_TRANS_OLLAMA]: OPT_LANGS_SPEC_DEFAULT,
|
||||
[OPT_TRANS_OPENROUTER]: OPT_LANGS_SPEC_DEFAULT,
|
||||
[OPT_TRANS_CLOUDFLAREAI]: OPT_LANGS_SPEC_DEFAULT,
|
||||
[OPT_TRANS_CUSTOMIZE]: OPT_LANGS_SPEC_DEFAULT,
|
||||
};
|
||||
|
||||
const specToCode = (m) =>
|
||||
new Map(
|
||||
Array.from(m.entries()).map(([k, v]) => {
|
||||
if (v === "") {
|
||||
return ["auto", "auto"];
|
||||
}
|
||||
if (v === "zh" || v === "ZH") {
|
||||
return [v, "zh-CN"];
|
||||
}
|
||||
return [v, k];
|
||||
})
|
||||
);
|
||||
|
||||
// 名称->CODE
|
||||
export const OPT_LANGS_TO_CODE = {};
|
||||
Object.entries(OPT_LANGS_TO_SPEC).forEach(([t, m]) => {
|
||||
OPT_LANGS_TO_CODE[t] = specToCode(m);
|
||||
});
|
||||
|
||||
export const defaultNobatchPrompt = `You are a professional, authentic machine translation engine.`;
|
||||
export const defaultNobatchUserPrompt = `Translate the following source text from ${INPUT_PLACE_FROM} to ${INPUT_PLACE_TO}. Output translation directly without any additional text.\n\nSource Text: ${INPUT_PLACE_TEXT}\n\nTranslated Text:`;
|
||||
|
||||
export const defaultSystemPrompt = `Act as a translation API. Output a single raw JSON object only. No extra text or fences.
|
||||
|
||||
Input:
|
||||
{"targetLanguage":"<lang>","title":"<context>","description":"<context>","segments":[{"id":1,"text":"..."}],"glossary":{"sourceTerm":"targetTerm"},"tone":"<formal|casual>"}
|
||||
|
||||
Output:
|
||||
{"translations":[{"id":1,"text":"...","sourceLanguage":"<detected>"}]}
|
||||
|
||||
Rules:
|
||||
1. Use title/description for context only; do not output them.
|
||||
2. Keep id, order, and count of segments.
|
||||
3. Preserve whitespace, HTML entities, and all HTML-like tags (e.g., <i1>, <a1>). Translate inner text only.
|
||||
4. Highest priority: Follow 'glossary'. Use value for translation; if value is "", keep the key.
|
||||
5. Do not translate: content in <code>, <pre>, text enclosed in backticks, or placeholders like {1}, {{1}}, [1], [[1]].
|
||||
6. Apply the specified tone to the translation.
|
||||
7. Detect sourceLanguage for each segment.
|
||||
8. Return empty or unchanged inputs as is.
|
||||
|
||||
Example:
|
||||
Input: {"targetLanguage":"zh-CN","segments":[{"id":1,"text":"A <b>React</b> component."}],"glossary":{"component":"组件","React":""}}
|
||||
Output: {"translations":[{"id":1,"text":"一个<b>React</b>组件","sourceLanguage":"en"}]}
|
||||
|
||||
Fail-safe: On any error, return {"translations":[]}.`;
|
||||
|
||||
// const defaultSubtitlePrompt = `Goal: Convert raw subtitle event JSON into a clean, sentence-based JSON array.
|
||||
|
||||
// Output (valid JSON array, output ONLY this array):
|
||||
// [{
|
||||
// "text": "string", // Full sentence with correct punctuation
|
||||
// "translation": "string", // Translation in ${INPUT_PLACE_TO}
|
||||
// "start": int, // Start time (ms)
|
||||
// "end": int, // End time (ms)
|
||||
// }]
|
||||
|
||||
// Guidelines:
|
||||
// 1. **Segmentation**: Merge sequential 'utf8' strings from 'segs' into full sentences, merging groups logically.
|
||||
// 2. **Punctuation**: Ensure proper sentence-final punctuation (., ?, !); add if missing.
|
||||
// 3. **Translation**: Translate 'text' into ${INPUT_PLACE_TO}, place result in 'translation'.
|
||||
// 4. **Special Cases**: '[Music]' (and similar cues) are standalone entries. Translate appropriately (e.g., '[音乐]', '[Musique]').
|
||||
// `;
|
||||
|
||||
export const defaultSubtitlePrompt = `You are an expert AI for subtitle generation. Convert a JSON array of word-level timestamps into a bilingual VTT file.
|
||||
|
||||
**Workflow:**
|
||||
1. Merge \`text\` fields into complete sentences; ignore empty text.
|
||||
2. Split long sentences into smaller, manageable subtitle cues (one sentence per cue).
|
||||
3. Translate each cue into ${INPUT_PLACE_TO}.
|
||||
4. Format as VTT:
|
||||
- Start with \`WEBVTT\`.
|
||||
- Each cue: timestamps (\`start --> end\` in milliseconds), original text, translated text.
|
||||
- Keep non-speech text (e.g., \`[Music]\`) untranslated.
|
||||
- Separate cues with a blank line.
|
||||
|
||||
**Output:** Only the pure VTT content.
|
||||
|
||||
**Example:**
|
||||
\`\`\`vtt
|
||||
WEBVTT
|
||||
|
||||
1000 --> 3500
|
||||
Hello world!
|
||||
你好,世界!
|
||||
|
||||
4000 --> 6000
|
||||
Good morning.
|
||||
早上好。
|
||||
\`\`\``;
|
||||
|
||||
const defaultRequestHook = `async (args, { url, body, headers, userMsg, method } = {}) => {
|
||||
console.log("request hook args:", { args, url, body, headers, userMsg, method });
|
||||
// return { url, body, headers, userMsg, method };
|
||||
};`;
|
||||
|
||||
const defaultResponseHook = `async ({ res, ...args }) => {
|
||||
console.log("reaponse hook args:", { res, args });
|
||||
// const translations = [["你好", "zh"]];
|
||||
// const modelMsg = "";
|
||||
// return { translations, modelMsg };
|
||||
};`;
|
||||
|
||||
// 翻译接口默认参数
|
||||
const defaultApi = {
|
||||
apiSlug: "", // 唯一标识
|
||||
apiName: "", // 接口名称
|
||||
apiType: "", // 接口类型
|
||||
url: "",
|
||||
key: "",
|
||||
model: "", // 模型名称
|
||||
systemPrompt: defaultSystemPrompt,
|
||||
subtitlePrompt: defaultSubtitlePrompt,
|
||||
nobatchPrompt: defaultNobatchPrompt,
|
||||
nobatchUserPrompt: defaultNobatchUserPrompt,
|
||||
userPrompt: "",
|
||||
tone: BUILTIN_STONES[0], // 翻译风格
|
||||
placeholder: BUILTIN_PLACEHOLDERS[0], // 占位符
|
||||
placetag: [BUILTIN_PLACETAGS[0]], // 占位标签
|
||||
// aiTerms: false, // AI智能专业术语 (todo: 备用)
|
||||
customHeader: "",
|
||||
customBody: "",
|
||||
reqHook: "", // request 钩子函数
|
||||
resHook: "", // response 钩子函数
|
||||
fetchLimit: DEFAULT_FETCH_LIMIT, // 最大请求数量
|
||||
fetchInterval: DEFAULT_FETCH_INTERVAL, // 请求间隔时间
|
||||
httpTimeout: DEFAULT_HTTP_TIMEOUT * 30, // 请求超时时间
|
||||
batchInterval: DEFAULT_BATCH_INTERVAL, // 批处理请求间隔时间
|
||||
batchSize: DEFAULT_BATCH_SIZE, // 每次最多发送段落数量
|
||||
batchLength: DEFAULT_BATCH_LENGTH, // 每次发送最大文字数量
|
||||
useBatchFetch: false, // 是否启用聚合发送请求
|
||||
useContext: false, // 是否启用智能上下文
|
||||
contextSize: DEFAULT_CONTEXT_SIZE, // 智能上下文保留会话数
|
||||
temperature: 0.0,
|
||||
maxTokens: 20480,
|
||||
// think: false, // (OpenAI 兼容接口未支持,暂时移除)
|
||||
// thinkIgnore: "qwen3,deepseek-r1", // (OpenAI 兼容接口未支持,暂时移除)
|
||||
isDisabled: false, // 是否不显示,
|
||||
region: "", // Azure 专用
|
||||
};
|
||||
|
||||
const defaultApiOpts = {
|
||||
[OPT_TRANS_BUILTINAI]: defaultApi,
|
||||
[OPT_TRANS_GOOGLE]: {
|
||||
...defaultApi,
|
||||
url: "https://translate.googleapis.com/translate_a/single",
|
||||
},
|
||||
[OPT_TRANS_GOOGLE_2]: {
|
||||
...defaultApi,
|
||||
url: "https://translate-pa.googleapis.com/v1/translateHtml",
|
||||
key: "AIzaSyATBXajvzQLTDHEQbcpq0Ihe0vWDHmO520",
|
||||
useBatchFetch: true,
|
||||
},
|
||||
[OPT_TRANS_MICROSOFT]: {
|
||||
...defaultApi,
|
||||
useBatchFetch: true,
|
||||
},
|
||||
[OPT_TRANS_AZUREAI]: {
|
||||
...defaultApi,
|
||||
url: "https://api.cognitive.microsofttranslator.com/translate?api-version=3.0",
|
||||
useBatchFetch: true,
|
||||
},
|
||||
[OPT_TRANS_BAIDU]: {
|
||||
...defaultApi,
|
||||
},
|
||||
[OPT_TRANS_TENCENT]: {
|
||||
...defaultApi,
|
||||
useBatchFetch: true,
|
||||
},
|
||||
[OPT_TRANS_VOLCENGINE]: {
|
||||
...defaultApi,
|
||||
},
|
||||
[OPT_TRANS_DEEPL]: {
|
||||
...defaultApi,
|
||||
url: "https://api-free.deepl.com/v2/translate",
|
||||
useBatchFetch: true,
|
||||
},
|
||||
[OPT_TRANS_DEEPLFREE]: {
|
||||
...defaultApi,
|
||||
fetchLimit: 1,
|
||||
},
|
||||
[OPT_TRANS_DEEPLX]: {
|
||||
...defaultApi,
|
||||
url: "http://localhost:1188/translate",
|
||||
},
|
||||
[OPT_TRANS_NIUTRANS]: {
|
||||
...defaultApi,
|
||||
url: "https://api.niutrans.com/NiuTransServer/translation",
|
||||
dictNo: "",
|
||||
memoryNo: "",
|
||||
},
|
||||
[OPT_TRANS_OPENAI]: {
|
||||
...defaultApi,
|
||||
url: "https://api.openai.com/v1/chat/completions",
|
||||
model: "gpt-4",
|
||||
useBatchFetch: true,
|
||||
},
|
||||
[OPT_TRANS_GEMINI]: {
|
||||
...defaultApi,
|
||||
url: `https://generativelanguage.googleapis.com/v1/models/${INPUT_PLACE_MODEL}:generateContent?key=${INPUT_PLACE_KEY}`,
|
||||
model: "gemini-2.5-flash",
|
||||
useBatchFetch: true,
|
||||
},
|
||||
[OPT_TRANS_GEMINI_2]: {
|
||||
...defaultApi,
|
||||
url: `https://generativelanguage.googleapis.com/v1beta/openai/chat/completions`,
|
||||
model: "gemini-2.0-flash",
|
||||
useBatchFetch: true,
|
||||
},
|
||||
[OPT_TRANS_CLAUDE]: {
|
||||
...defaultApi,
|
||||
url: "https://api.anthropic.com/v1/messages",
|
||||
model: "claude-3-haiku-20240307",
|
||||
useBatchFetch: true,
|
||||
},
|
||||
[OPT_TRANS_CLOUDFLAREAI]: {
|
||||
...defaultApi,
|
||||
url: "https://api.cloudflare.com/client/v4/accounts/{{ACCOUNT_ID}}/ai/run/@cf/meta/m2m100-1.2b",
|
||||
},
|
||||
[OPT_TRANS_OLLAMA]: {
|
||||
...defaultApi,
|
||||
url: "http://localhost:11434/v1/chat/completions",
|
||||
model: "llama3.1",
|
||||
useBatchFetch: true,
|
||||
},
|
||||
[OPT_TRANS_OPENROUTER]: {
|
||||
...defaultApi,
|
||||
url: "https://openrouter.ai/api/v1/chat/completions",
|
||||
model: "openai/gpt-4o",
|
||||
useBatchFetch: true,
|
||||
},
|
||||
[OPT_TRANS_CUSTOMIZE]: {
|
||||
...defaultApi,
|
||||
url: "https://translate.googleapis.com/translate_a/single?client=gtx&dj=1&dt=t&ie=UTF-8&q={{text}}&sl=en&tl=zh-CN",
|
||||
reqHook: defaultRequestHook,
|
||||
resHook: defaultResponseHook,
|
||||
},
|
||||
};
|
||||
|
||||
// 内置翻译接口列表(带参数)
|
||||
export const DEFAULT_API_LIST = OPT_ALL_TRANS_TYPES.map((apiType) => ({
|
||||
...defaultApiOpts[apiType],
|
||||
apiSlug: apiType,
|
||||
apiName: apiType,
|
||||
apiType,
|
||||
}));
|
||||
|
||||
export const DEFAULT_API_TYPE = OPT_TRANS_MICROSOFT;
|
||||
export const DEFAULT_API_SETTING = DEFAULT_API_LIST.find(
|
||||
(a) => a.apiType === DEFAULT_API_TYPE
|
||||
);
|
||||
@@ -2,3 +2,14 @@ export const APP_NAME = process.env.REACT_APP_NAME.trim()
|
||||
.split(/\s+/)
|
||||
.join("-");
|
||||
export const APP_LCNAME = APP_NAME.toLowerCase();
|
||||
export const APP_UPNAME = APP_NAME.toUpperCase();
|
||||
export const APP_CONSTS = {
|
||||
fabID: `${APP_LCNAME}-fab`,
|
||||
boxID: `${APP_LCNAME}-box`,
|
||||
popupID: `${APP_LCNAME}-popup`,
|
||||
};
|
||||
|
||||
export const APP_VERSION = process.env.REACT_APP_VERSION.split(".");
|
||||
|
||||
export const THEME_LIGHT = "light";
|
||||
export const THEME_DARK = "dark";
|
||||
|
||||
15
src/config/client.js
Normal file
15
src/config/client.js
Normal file
@@ -0,0 +1,15 @@
|
||||
export const CLIENT_WEB = "web";
|
||||
export const CLIENT_CHROME = "chrome";
|
||||
export const CLIENT_EDGE = "edge";
|
||||
export const CLIENT_FIREFOX = "firefox";
|
||||
export const CLIENT_USERSCRIPT = "userscript";
|
||||
export const CLIENT_THUNDERBIRD = "thunderbird";
|
||||
export const CLIENT_EXTS = [
|
||||
CLIENT_CHROME,
|
||||
CLIENT_EDGE,
|
||||
CLIENT_FIREFOX,
|
||||
CLIENT_THUNDERBIRD,
|
||||
];
|
||||
|
||||
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";
|
||||
1390
src/config/i18n.js
1390
src/config/i18n.js
File diff suppressed because it is too large
Load Diff
@@ -1,287 +1,9 @@
|
||||
import {
|
||||
DEFAULT_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 {
|
||||
GLOBAL_KEY,
|
||||
REMAIN_KEY,
|
||||
SHADOW_KEY,
|
||||
DEFAULT_RULE,
|
||||
DEFAULT_OW_RULE,
|
||||
BUILTIN_RULES,
|
||||
APP_LCNAME,
|
||||
};
|
||||
|
||||
export const STOKEY_MSAUTH = `${APP_NAME}_msauth`;
|
||||
export const STOKEY_SETTING = `${APP_NAME}_setting`;
|
||||
export const STOKEY_RULES = `${APP_NAME}_rules`;
|
||||
export const STOKEY_SYNC = `${APP_NAME}_sync`;
|
||||
export const STOKEY_FAB = `${APP_NAME}_fab`;
|
||||
export const STOKEY_RULESCACHE_PREFIX = `${APP_NAME}_rulescache_`;
|
||||
export const STOKEY_WEBFIXCACHE_PREFIX = `${APP_NAME}_webfixcache_`;
|
||||
|
||||
export const CMD_TOGGLE_TRANSLATE = "toggleTranslate";
|
||||
export const CMD_TOGGLE_STYLE = "toggleStyle";
|
||||
|
||||
export const CLIENT_WEB = "web";
|
||||
export const CLIENT_CHROME = "chrome";
|
||||
export const CLIENT_EDGE = "edge";
|
||||
export const CLIENT_FIREFOX = "firefox";
|
||||
export const CLIENT_USERSCRIPT = "userscript";
|
||||
export const CLIENT_EXTS = [CLIENT_CHROME, CLIENT_EDGE, CLIENT_FIREFOX];
|
||||
|
||||
export const KV_RULES_KEY = "KT_RULES";
|
||||
export const KV_RULES_SHARE_KEY = "KT_RULES_SHARE";
|
||||
export const KV_SETTING_KEY = "KT_SETTING";
|
||||
export const KV_SALT_SYNC = "KISS-Translator-SYNC";
|
||||
export const KV_SALT_SHARE = "KISS-Translator-SHARE";
|
||||
|
||||
export const CACHE_NAME = `${APP_NAME}_cache`;
|
||||
|
||||
export const MSG_FETCH = "fetch";
|
||||
export const MSG_FETCH_LIMIT = "fetch_limit";
|
||||
export const MSG_FETCH_CLEAR = "fetch_clear";
|
||||
export const MSG_TRANS_TOGGLE = "trans_toggle";
|
||||
export const MSG_TRANS_TOGGLE_STYLE = "trans_toggle_style";
|
||||
export const MSG_TRANS_GETRULE = "trans_getrule";
|
||||
export const MSG_TRANS_PUTRULE = "trans_putrule";
|
||||
export const MSG_TRANS_CURRULE = "trans_currule";
|
||||
|
||||
export const THEME_LIGHT = "light";
|
||||
export const THEME_DARK = "dark";
|
||||
|
||||
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 =
|
||||
"https://raw.githubusercontent.com/fishjar/kiss-translator/master";
|
||||
export const URL_MICROSOFT_AUTH = "https://edge.microsoft.com/translate/auth";
|
||||
|
||||
export const OPT_TRANS_GOOGLE = "Google";
|
||||
export const OPT_TRANS_MICROSOFT = "Microsoft";
|
||||
export const OPT_TRANS_DEEPL = "DeepL";
|
||||
export const OPT_TRANS_OPENAI = "OpenAI";
|
||||
export const OPT_TRANS_CUSTOMIZE = "Custom";
|
||||
export const OPT_TRANS_ALL = [
|
||||
OPT_TRANS_GOOGLE,
|
||||
OPT_TRANS_MICROSOFT,
|
||||
OPT_TRANS_DEEPL,
|
||||
OPT_TRANS_OPENAI,
|
||||
OPT_TRANS_CUSTOMIZE,
|
||||
];
|
||||
|
||||
export const OPT_LANGS_TO = [
|
||||
["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"],
|
||||
];
|
||||
export const OPT_LANGS_FROM = [["auto", "Auto-detect"], ...OPT_LANGS_TO];
|
||||
export const OPT_LANGS_SPECIAL = {
|
||||
[OPT_TRANS_MICROSOFT]: new Map([
|
||||
["auto", ""],
|
||||
["zh-CN", "zh-Hans"],
|
||||
["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_OPENAI]: new Map(
|
||||
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
||||
),
|
||||
[OPT_TRANS_CUSTOMIZE]: new Map([["auto", ""]]),
|
||||
};
|
||||
|
||||
export const OPT_STYLE_NONE = "style_none"; // 无
|
||||
export const OPT_STYLE_LINE = "under_line"; // 下划线
|
||||
export const OPT_STYLE_DOTLINE = "dot_line"; // 点状线
|
||||
export const OPT_STYLE_DASHLINE = "dash_line"; // 虚线
|
||||
export const OPT_STYLE_WAVYLINE = "wavy_line"; // 波浪线
|
||||
export const OPT_STYLE_FUZZY = "fuzzy"; // 模糊
|
||||
export const OPT_STYLE_HIGHLIGHT = "highlight"; // 高亮
|
||||
export const OPT_STYLE_DIY = "diy_style"; // 自定义样式
|
||||
export const OPT_STYLE_ALL = [
|
||||
OPT_STYLE_NONE,
|
||||
OPT_STYLE_LINE,
|
||||
OPT_STYLE_DOTLINE,
|
||||
OPT_STYLE_DASHLINE,
|
||||
OPT_STYLE_WAVYLINE,
|
||||
OPT_STYLE_FUZZY,
|
||||
OPT_STYLE_HIGHLIGHT,
|
||||
OPT_STYLE_DIY,
|
||||
];
|
||||
export const OPT_STYLE_USE_COLOR = [
|
||||
OPT_STYLE_LINE,
|
||||
OPT_STYLE_DOTLINE,
|
||||
OPT_STYLE_DASHLINE,
|
||||
OPT_STYLE_WAVYLINE,
|
||||
OPT_STYLE_HIGHLIGHT,
|
||||
];
|
||||
|
||||
export const OPT_MOUSEKEY_DISABLE = "mk_disable";
|
||||
export const OPT_MOUSEKEY_MOUSEOVER = "mk_mouseover";
|
||||
export const OPT_MOUSEKEY_CONTROL = "mk_ctrlKey";
|
||||
export const OPT_MOUSEKEY_SHIFT = "mk_shiftKey";
|
||||
export const OPT_MOUSEKEY_ALT = "mk_altKey";
|
||||
export const OPT_MOUSEKEY_ALL = [
|
||||
OPT_MOUSEKEY_DISABLE,
|
||||
OPT_MOUSEKEY_MOUSEOVER,
|
||||
OPT_MOUSEKEY_CONTROL,
|
||||
OPT_MOUSEKEY_SHIFT,
|
||||
OPT_MOUSEKEY_ALT,
|
||||
];
|
||||
|
||||
export const DEFAULT_FETCH_LIMIT = 10; // 默认最大任务数量
|
||||
export const DEFAULT_FETCH_INTERVAL = 100; // 默认任务间隔时间
|
||||
|
||||
export const PROMPT_PLACE_FROM = "{{from}}"; // 占位符
|
||||
export const PROMPT_PLACE_TO = "{{to}}"; // 占位符
|
||||
|
||||
export const DEFAULT_COLOR = "#209CEE"; // 默认高亮背景色/线条颜色
|
||||
|
||||
// 全局规则
|
||||
export const GLOBLA_RULE = {
|
||||
pattern: "*",
|
||||
selector: DEFAULT_SELECTOR,
|
||||
translator: OPT_TRANS_MICROSOFT,
|
||||
fromLang: "auto",
|
||||
toLang: "zh-CN",
|
||||
textStyle: OPT_STYLE_DASHLINE,
|
||||
transOpen: "false",
|
||||
bgColor: "",
|
||||
textDiyStyle: "",
|
||||
};
|
||||
|
||||
// 订阅列表
|
||||
export const DEFAULT_SUBRULES_LIST = [
|
||||
{
|
||||
url: process.env.REACT_APP_RULESURL,
|
||||
selected: false,
|
||||
},
|
||||
{
|
||||
url: process.env.REACT_APP_RULESURL_ON,
|
||||
selected: true,
|
||||
},
|
||||
{
|
||||
url: process.env.REACT_APP_RULESURL_OFF,
|
||||
selected: false,
|
||||
},
|
||||
];
|
||||
|
||||
// 翻译接口
|
||||
export const DEFAULT_TRANS_APIS = {
|
||||
[OPT_TRANS_GOOGLE]: {
|
||||
url: "https://translate.googleapis.com/translate_a/single",
|
||||
key: "",
|
||||
},
|
||||
[OPT_TRANS_MICROSOFT]: {
|
||||
url: "https://api-edge.cognitive.microsofttranslator.com/translate",
|
||||
authUrl: "https://edge.microsoft.com/translate/auth",
|
||||
},
|
||||
[OPT_TRANS_DEEPL]: {
|
||||
url: "https://api-free.deepl.com/v2/translate",
|
||||
key: "",
|
||||
},
|
||||
[OPT_TRANS_OPENAI]: {
|
||||
url: "https://api.openai.com/v1/chat/completion",
|
||||
key: "",
|
||||
model: "gpt-4",
|
||||
prompt: `You will be provided with a sentence in ${PROMPT_PLACE_FROM}, and your task is to translate it into ${PROMPT_PLACE_TO}.`,
|
||||
},
|
||||
[OPT_TRANS_CUSTOMIZE]: {
|
||||
url: "",
|
||||
key: "",
|
||||
},
|
||||
};
|
||||
|
||||
// 默认快捷键
|
||||
export const OPT_SHORTCUT_TRANSLATE = "toggleTranslate";
|
||||
export const OPT_SHORTCUT_STYLE = "toggleStyle";
|
||||
export const OPT_SHORTCUT_POPUP = "togglePopup";
|
||||
export const OPT_SHORTCUT_SETTING = "openSetting";
|
||||
export const DEFAULT_SHORTCUTS = {
|
||||
[OPT_SHORTCUT_TRANSLATE]: ["Alt", "q"],
|
||||
[OPT_SHORTCUT_STYLE]: ["Alt", "c"],
|
||||
[OPT_SHORTCUT_POPUP]: ["Alt", "k"],
|
||||
[OPT_SHORTCUT_SETTING]: ["Alt", "o"],
|
||||
};
|
||||
|
||||
export const TRANS_MIN_LENGTH = 5; // 最短翻译长度
|
||||
export const TRANS_MAX_LENGTH = 5000; // 最长翻译长度
|
||||
export const TRANS_NEWLINE_LENGTH = 40; // 换行字符数
|
||||
|
||||
export const DEFAULT_SETTING = {
|
||||
darkMode: false, // 深色模式
|
||||
uiLang: "en", // 界面语言
|
||||
fetchLimit: DEFAULT_FETCH_LIMIT, // 最大任务数量
|
||||
fetchInterval: DEFAULT_FETCH_INTERVAL, // 任务间隔时间
|
||||
minLength: TRANS_MIN_LENGTH,
|
||||
maxLength: TRANS_MAX_LENGTH,
|
||||
newlineLength: TRANS_NEWLINE_LENGTH,
|
||||
clearCache: false, // 是否在浏览器下次启动时清除缓存
|
||||
injectRules: true, // 是否注入订阅规则
|
||||
injectWebfix: true, // 是否注入修复补丁
|
||||
subrulesList: DEFAULT_SUBRULES_LIST, // 订阅列表
|
||||
owSubrule: DEFAULT_OW_RULE, // 覆写订阅规则
|
||||
transApis: DEFAULT_TRANS_APIS, // 翻译接口
|
||||
mouseKey: OPT_MOUSEKEY_DISABLE, // 鼠标悬停翻译
|
||||
shortcuts: DEFAULT_SHORTCUTS, // 快捷键
|
||||
hideFab: false, // 是否隐藏按钮
|
||||
};
|
||||
|
||||
export const DEFAULT_RULES = [GLOBLA_RULE];
|
||||
|
||||
export const DEFAULT_SYNC = {
|
||||
syncUrl: "", // 数据同步接口
|
||||
syncKey: "", // 数据同步密钥
|
||||
settingUpdateAt: 0,
|
||||
settingSyncAt: 0,
|
||||
rulesUpdateAt: 0,
|
||||
rulesSyncAt: 0,
|
||||
subRulesSyncAt: 0, // 订阅规则同步时间
|
||||
};
|
||||
export * from "./app";
|
||||
export * from "./rules";
|
||||
export * from "./api";
|
||||
export * from "./setting";
|
||||
export * from "./i18n";
|
||||
export * from "./storage";
|
||||
export * from "./url";
|
||||
export * from "./msg";
|
||||
export * from "./client";
|
||||
|
||||
35
src/config/msg.js
Normal file
35
src/config/msg.js
Normal file
@@ -0,0 +1,35 @@
|
||||
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 MSG_FETCH = "kiss_fetch";
|
||||
export const MSG_GET_HTTPCACHE = "get_httpcache";
|
||||
export const MSG_PUT_HTTPCACHE = "put_httpcache";
|
||||
export const MSG_OPEN_OPTIONS = "open_options";
|
||||
export const MSG_SAVE_RULE = "save_rule";
|
||||
export const MSG_TRANS_TOGGLE = "trans_toggle";
|
||||
export const MSG_TRANS_TOGGLE_STYLE = "trans_toggle_style";
|
||||
export const MSG_OPEN_TRANBOX = "open_tranbox";
|
||||
export const MSG_TRANS_GETRULE = "trans_getrule";
|
||||
export const MSG_TRANS_PUTRULE = "trans_putrule";
|
||||
export const MSG_TRANS_CURRULE = "trans_currule";
|
||||
export const MSG_TRANSBOX_TOGGLE = "transbox_toggle";
|
||||
export const MSG_POPUP_TOGGLE = "popup_toggle";
|
||||
export const MSG_MOUSEHOVER_TOGGLE = "mousehover_toggle";
|
||||
export const MSG_TRANSINPUT_TOGGLE = "transinput_toggle";
|
||||
export const MSG_CONTEXT_MENUS = "context_menus";
|
||||
export const MSG_COMMAND_SHORTCUTS = "command_shortcuts";
|
||||
export const MSG_INJECT_JS = "inject_js";
|
||||
export const MSG_INJECT_CSS = "inject_css";
|
||||
export const MSG_UPDATE_CSP = "update_csp";
|
||||
export const MSG_BUILTINAI_DETECT = "builtinai_detect";
|
||||
export const MSG_BUILTINAI_TRANSLATE = "builtinai_translte";
|
||||
export const MSG_SET_LOGLEVEL = "set_loglevel";
|
||||
export const MSG_CLEAR_CACHES = "clear_caches";
|
||||
|
||||
export const EVENT_KISS = "event_kiss_translate";
|
||||
|
||||
export const MSG_XHR_DATA_YOUTUBE = "KISS_XHR_DATA_YOUTUBE";
|
||||
// export const MSG_GLOBAL_VAR_FETCH = "KISS_GLOBAL_VAR_FETCH";
|
||||
// export const MSG_GLOBAL_VAR_BACK = "KISS_GLOBAL_VAR_BACK";
|
||||
@@ -1,25 +1,87 @@
|
||||
const els = `li, p, h1, h2, h3, h4, h5, h6, dd, blockquote`;
|
||||
|
||||
export const DEFAULT_SELECTOR = `:is(${els})`;
|
||||
import { OPT_TRANS_MICROSOFT } from "./api";
|
||||
|
||||
export const GLOBAL_KEY = "*";
|
||||
export const REMAIN_KEY = "-";
|
||||
|
||||
export const SHADOW_KEY = ">>>";
|
||||
|
||||
export const DEFAULT_RULE = {
|
||||
pattern: "",
|
||||
selector: "",
|
||||
translator: GLOBAL_KEY,
|
||||
fromLang: GLOBAL_KEY,
|
||||
toLang: GLOBAL_KEY,
|
||||
textStyle: GLOBAL_KEY,
|
||||
transOpen: GLOBAL_KEY,
|
||||
bgColor: "",
|
||||
textDiyStyle: "",
|
||||
};
|
||||
export const DEFAULT_COLOR = "#209CEE"; // 默认高亮背景色/线条颜色
|
||||
|
||||
const DEFAULT_DIY_STYLE = `color: #666;
|
||||
export const DEFAULT_TRANS_TAG = "font";
|
||||
export const DEFAULT_SELECT_STYLE =
|
||||
"-webkit-line-clamp: unset; max-height: none; height: auto;";
|
||||
|
||||
export const OPT_STYLE_NONE = "style_none"; // 无
|
||||
export const OPT_STYLE_LINE = "under_line"; // 下划线
|
||||
export const OPT_STYLE_DOTLINE = "dot_line"; // 点状线
|
||||
export const OPT_STYLE_DASHLINE = "dash_line"; // 虚线
|
||||
export const OPT_STYLE_DASHBOX = "dash_box"; // 虚线框
|
||||
export const OPT_STYLE_WAVYLINE = "wavy_line"; // 波浪线
|
||||
export const OPT_STYLE_FUZZY = "fuzzy"; // 模糊
|
||||
export const OPT_STYLE_HIGHLIGHT = "highlight"; // 高亮
|
||||
export const OPT_STYLE_BLOCKQUOTE = "blockquote"; // 引用
|
||||
export const OPT_STYLE_GRADIENT = "gradient"; // 渐变
|
||||
export const OPT_STYLE_BLINK = "blink"; // 闪现
|
||||
export const OPT_STYLE_GLOW = "glow"; // 发光
|
||||
export const OPT_STYLE_DIY = "diy_style"; // 自定义样式
|
||||
export const OPT_STYLE_ALL = [
|
||||
OPT_STYLE_NONE,
|
||||
OPT_STYLE_LINE,
|
||||
OPT_STYLE_DOTLINE,
|
||||
OPT_STYLE_DASHLINE,
|
||||
OPT_STYLE_WAVYLINE,
|
||||
OPT_STYLE_DASHBOX,
|
||||
OPT_STYLE_FUZZY,
|
||||
OPT_STYLE_HIGHLIGHT,
|
||||
OPT_STYLE_BLOCKQUOTE,
|
||||
OPT_STYLE_GRADIENT,
|
||||
OPT_STYLE_BLINK,
|
||||
OPT_STYLE_GLOW,
|
||||
OPT_STYLE_DIY,
|
||||
];
|
||||
export const OPT_STYLE_USE_COLOR = [
|
||||
OPT_STYLE_LINE,
|
||||
OPT_STYLE_DOTLINE,
|
||||
OPT_STYLE_DASHLINE,
|
||||
OPT_STYLE_DASHBOX,
|
||||
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 OPT_SPLIT_PARAGRAPH_DISABLE = "split_disable";
|
||||
export const OPT_SPLIT_PARAGRAPH_TEXTLENGTH = "split_textlength";
|
||||
export const OPT_SPLIT_PARAGRAPH_PUNCTUATION = "split_punctuation";
|
||||
export const OPT_SPLIT_PARAGRAPH_ALL = [
|
||||
OPT_SPLIT_PARAGRAPH_DISABLE,
|
||||
OPT_SPLIT_PARAGRAPH_PUNCTUATION,
|
||||
OPT_SPLIT_PARAGRAPH_TEXTLENGTH,
|
||||
];
|
||||
|
||||
export const OPT_HIGHLIGHT_WORDS_DISABLE = "highlight_disable";
|
||||
export const OPT_HIGHLIGHT_WORDS_BEFORETRANS = "highlight_beforetrans";
|
||||
export const OPT_HIGHLIGHT_WORDS_AFTERTRANS = "highlight_aftertrans";
|
||||
export const OPT_HIGHLIGHT_WORDS_ALL = [
|
||||
OPT_HIGHLIGHT_WORDS_DISABLE,
|
||||
OPT_HIGHLIGHT_WORDS_BEFORETRANS,
|
||||
OPT_HIGHLIGHT_WORDS_AFTERTRANS,
|
||||
];
|
||||
|
||||
export const DEFAULT_DIY_STYLE = `color: #333;
|
||||
background: linear-gradient(
|
||||
45deg,
|
||||
LightGreen 20%,
|
||||
@@ -29,11 +91,102 @@ background: linear-gradient(
|
||||
LightSkyBlue 80%
|
||||
);
|
||||
&:hover {
|
||||
color: #333;
|
||||
color: #111;
|
||||
};`;
|
||||
|
||||
export const DEFAULT_SELECTOR =
|
||||
"h1, h2, h3, h4, h5, h6, li, p, dd, blockquote, figcaption, label, legend";
|
||||
export const DEFAULT_IGNORE_SELECTOR = "button, footer, pre, mark, nav";
|
||||
export const DEFAULT_KEEP_SELECTOR = `code, cite, math, .math, a:has(code)`;
|
||||
export const DEFAULT_RULE = {
|
||||
pattern: "", // 匹配网址
|
||||
selector: "", // 选择器
|
||||
keepSelector: "", // 保留元素选择器
|
||||
terms: "", // 专业术语
|
||||
aiTerms: "", // AI专业术语
|
||||
apiSlug: GLOBAL_KEY, // 翻译服务
|
||||
fromLang: GLOBAL_KEY, // 源语言
|
||||
toLang: GLOBAL_KEY, // 目标语言
|
||||
textStyle: GLOBAL_KEY, // 译文样式
|
||||
transOpen: GLOBAL_KEY, // 开启翻译
|
||||
bgColor: "", // 译文颜色
|
||||
textDiyStyle: "", // 自定义译文样式
|
||||
termsStyle: "", // 专业术语样式
|
||||
highlightStyle: "", // 高亮词汇样式
|
||||
selectStyle: "", // 选择器节点样式
|
||||
parentStyle: "", // 选择器父节点样式
|
||||
grandStyle: "", // 选择器父节点样式
|
||||
injectJs: "", // 注入JS
|
||||
// injectCss: "", // 注入CSS (作废)
|
||||
transOnly: GLOBAL_KEY, // 是否仅显示译文
|
||||
// transTiming: GLOBAL_KEY, // 翻译时机/鼠标悬停翻译 (暂时作废)
|
||||
transTag: GLOBAL_KEY, // 译文元素标签
|
||||
transTitle: GLOBAL_KEY, // 是否同时翻译页面标题
|
||||
// transSelected: GLOBAL_KEY, // 是否启用划词翻译 (移回setting)
|
||||
// detectRemote: GLOBAL_KEY, // 是否使用远程语言检测 (移回setting)
|
||||
// skipLangs: [], // 不翻译的语言 (移回setting)
|
||||
// fixerSelector: "", // 修复函数选择器 (暂时作废)
|
||||
// fixerFunc: GLOBAL_KEY, // 修复函数 (暂时作废)
|
||||
transStartHook: "", // 钩子函数
|
||||
transEndHook: "", // 钩子函数
|
||||
// transRemoveHook: "", // 钩子函数 (暂时作废)
|
||||
autoScan: GLOBAL_KEY, // 是否自动识别文本节点
|
||||
hasRichText: GLOBAL_KEY, // 是否启用富文本翻译
|
||||
hasShadowroot: GLOBAL_KEY, // 是否包含shadowroot
|
||||
rootsSelector: "", // 翻译范围选择器
|
||||
ignoreSelector: "", // 不翻译的选择器
|
||||
splitParagraph: GLOBAL_KEY, // 切分段落
|
||||
splitLength: 0, // 切分段落长度
|
||||
highlightWords: GLOBAL_KEY, // 高亮词汇
|
||||
};
|
||||
|
||||
// 全局规则
|
||||
export const GLOBLA_RULE = {
|
||||
pattern: "*", // 匹配网址
|
||||
selector: DEFAULT_SELECTOR, // 选择器
|
||||
keepSelector: DEFAULT_KEEP_SELECTOR, // 保留元素选择器
|
||||
terms: "", // 专业术语
|
||||
aiTerms: "", // AI专业术语
|
||||
apiSlug: OPT_TRANS_MICROSOFT, // 翻译服务
|
||||
fromLang: "auto", // 源语言
|
||||
toLang: "zh-CN", // 目标语言
|
||||
textStyle: OPT_STYLE_NONE, // 译文样式
|
||||
transOpen: "false", // 开启翻译
|
||||
bgColor: "", // 译文颜色
|
||||
textDiyStyle: DEFAULT_DIY_STYLE, // 自定义译文样式
|
||||
termsStyle: "font-weight: bold;", // 专业术语样式
|
||||
highlightStyle: "color: red;", // 高亮词汇样式
|
||||
selectStyle: DEFAULT_SELECT_STYLE, // 选择器节点样式
|
||||
parentStyle: DEFAULT_SELECT_STYLE, // 选择器父节点样式
|
||||
grandStyle: DEFAULT_SELECT_STYLE, // 选择器祖节点样式
|
||||
injectJs: "", // 注入JS
|
||||
// injectCss: "", // 注入CSS(作废)
|
||||
transOnly: "false", // 是否仅显示译文
|
||||
// transTiming: OPT_TIMING_PAGESCROLL, // 翻译时机/鼠标悬停翻译 (暂时作废)
|
||||
transTag: DEFAULT_TRANS_TAG, // 译文元素标签
|
||||
transTitle: "false", // 是否同时翻译页面标题
|
||||
// transSelected: "true", // 是否启用划词翻译 (移回setting)
|
||||
// detectRemote: "true", // 是否使用远程语言检测 (移回setting)
|
||||
// skipLangs: [], // 不翻译的语言 (移回setting)
|
||||
// fixerSelector: "", // 修复函数选择器 (暂时作废)
|
||||
// fixerFunc: "-", // 修复函数 (暂时作废)
|
||||
transStartHook: "", // 钩子函数
|
||||
transEndHook: "", // 钩子函数
|
||||
// transRemoveHook: "", // 钩子函数 (暂时作废)
|
||||
autoScan: "true", // 是否自动识别文本节点
|
||||
hasRichText: "true", // 是否启用富文本翻译
|
||||
hasShadowroot: "false", // 是否包含shadowroot
|
||||
rootsSelector: "body", // 翻译范围选择器
|
||||
ignoreSelector: DEFAULT_IGNORE_SELECTOR, // 不翻译的选择器
|
||||
splitParagraph: OPT_SPLIT_PARAGRAPH_DISABLE, // 切分段落
|
||||
splitLength: 100, // 切分段落长度
|
||||
highlightWords: OPT_HIGHLIGHT_WORDS_DISABLE, // 高亮词汇
|
||||
};
|
||||
|
||||
export const DEFAULT_RULES = [GLOBLA_RULE];
|
||||
|
||||
export const DEFAULT_OW_RULE = {
|
||||
translator: REMAIN_KEY,
|
||||
apiSlug: REMAIN_KEY,
|
||||
fromLang: REMAIN_KEY,
|
||||
toLang: REMAIN_KEY,
|
||||
textStyle: REMAIN_KEY,
|
||||
@@ -42,145 +195,46 @@ export const DEFAULT_OW_RULE = {
|
||||
textDiyStyle: DEFAULT_DIY_STYLE,
|
||||
};
|
||||
|
||||
const RULES = [
|
||||
{
|
||||
pattern: `www.google.com/search`,
|
||||
selector: `h3, .IsZvec, .VwiC3b`,
|
||||
// todo: 校验几个内置规则
|
||||
const RULES_MAP = {
|
||||
// "www.google.com/search": {
|
||||
// rootsSelector: `#rcnt`,
|
||||
// },
|
||||
"en.wikipedia.org": {
|
||||
ignoreSelector: `.button, code, footer, form, mark, pre, .mwe-math-element, .mw-editsection`,
|
||||
},
|
||||
{
|
||||
pattern: `news.google.com`,
|
||||
selector: `h4`,
|
||||
"news.ycombinator.com": {
|
||||
selector: `p, .titleline, .commtext, .hn-item-title, .hn-comment-text, .hn-story-title`,
|
||||
keepSelector: `code, img, svg, pre, .sitebit`,
|
||||
ignoreSelector: `button, code, footer, form, header, mark, nav, pre, .reply`,
|
||||
autoScan: `false`,
|
||||
},
|
||||
{
|
||||
pattern: `www.foxnews.com`,
|
||||
selector: `h1, h2, .title, .sidebar [data-type="Title"], .article-content ${DEFAULT_SELECTOR}; [data-spotim-module="conversation"]>div >>> [data-spot-im-class="message-text"] p, [data-spot-im-class="message-text"]`,
|
||||
},
|
||||
{
|
||||
pattern: `bearblog.dev, www.theverge.com, www.tampermonkey.net/documentation.php`,
|
||||
selector: DEFAULT_SELECTOR,
|
||||
},
|
||||
{
|
||||
pattern: `themessenger.com`,
|
||||
selector: `.leading-tight, .leading-tighter, .my-2 p, .font-body p, article ${DEFAULT_SELECTOR}`,
|
||||
},
|
||||
{
|
||||
pattern: `www.telegraph.co.uk`,
|
||||
selector: `article ${DEFAULT_SELECTOR}`,
|
||||
},
|
||||
{
|
||||
pattern: `www.theguardian.com`,
|
||||
selector: `.show-underline, .dcr-hup5wm div, .dcr-7vl6y8 div, .dcr-12evv1c, figcaption, article ${DEFAULT_SELECTOR}, [data-cy="mostviewed-footer"] h4`,
|
||||
},
|
||||
{
|
||||
pattern: `www.semafor.com`,
|
||||
selector: `${DEFAULT_SELECTOR}, .styles_intro__IYj__, [class*="styles_description"]`,
|
||||
},
|
||||
{
|
||||
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`,
|
||||
},
|
||||
{
|
||||
pattern: `restofworld.org`,
|
||||
selector: `${DEFAULT_SELECTOR}, .recirc-story__headline, .recirc-story__dek`,
|
||||
},
|
||||
{
|
||||
pattern: `www.axios.com`,
|
||||
selector: `.h7, ${DEFAULT_SELECTOR}`,
|
||||
},
|
||||
{
|
||||
pattern: `www.newyorker.com`,
|
||||
selector: `.summary-item__hed, .summary-item__dek, .summary-collection-grid__dek, .dqtvfu, .rubric__link, .caption, article ${DEFAULT_SELECTOR}, .HEhan ${DEFAULT_SELECTOR}, .ContributorBioBio-fBolsO`,
|
||||
},
|
||||
{
|
||||
pattern: `https://time.com/`,
|
||||
selector: `h1, h3, .summary, .video-title, #article-body ${DEFAULT_SELECTOR}, .image-wrap-container .credit.body-caption, .media-heading`,
|
||||
},
|
||||
{
|
||||
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}`,
|
||||
},
|
||||
{
|
||||
pattern: `www.bbc.com`,
|
||||
selector: `h1, h2, .media__link, .media__summary, article ${DEFAULT_SELECTOR}, .ssrcss-y7krbn-Stack, .ssrcss-1mrs5ns-PromoLink, .ssrcss-18cjaf3-Headline, .gs-c-promo-heading__title, .gs-c-promo-summary, .media__content h3, .article__intro`,
|
||||
},
|
||||
{
|
||||
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}`,
|
||||
},
|
||||
{
|
||||
pattern: `www.facebook.com`,
|
||||
selector: `[role="main"] [dir="auto"]`,
|
||||
},
|
||||
{
|
||||
pattern: `www.reddit.com`,
|
||||
selector: `[slot="title"], [slot="text-body"] ${DEFAULT_SELECTOR}, #-post-rtjson-content p`,
|
||||
},
|
||||
{
|
||||
pattern: `www.quora.com`,
|
||||
selector: `.qu-wordBreak--break-word`,
|
||||
},
|
||||
{
|
||||
pattern: `edition.cnn.com`,
|
||||
selector: `.container__title, .container__headline, .headline__text, .image__caption, [data-type="Title"], .article__content ${DEFAULT_SELECTOR}`,
|
||||
},
|
||||
{
|
||||
pattern: `www.reuters.com`,
|
||||
selector: `#main-content [data-testid="Heading"], #main-content [data-testid="Body"], .article-body__content__17Yit ${DEFAULT_SELECTOR}`,
|
||||
},
|
||||
{
|
||||
pattern: `www.bloomberg.com`,
|
||||
selector: `[data-component="headline"], [data-component="related-item-headline"], [data-component="title"], article ${DEFAULT_SELECTOR}`,
|
||||
},
|
||||
{
|
||||
pattern: `deno.land, docs.github.com`,
|
||||
selector: `main ${DEFAULT_SELECTOR}`,
|
||||
},
|
||||
{
|
||||
pattern: `doc.rust-lang.org`,
|
||||
selector: `#content ${DEFAULT_SELECTOR}`,
|
||||
},
|
||||
{
|
||||
pattern: `www.indiehackers.com`,
|
||||
selector: `h1, h3, .content ${DEFAULT_SELECTOR}, .feed-item__title-link`,
|
||||
},
|
||||
{
|
||||
pattern: `platform.openai.com/docs`,
|
||||
selector: `.docs-body ${DEFAULT_SELECTOR}`,
|
||||
},
|
||||
{
|
||||
pattern: `en.wikipedia.org`,
|
||||
selector: `h1, .mw-parser-output ${DEFAULT_SELECTOR}`,
|
||||
},
|
||||
{
|
||||
pattern: `stackoverflow.com`,
|
||||
selector: `h1, .s-prose p, .comment-body .comment-copy`,
|
||||
},
|
||||
{
|
||||
pattern: `www.npmjs.com/package/, developer.chrome.com/docs, medium.com, developers.cloudflare.com, react.dev, create-react-app.dev, pytorch.org/`,
|
||||
selector: `article ${DEFAULT_SELECTOR}`,
|
||||
},
|
||||
{
|
||||
pattern: `news.ycombinator.com`,
|
||||
selector: `.title, .commtext`,
|
||||
},
|
||||
{
|
||||
pattern: `https://github.com/`,
|
||||
selector: `.markdown-body ${DEFAULT_SELECTOR}, .repo-description p, .Layout-sidebar .f4, .container-lg .py-4 .f5, .container-lg .my-4 .f5, .Box-row .pr-4, .Box-row article .mt-1, [itemprop='description'], .markdown-title, bdi`,
|
||||
},
|
||||
{
|
||||
pattern: `twitter.com`,
|
||||
"twitter.com, https://x.com": {
|
||||
selector: `[data-testid='tweetText']`,
|
||||
keepSelector: `img, svg, a, span:has(a), div:has(a)`,
|
||||
ignoreSelector: `button, [data-testid='videoPlayer'], [role='group']`,
|
||||
autoScan: `false`,
|
||||
},
|
||||
{
|
||||
pattern: `youtube.com`,
|
||||
selector: `h1, #video-title, #content-text, #title, yt-attributed-string>span>span`,
|
||||
"www.youtube.com/live_chat": {
|
||||
rootsSelector: `div#items`,
|
||||
selector: `span.yt-live-chat-text-message-renderer`,
|
||||
autoScan: `false`,
|
||||
},
|
||||
];
|
||||
"www.youtube.com": {
|
||||
rootsSelector: `ytd-page-manager`,
|
||||
ignoreSelector: `aside, button, footer, form, header, pre, mark, nav, #player, #container, .caption-window, .ytp-settings-menu`,
|
||||
},
|
||||
"web.telegram.org": {
|
||||
autoScan: `false`,
|
||||
selector: ".text-content, .embedded-text-wrapper",
|
||||
rootsSelector: ".Transition",
|
||||
},
|
||||
};
|
||||
|
||||
export const BUILTIN_RULES = RULES.sort((a, b) =>
|
||||
a.pattern.localeCompare(b.pattern)
|
||||
).map((item) => ({
|
||||
...DEFAULT_RULE,
|
||||
...item,
|
||||
transOpen: "true",
|
||||
}));
|
||||
export const BUILTIN_RULES = Object.entries(RULES_MAP).map(
|
||||
([pattern, rule]) => ({
|
||||
// ...DEFAULT_RULE,
|
||||
...rule,
|
||||
pattern,
|
||||
})
|
||||
);
|
||||
|
||||
186
src/config/setting.js
Normal file
186
src/config/setting.js
Normal file
@@ -0,0 +1,186 @@
|
||||
import { LogLevel } from "../libs/log";
|
||||
import {
|
||||
OPT_DICT_BING,
|
||||
OPT_SUG_YOUDAO,
|
||||
DEFAULT_HTTP_TIMEOUT,
|
||||
OPT_TRANS_MICROSOFT,
|
||||
DEFAULT_API_LIST,
|
||||
} from "./api";
|
||||
|
||||
// 默认快捷键
|
||||
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 = 2; // 最短翻译长度
|
||||
export const TRANS_MAX_LENGTH = 100000; // 最长翻译长度
|
||||
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",
|
||||
]; // 禁用翻译名单
|
||||
export const DEFAULT_CSPLIST = []; // 禁用CSP名单
|
||||
export const DEFAULT_ORILIST = ["https://dict.youdao.com"]; // 移除Origin名单
|
||||
|
||||
// 同步设置
|
||||
export const OPT_SYNCTYPE_WORKER = "KISS-Worker";
|
||||
export const OPT_SYNCTYPE_WEBDAV = "WebDAV";
|
||||
export const OPT_SYNCTOKEN_PERFIX = "kt_";
|
||||
export const OPT_SYNCTYPE_ALL = [OPT_SYNCTYPE_WORKER, OPT_SYNCTYPE_WEBDAV];
|
||||
export const DEFAULT_SYNC = {
|
||||
syncType: OPT_SYNCTYPE_WORKER, // 同步方式
|
||||
syncUrl: "", // 数据同步接口
|
||||
syncUser: "", // 数据同步用户名
|
||||
syncKey: "", // 数据同步密钥
|
||||
syncMeta: {}, // 数据更新及同步信息
|
||||
subRulesSyncAt: 0, // 订阅规则同步时间
|
||||
dataCaches: {}, // 缓存同步时间
|
||||
};
|
||||
|
||||
// 输入框翻译
|
||||
export const OPT_INPUT_TRANS_SIGNS = ["/", "//", "\\", "\\\\", ">", ">>"];
|
||||
export const DEFAULT_INPUT_SHORTCUT = ["AltLeft", "KeyI"];
|
||||
export const DEFAULT_INPUT_RULE = {
|
||||
transOpen: true,
|
||||
apiSlug: 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, // 是否启用划词翻译
|
||||
apiSlugs: [OPT_TRANS_MICROSOFT],
|
||||
fromLang: "auto",
|
||||
toLang: "zh-CN",
|
||||
toLang2: "en",
|
||||
tranboxShortcut: DEFAULT_TRANBOX_SHORTCUT,
|
||||
btnOffsetX: 10,
|
||||
btnOffsetY: 10,
|
||||
boxOffsetX: 0,
|
||||
boxOffsetY: 10,
|
||||
hideTranBtn: false, // 是否隐藏翻译按钮
|
||||
hideClickAway: false, // 是否点击外部关闭弹窗
|
||||
simpleStyle: false, // 是否简洁界面
|
||||
followSelection: false, // 翻译框是否跟随选中文本
|
||||
autoHeight: false, // 自适应高度
|
||||
triggerMode: OPT_TRANBOX_TRIGGER_CLICK, // 触发翻译方式
|
||||
// extStyles: "", // 附加样式
|
||||
enDict: OPT_DICT_BING, // 英文词典
|
||||
enSug: OPT_SUG_YOUDAO, // 英文建议
|
||||
};
|
||||
|
||||
const SUBTITLE_WINDOW_STYLE = `padding: 0.5em 1em;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
color: white;
|
||||
line-height: 1.3;
|
||||
text-shadow: 1px 1px 2px black;
|
||||
display: inline-block`;
|
||||
|
||||
const SUBTITLE_ORIGIN_STYLE = `font-size: clamp(1.5rem, 3cqw, 3rem);`;
|
||||
|
||||
const SUBTITLE_TRANSLATION_STYLE = `font-size: clamp(1.5rem, 3cqw, 3rem);`;
|
||||
|
||||
export const DEFAULT_SUBTITLE_SETTING = {
|
||||
enabled: true, // 是否开启
|
||||
apiSlug: OPT_TRANS_MICROSOFT,
|
||||
segSlug: "-", // AI智能断句
|
||||
chunkLength: 1000, // AI处理切割长度
|
||||
// fromLang: "en",
|
||||
toLang: "zh-CN",
|
||||
isBilingual: true, // 是否双语显示
|
||||
skipAd: false, // 是否快进广告
|
||||
windowStyle: SUBTITLE_WINDOW_STYLE, // 背景样式
|
||||
originStyle: SUBTITLE_ORIGIN_STYLE, // 原文样式
|
||||
translationStyle: SUBTITLE_TRANSLATION_STYLE, // 译文样式
|
||||
};
|
||||
|
||||
// 订阅列表
|
||||
export const DEFAULT_SUBRULES_LIST = [
|
||||
{
|
||||
url: process.env.REACT_APP_RULESURL,
|
||||
selected: true,
|
||||
},
|
||||
{
|
||||
url: process.env.REACT_APP_RULESURL_ON,
|
||||
selected: false,
|
||||
},
|
||||
{
|
||||
url: process.env.REACT_APP_RULESURL_OFF,
|
||||
selected: false,
|
||||
},
|
||||
];
|
||||
|
||||
export const DEFAULT_MOUSEHOVER_KEY = ["ControlLeft"];
|
||||
export const DEFAULT_MOUSE_HOVER_SETTING = {
|
||||
useMouseHover: false, // 是否启用鼠标悬停翻译
|
||||
mouseHoverKey: DEFAULT_MOUSEHOVER_KEY, // 鼠标悬停翻译组合键
|
||||
};
|
||||
|
||||
export const DEFAULT_SETTING = {
|
||||
darkMode: "auto", // 深色模式
|
||||
uiLang: "en", // 界面语言
|
||||
// fetchLimit: DEFAULT_FETCH_LIMIT, // 最大任务数量(移至rule,作废)
|
||||
// fetchInterval: DEFAULT_FETCH_INTERVAL, // 任务间隔时间(移至rule,作废)
|
||||
minLength: TRANS_MIN_LENGTH,
|
||||
maxLength: TRANS_MAX_LENGTH,
|
||||
newlineLength: TRANS_NEWLINE_LENGTH,
|
||||
httpTimeout: DEFAULT_HTTP_TIMEOUT,
|
||||
clearCache: false, // 是否在浏览器下次启动时清除缓存
|
||||
injectRules: true, // 是否注入订阅规则
|
||||
fabClickAction: 0, // 悬浮按钮点击行为
|
||||
// injectWebfix: true, // 是否注入修复补丁(作废)
|
||||
// detectRemote: false, // 是否使用远程语言检测 (从rule移回)
|
||||
// contextMenus: true, // 是否添加右键菜单(作废)
|
||||
contextMenuType: 1, // 右键菜单类型(0不显示,1简单菜单,2多级菜单)
|
||||
// transTag: DEFAULT_TRANS_TAG, // 译文元素标签(移至rule,作废)
|
||||
// transOnly: false, // 是否仅显示译文(移至rule,作废)
|
||||
// transTitle: false, // 是否同时翻译页面标题(移至rule,作废)
|
||||
subrulesList: DEFAULT_SUBRULES_LIST, // 订阅列表
|
||||
// owSubrule: DEFAULT_OW_RULE, // 覆写订阅规则 (作废)
|
||||
transApis: DEFAULT_API_LIST, // 翻译接口 (v2.0 对象改为数组)
|
||||
// mouseKey: OPT_TIMING_PAGESCROLL, // 翻译时机/鼠标悬停翻译(移至rule,作废)
|
||||
shortcuts: DEFAULT_SHORTCUTS, // 快捷键
|
||||
inputRule: DEFAULT_INPUT_RULE, // 输入框设置
|
||||
tranboxSetting: DEFAULT_TRANBOX_SETTING, // 划词翻译设置
|
||||
// touchTranslate: 2, // 触屏翻译 {5:单指双击,6:单指三击,7:双指双击} (作废)
|
||||
touchModes: [2], // 触屏翻译 {5:单指双击,6:单指三击,7:双指双击} (多选)
|
||||
blacklist: DEFAULT_BLACKLIST.join(",\n"), // 禁用翻译名单
|
||||
csplist: DEFAULT_CSPLIST.join(",\n"), // 禁用CSP名单
|
||||
orilist: DEFAULT_ORILIST.join(",\n"), // 禁用CSP名单
|
||||
// disableLangs: [], // 不翻译的语言(移至rule,作废)
|
||||
skipLangs: [], // 不翻译的语言(从rule移回)
|
||||
transInterval: 100, // 翻译等待时间
|
||||
langDetector: "-", // 远程语言识别服务
|
||||
mouseHoverSetting: DEFAULT_MOUSE_HOVER_SETTING, // 鼠标悬停翻译
|
||||
preInit: true, // 是否预加载脚本
|
||||
transAllnow: false, // 是否立即全部翻译
|
||||
subtitleSetting: DEFAULT_SUBTITLE_SETTING, // 字幕设置
|
||||
logLevel: LogLevel.INFO.value, // 日志级别
|
||||
rootMargin: 500, // 提前触发翻译
|
||||
};
|
||||
23
src/config/storage.js
Normal file
23
src/config/storage.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { APP_NAME, APP_VERSION } from "./app";
|
||||
|
||||
export const KV_RULES_KEY = `kiss-rules_v${APP_VERSION[0]}.json`;
|
||||
export const KV_WORDS_KEY = "kiss-words.json";
|
||||
export const KV_RULES_SHARE_KEY = `kiss-rules-share_v${APP_VERSION[0]}.json`;
|
||||
export const KV_SETTING_KEY = `kiss-setting_v${APP_VERSION[0]}.json`;
|
||||
export const KV_SALT_SYNC = "KISS-Translator-SYNC";
|
||||
export const KV_SALT_SHARE = "KISS-Translator-SHARE";
|
||||
|
||||
export const STOKEY_MSAUTH = `${APP_NAME}_msauth`;
|
||||
export const STOKEY_BDAUTH = `${APP_NAME}_bdauth`;
|
||||
export const STOKEY_SETTING_OLD = `${APP_NAME}_setting`;
|
||||
export const STOKEY_RULES_OLD = `${APP_NAME}_rules`;
|
||||
export const STOKEY_SETTING = `${APP_NAME}_setting_v${APP_VERSION[0]}`;
|
||||
export const STOKEY_RULES = `${APP_NAME}_rules_v${APP_VERSION[0]}`;
|
||||
export const STOKEY_WORDS = `${APP_NAME}_words`;
|
||||
export const STOKEY_SYNC = `${APP_NAME}_sync`;
|
||||
export const STOKEY_FAB = `${APP_NAME}_fab`;
|
||||
export const STOKEY_TRANBOX = `${APP_NAME}_tranbox`;
|
||||
export const STOKEY_RULESCACHE_PREFIX = `${APP_NAME}_rulescache_`;
|
||||
|
||||
export const CACHE_NAME = `${APP_NAME}_cache`;
|
||||
export const DEFAULT_CACHE_TIMEOUT = 3600 * 24 * 7; // 缓存超时时间(7天)
|
||||
14
src/config/url.js
Normal file
14
src/config/url.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { APP_LCNAME } from "./app";
|
||||
|
||||
export const URL_CACHE_TRAN = `https://${APP_LCNAME}/translate`;
|
||||
export const URL_CACHE_SUBTITLE = `https://${APP_LCNAME}/subtitle`;
|
||||
export const URL_CACHE_DELANG = `https://${APP_LCNAME}/detectlang`;
|
||||
export const URL_CACHE_BINGDICT = `https://${APP_LCNAME}/bingdict`;
|
||||
|
||||
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 =
|
||||
"https://raw.githubusercontent.com/fishjar/kiss-translator/master";
|
||||
@@ -1,56 +1,5 @@
|
||||
import { browser } from "./libs/browser";
|
||||
import {
|
||||
MSG_TRANS_TOGGLE,
|
||||
MSG_TRANS_TOGGLE_STYLE,
|
||||
MSG_TRANS_GETRULE,
|
||||
MSG_TRANS_PUTRULE,
|
||||
} from "./config";
|
||||
import { getSettingWithDefault, getRulesWithDefault } from "./libs/storage";
|
||||
import { Translator } from "./libs/translator";
|
||||
import { isIframe } from "./libs/iframe";
|
||||
import { matchRule } from "./libs/rules";
|
||||
import { webfix } from "./libs/webfix";
|
||||
import { run } from "./common";
|
||||
|
||||
/**
|
||||
* 入口函数
|
||||
*/
|
||||
const init = async () => {
|
||||
const href = isIframe ? document.referrer : document.location.href;
|
||||
const setting = await getSettingWithDefault();
|
||||
const rules = await getRulesWithDefault();
|
||||
const rule = await matchRule(rules, href, setting);
|
||||
const translator = new Translator(rule, setting);
|
||||
webfix(href, setting);
|
||||
|
||||
// 监听消息
|
||||
browser?.runtime.onMessage.addListener(async ({ action, args }) => {
|
||||
switch (action) {
|
||||
case MSG_TRANS_TOGGLE:
|
||||
translator.toggle();
|
||||
break;
|
||||
case MSG_TRANS_TOGGLE_STYLE:
|
||||
translator.toggleStyle();
|
||||
break;
|
||||
case MSG_TRANS_GETRULE:
|
||||
break;
|
||||
case MSG_TRANS_PUTRULE:
|
||||
translator.updateRule(args);
|
||||
break;
|
||||
default:
|
||||
return { error: `message action is unavailable: ${action}` };
|
||||
}
|
||||
return { data: translator.rule };
|
||||
});
|
||||
};
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
await init();
|
||||
} catch (err) {
|
||||
console.error("[KISS-Translator]", err);
|
||||
const $err = document.createElement("div");
|
||||
$err.innerText = `KISS-Translator: ${err.message}`;
|
||||
$err.style.cssText = "background:red; color:#fff;";
|
||||
document.body.prepend($err);
|
||||
}
|
||||
})();
|
||||
if (document.documentElement && document.documentElement.tagName === "HTML") {
|
||||
run();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { createContext, useContext, useState, forwardRef } from "react";
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useState,
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from "react";
|
||||
import Snackbar from "@mui/material/Snackbar";
|
||||
import MuiAlert from "@mui/material/Alert";
|
||||
|
||||
@@ -18,36 +25,45 @@ export function AlertProvider({ children }) {
|
||||
const horizontal = "center";
|
||||
const [open, setOpen] = useState(false);
|
||||
const [severity, setSeverity] = useState("info");
|
||||
const [message, setMessage] = useState("");
|
||||
const [message, setMessage] = useState(null);
|
||||
|
||||
const showAlert = (msg, type) => {
|
||||
const showAlert = useCallback((msg, type) => {
|
||||
setOpen(true);
|
||||
setMessage(msg);
|
||||
setSeverity(type);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleClose = (_, reason) => {
|
||||
const handleClose = useCallback((_, 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");
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
error: (msg) => showAlert(msg, "error"),
|
||||
warning: (msg) => showAlert(msg, "warning"),
|
||||
info: (msg) => showAlert(msg, "info"),
|
||||
success: (msg) => showAlert(msg, "success"),
|
||||
}),
|
||||
[showAlert]
|
||||
);
|
||||
|
||||
return (
|
||||
<AlertContext.Provider value={{ error, warning, info, success }}>
|
||||
<AlertContext.Provider value={value}>
|
||||
{children}
|
||||
<Snackbar
|
||||
open={open}
|
||||
autoHideDuration={3000}
|
||||
autoHideDuration={10000}
|
||||
onClose={handleClose}
|
||||
anchorOrigin={{ vertical, horizontal }}
|
||||
>
|
||||
<Alert onClose={handleClose} severity={severity} sx={{ width: "100%" }}>
|
||||
<Alert
|
||||
onClose={handleClose}
|
||||
severity={severity}
|
||||
sx={{ minWidth: "300px", maxWidth: "80%" }}
|
||||
>
|
||||
{message}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
|
||||
144
src/hooks/Api.js
144
src/hooks/Api.js
@@ -1,24 +1,136 @@
|
||||
import { useCallback } from "react";
|
||||
import { DEFAULT_TRANS_APIS } from "../config";
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import { DEFAULT_API_LIST, API_SPE_TYPES } from "../config";
|
||||
import { useSetting } from "./Setting";
|
||||
|
||||
export function useApi(translator) {
|
||||
function useApiState() {
|
||||
const { setting, updateSetting } = useSetting();
|
||||
const transApis = setting?.transApis || DEFAULT_TRANS_APIS;
|
||||
const transApis = setting?.transApis || [];
|
||||
|
||||
const updateApi = useCallback(
|
||||
async (obj) => {
|
||||
const api = transApis[translator] || {};
|
||||
Object.assign(transApis, { [translator]: { ...api, ...obj } });
|
||||
await updateSetting({ transApis });
|
||||
},
|
||||
[translator, transApis, updateSetting]
|
||||
return { transApis, updateSetting };
|
||||
}
|
||||
|
||||
export function useApiList() {
|
||||
const { transApis, updateSetting } = useApiState();
|
||||
|
||||
useEffect(() => {
|
||||
const curSlugs = new Set(transApis.map((api) => api.apiSlug));
|
||||
const missApis = DEFAULT_API_LIST.filter(
|
||||
(api) => !curSlugs.has(api.apiSlug)
|
||||
);
|
||||
if (missApis.length > 0) {
|
||||
updateSetting((prev) => ({
|
||||
...prev,
|
||||
transApis: [...(prev?.transApis || []), ...missApis],
|
||||
}));
|
||||
}
|
||||
}, [transApis, updateSetting]);
|
||||
|
||||
const userApis = useMemo(
|
||||
() =>
|
||||
transApis
|
||||
.filter((api) => !API_SPE_TYPES.builtin.has(api.apiSlug))
|
||||
.sort((a, b) => a.apiSlug.localeCompare(b.apiSlug)),
|
||||
[transApis]
|
||||
);
|
||||
|
||||
const resetApi = useCallback(async () => {
|
||||
Object.assign(transApis, { [translator]: DEFAULT_TRANS_APIS[translator] });
|
||||
await updateSetting({ transApis });
|
||||
}, [translator, transApis, updateSetting]);
|
||||
const builtinApis = useMemo(
|
||||
() => transApis.filter((api) => API_SPE_TYPES.builtin.has(api.apiSlug)),
|
||||
[transApis]
|
||||
);
|
||||
|
||||
return { api: transApis[translator] || {}, updateApi, resetApi };
|
||||
const enabledApis = useMemo(
|
||||
() => transApis.filter((api) => !api.isDisabled),
|
||||
[transApis]
|
||||
);
|
||||
|
||||
const aiEnabledApis = useMemo(
|
||||
() => enabledApis.filter((api) => API_SPE_TYPES.ai.has(api.apiType)),
|
||||
[enabledApis]
|
||||
);
|
||||
|
||||
const addApi = useCallback(
|
||||
(apiType) => {
|
||||
const defaultApiOpt =
|
||||
DEFAULT_API_LIST.find((da) => da.apiType === apiType) || {};
|
||||
const uuid = crypto.randomUUID();
|
||||
const apiSlug = `${apiType}_${crypto.randomUUID()}`;
|
||||
const apiName = `${apiType}_${uuid.slice(0, 8)}`;
|
||||
const newApi = {
|
||||
...defaultApiOpt,
|
||||
apiSlug,
|
||||
apiName,
|
||||
apiType,
|
||||
};
|
||||
updateSetting((prev) => ({
|
||||
...prev,
|
||||
transApis: [...(prev?.transApis || []), newApi],
|
||||
}));
|
||||
},
|
||||
[updateSetting]
|
||||
);
|
||||
|
||||
const deleteApi = useCallback(
|
||||
(apiSlug) => {
|
||||
updateSetting((prev) => ({
|
||||
...prev,
|
||||
transApis: (prev?.transApis || []).filter(
|
||||
(api) => api.apiSlug !== apiSlug
|
||||
),
|
||||
}));
|
||||
},
|
||||
[updateSetting]
|
||||
);
|
||||
|
||||
return {
|
||||
transApis,
|
||||
userApis,
|
||||
builtinApis,
|
||||
enabledApis,
|
||||
aiEnabledApis,
|
||||
addApi,
|
||||
deleteApi,
|
||||
};
|
||||
}
|
||||
|
||||
export function useApiItem(apiSlug) {
|
||||
const { transApis, updateSetting } = useApiState();
|
||||
|
||||
const api = useMemo(
|
||||
() => transApis.find((a) => a.apiSlug === apiSlug),
|
||||
[transApis, apiSlug]
|
||||
);
|
||||
|
||||
const update = useCallback(
|
||||
(updateData) => {
|
||||
updateSetting((prev) => ({
|
||||
...prev,
|
||||
transApis: (prev?.transApis || []).map((item) =>
|
||||
item.apiSlug === apiSlug ? { ...item, ...updateData, apiSlug } : item
|
||||
),
|
||||
}));
|
||||
},
|
||||
[apiSlug, updateSetting]
|
||||
);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
updateSetting((prev) => ({
|
||||
...prev,
|
||||
transApis: (prev?.transApis || []).map((item) => {
|
||||
if (item.apiSlug === apiSlug) {
|
||||
const defaultApiOpt =
|
||||
DEFAULT_API_LIST.find((da) => da.apiType === item.apiType) || {};
|
||||
return {
|
||||
...defaultApiOpt,
|
||||
apiSlug: item.apiSlug,
|
||||
apiName: item.apiName,
|
||||
apiType: item.apiType,
|
||||
key: item.key,
|
||||
};
|
||||
}
|
||||
return item;
|
||||
}),
|
||||
}));
|
||||
}, [apiSlug, updateSetting]);
|
||||
|
||||
return { api, update, reset };
|
||||
}
|
||||
|
||||
61
src/hooks/Audio.js
Normal file
61
src/hooks/Audio.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { apiBaiduTTS } from "../apis";
|
||||
import { kissLog } from "../libs/log";
|
||||
|
||||
/**
|
||||
* 声音播放hook
|
||||
* @param {*} src
|
||||
* @returns
|
||||
*/
|
||||
export function useAudio(src) {
|
||||
const audioRef = useRef(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [ready, setReady] = useState(false);
|
||||
const [playing, setPlaying] = useState(false);
|
||||
|
||||
const onPlay = useCallback(() => {
|
||||
audioRef.current?.play();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!src) {
|
||||
return;
|
||||
}
|
||||
const audio = new Audio(src);
|
||||
audio.addEventListener("error", (err) => setError(err));
|
||||
audio.addEventListener("canplaythrough", () => setReady(true));
|
||||
audio.addEventListener("play", () => setPlaying(true));
|
||||
audio.addEventListener("ended", () => setPlaying(false));
|
||||
audioRef.current = audio;
|
||||
}, [src]);
|
||||
|
||||
return {
|
||||
error,
|
||||
ready,
|
||||
playing,
|
||||
onPlay,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取语音hook
|
||||
* @param {*} text
|
||||
* @param {*} lan
|
||||
* @param {*} spd
|
||||
* @returns
|
||||
*/
|
||||
export function useTextAudio(text, lan = "uk", spd = 3) {
|
||||
const [src, setSrc] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
setSrc(await apiBaiduTTS(text, lan, spd));
|
||||
} catch (err) {
|
||||
kissLog("baidu tts", err);
|
||||
}
|
||||
})();
|
||||
}, [text, lan, spd]);
|
||||
|
||||
return useAudio(src);
|
||||
}
|
||||
@@ -11,8 +11,13 @@ export function useDarkMode() {
|
||||
updateSetting,
|
||||
} = useSetting();
|
||||
|
||||
const toggleDarkMode = useCallback(async () => {
|
||||
await updateSetting({ darkMode: !darkMode });
|
||||
const toggleDarkMode = useCallback(() => {
|
||||
const nextMode = {
|
||||
light: "dark",
|
||||
dark: "auto",
|
||||
auto: "light",
|
||||
};
|
||||
updateSetting({ darkMode: nextMode[darkMode] || "light" });
|
||||
}, [darkMode, updateSetting]);
|
||||
|
||||
return { darkMode, toggleDarkMode };
|
||||
|
||||
97
src/hooks/Confirm.js
Normal file
97
src/hooks/Confirm.js
Normal file
@@ -0,0 +1,97 @@
|
||||
import {
|
||||
useState,
|
||||
useContext,
|
||||
createContext,
|
||||
useCallback,
|
||||
useRef,
|
||||
useMemo,
|
||||
} from "react";
|
||||
import Dialog from "@mui/material/Dialog";
|
||||
import DialogActions from "@mui/material/DialogActions";
|
||||
import DialogContent from "@mui/material/DialogContent";
|
||||
import DialogContentText from "@mui/material/DialogContentText";
|
||||
import DialogTitle from "@mui/material/DialogTitle";
|
||||
import Button from "@mui/material/Button";
|
||||
import { useI18n } from "./I18n";
|
||||
|
||||
const ConfirmContext = createContext(null);
|
||||
|
||||
export function ConfirmProvider({ children }) {
|
||||
const [dialogConfig, setDialogConfig] = useState(null);
|
||||
const resolveRef = useRef(null);
|
||||
const i18n = useI18n();
|
||||
|
||||
const translatedDefaults = useMemo(
|
||||
() => ({
|
||||
title: i18n("confirm_title", "Confirm"),
|
||||
message: i18n("confirm_message", "Are you sure you want to proceed?"),
|
||||
confirmText: i18n("confirm_action", "Confirm"),
|
||||
cancelText: i18n("cancel_action", "Cancel"),
|
||||
}),
|
||||
[i18n]
|
||||
);
|
||||
|
||||
const confirm = useCallback(
|
||||
(config) => {
|
||||
return new Promise((resolve) => {
|
||||
setDialogConfig({ ...translatedDefaults, ...config });
|
||||
resolveRef.current = resolve;
|
||||
});
|
||||
},
|
||||
[translatedDefaults]
|
||||
);
|
||||
|
||||
const handleClose = () => {
|
||||
if (resolveRef.current) {
|
||||
resolveRef.current(false);
|
||||
}
|
||||
setDialogConfig(null);
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (resolveRef.current) {
|
||||
resolveRef.current(true);
|
||||
}
|
||||
setDialogConfig(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfirmContext.Provider value={confirm}>
|
||||
{children}
|
||||
|
||||
<Dialog
|
||||
open={!!dialogConfig}
|
||||
onClose={handleClose}
|
||||
aria-labelledby="confirm-dialog-title"
|
||||
aria-describedby="confirm-dialog-description"
|
||||
>
|
||||
{dialogConfig && (
|
||||
<>
|
||||
<DialogTitle id="confirm-dialog-title">
|
||||
{dialogConfig.title}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText id="confirm-dialog-description">
|
||||
{dialogConfig.message}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose}>{dialogConfig.cancelText}</Button>
|
||||
<Button onClick={handleConfirm} color="primary" autoFocus>
|
||||
{dialogConfig.confirmText}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</>
|
||||
)}
|
||||
</Dialog>
|
||||
</ConfirmContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useConfirm() {
|
||||
const context = useContext(ConfirmContext);
|
||||
if (!context) {
|
||||
throw new Error("useConfirm must be used within a ConfirmProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
17
src/hooks/DebouncedCallback.js
Normal file
17
src/hooks/DebouncedCallback.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useMemo, useEffect, useRef } from "react";
|
||||
import { debounce } from "../libs/utils";
|
||||
|
||||
export function useDebouncedCallback(callback, delay) {
|
||||
const callbackRef = useRef(callback);
|
||||
|
||||
useEffect(() => {
|
||||
callbackRef.current = callback;
|
||||
}, [callback]);
|
||||
|
||||
const debouncedCallback = useMemo(
|
||||
() => debounce((...args) => callbackRef.current(...args), delay),
|
||||
[delay]
|
||||
);
|
||||
|
||||
return debouncedCallback;
|
||||
}
|
||||
13
src/hooks/Fab.js
Normal file
13
src/hooks/Fab.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { STOKEY_FAB } from "../config";
|
||||
import { useStorage } from "./Storage";
|
||||
|
||||
const DEFAULT_FAB = {};
|
||||
|
||||
/**
|
||||
* fab hook
|
||||
* @returns
|
||||
*/
|
||||
export function useFab() {
|
||||
const { data, update } = useStorage(STOKEY_FAB, DEFAULT_FAB);
|
||||
return { fab: data, updateFab: update };
|
||||
}
|
||||
64
src/hooks/FavWords.js
Normal file
64
src/hooks/FavWords.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import { STOKEY_WORDS, KV_WORDS_KEY } from "../config";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useStorage } from "./Storage";
|
||||
import { debounceSyncMeta } from "../libs/storage";
|
||||
|
||||
const DEFAULT_FAVWORDS = {};
|
||||
|
||||
export function useFavWords() {
|
||||
const { data: favWords, save: saveWords } = useStorage(
|
||||
STOKEY_WORDS,
|
||||
DEFAULT_FAVWORDS,
|
||||
KV_WORDS_KEY
|
||||
);
|
||||
|
||||
const save = useCallback(
|
||||
(objOrFn) => {
|
||||
saveWords(objOrFn);
|
||||
debounceSyncMeta(KV_WORDS_KEY);
|
||||
},
|
||||
[saveWords]
|
||||
);
|
||||
|
||||
const toggleFav = useCallback(
|
||||
(word) => {
|
||||
save((prev) => {
|
||||
if (!prev[word]) {
|
||||
return { ...prev, [word]: { createdAt: Date.now() } };
|
||||
}
|
||||
|
||||
const favs = { ...prev };
|
||||
delete favs[word];
|
||||
return favs;
|
||||
});
|
||||
},
|
||||
[save]
|
||||
);
|
||||
|
||||
const mergeWords = useCallback(
|
||||
(words) => {
|
||||
save((prev) => ({
|
||||
...words.reduce((acc, key) => {
|
||||
acc[key] = { createdAt: Date.now() };
|
||||
return acc;
|
||||
}, {}),
|
||||
...prev,
|
||||
}));
|
||||
},
|
||||
[save]
|
||||
);
|
||||
|
||||
const clearWords = useCallback(() => {
|
||||
save({});
|
||||
}, [save]);
|
||||
|
||||
const favList = useMemo(
|
||||
() =>
|
||||
Object.entries(favWords || {}).sort((a, b) => a[0].localeCompare(b[0])),
|
||||
[favWords]
|
||||
);
|
||||
|
||||
const wordList = useMemo(() => favList.map(([word]) => word), [favList]);
|
||||
|
||||
return { favWords, favList, wordList, toggleFav, mergeWords, clearWords };
|
||||
}
|
||||
@@ -1,40 +1,152 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
|
||||
/**
|
||||
* fetch data hook
|
||||
* @returns
|
||||
*/
|
||||
export const useFetch = (url) => {
|
||||
export const useAsync = () => {
|
||||
const [data, setData] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!url) {
|
||||
const execute = useCallback(async (fn, ...args) => {
|
||||
if (!fn) {
|
||||
return;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) {
|
||||
throw new Error(`[${res.status}] ${res.statusText}`);
|
||||
}
|
||||
let data;
|
||||
if (res.headers.get("Content-Type")?.includes("json")) {
|
||||
data = await res.json();
|
||||
} else {
|
||||
data = await res.text();
|
||||
}
|
||||
setData(data);
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
})();
|
||||
}, [url]);
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
return [data, loading, error];
|
||||
try {
|
||||
const res = await fn(...args);
|
||||
setData(res);
|
||||
setLoading(false);
|
||||
return res;
|
||||
} catch (err) {
|
||||
setError(err?.message || "An unknown error occurred");
|
||||
setLoading(false);
|
||||
// throw err;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setData(null);
|
||||
setLoading(false);
|
||||
setError(null);
|
||||
}, []);
|
||||
|
||||
return { data, loading, error, execute, reset };
|
||||
};
|
||||
|
||||
export const useAsyncNow = (fn, arg) => {
|
||||
const { execute, ...asyncState } = useAsync();
|
||||
|
||||
useEffect(() => {
|
||||
if (fn) {
|
||||
execute(fn, arg);
|
||||
}
|
||||
}, [execute, fn, arg]);
|
||||
|
||||
return { ...asyncState };
|
||||
};
|
||||
|
||||
export const useFetch = () => {
|
||||
const { execute, ...asyncState } = useAsync();
|
||||
|
||||
const requester = useCallback(async (url, options) => {
|
||||
const response = await fetch(url, options);
|
||||
if (!response.ok) {
|
||||
const errorInfo = await response.text();
|
||||
throw new Error(
|
||||
`Request failed: ${response.status} ${response.statusText} - ${errorInfo}`
|
||||
);
|
||||
}
|
||||
if (response.status === 204) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (response.headers.get("Content-Type")?.includes("json")) {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
return response.text();
|
||||
}, []);
|
||||
|
||||
const get = useCallback(
|
||||
async (url, options = {}) => {
|
||||
try {
|
||||
const result = await execute(requester, url, {
|
||||
...options,
|
||||
method: "GET",
|
||||
});
|
||||
return result;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[execute, requester]
|
||||
);
|
||||
|
||||
const post = useCallback(
|
||||
async (url, body, options = {}) => {
|
||||
try {
|
||||
const result = await execute(requester, url, {
|
||||
...options,
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", ...options.headers },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
return result;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[execute, requester]
|
||||
);
|
||||
|
||||
const put = useCallback(
|
||||
async (url, body, options = {}) => {
|
||||
try {
|
||||
const result = await execute(requester, url, {
|
||||
...options,
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json", ...options.headers },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
return result;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[execute, requester]
|
||||
);
|
||||
|
||||
const del = useCallback(
|
||||
async (url, options = {}) => {
|
||||
try {
|
||||
const result = await execute(requester, url, {
|
||||
...options,
|
||||
method: "DELETE",
|
||||
});
|
||||
return result;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[execute, requester]
|
||||
);
|
||||
|
||||
return {
|
||||
...asyncState,
|
||||
get,
|
||||
post,
|
||||
put,
|
||||
del,
|
||||
};
|
||||
};
|
||||
|
||||
export const useGet = (url) => {
|
||||
const { get, ...fetchState } = useFetch();
|
||||
|
||||
useEffect(() => {
|
||||
if (url) get(url);
|
||||
}, [url, get]);
|
||||
|
||||
return { ...fetchState };
|
||||
};
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import { useSetting } from "./Setting";
|
||||
import { I18N, URL_RAW_PREFIX } from "../config";
|
||||
import { useFetch } from "./Fetch";
|
||||
import { useGet } 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
|
||||
@@ -10,12 +18,12 @@ export const useI18n = () => {
|
||||
const {
|
||||
setting: { uiLang },
|
||||
} = useSetting();
|
||||
return (key, defaultText = "") => I18N?.[key]?.[uiLang] ?? defaultText;
|
||||
return useLangMap(uiLang);
|
||||
};
|
||||
|
||||
export const useI18nMd = (key) => {
|
||||
const i18n = useI18n();
|
||||
const fileName = i18n(key);
|
||||
const url = fileName ? `${URL_RAW_PREFIX}/${fileName}` : "";
|
||||
return useFetch(url);
|
||||
return useGet(url);
|
||||
};
|
||||
|
||||
10
src/hooks/InputRule.js
Normal file
10
src/hooks/InputRule.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { DEFAULT_INPUT_RULE } from "../config";
|
||||
import { useSetting } from "./Setting";
|
||||
|
||||
export function useInputRule() {
|
||||
const { setting, updateChild } = useSetting();
|
||||
const inputRule = setting?.inputRule || DEFAULT_INPUT_RULE;
|
||||
const updateInputRule = updateChild("inputRule");
|
||||
|
||||
return { inputRule, updateInputRule };
|
||||
}
|
||||
16
src/hooks/Loading.js
Normal file
16
src/hooks/Loading.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import Link from "@mui/material/Link";
|
||||
import Divider from "@mui/material/Divider";
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<center>
|
||||
<Divider>
|
||||
<Link
|
||||
href={process.env.REACT_APP_HOMEPAGE}
|
||||
>{`KISS Translator v${process.env.REACT_APP_VERSION}`}</Link>
|
||||
</Divider>
|
||||
<CircularProgress />
|
||||
</center>
|
||||
);
|
||||
}
|
||||
11
src/hooks/MouseHover.js
Normal file
11
src/hooks/MouseHover.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { DEFAULT_MOUSE_HOVER_SETTING } from "../config";
|
||||
import { useSetting } from "./Setting";
|
||||
|
||||
export function useMouseHoverSetting() {
|
||||
const { setting, updateChild } = useSetting();
|
||||
const mouseHoverSetting =
|
||||
setting?.mouseHoverSetting || DEFAULT_MOUSE_HOVER_SETTING;
|
||||
const updateMouseHoverSetting = updateChild("mouseHoverSetting");
|
||||
|
||||
return { mouseHoverSetting, updateMouseHoverSetting };
|
||||
}
|
||||
@@ -1,87 +1,104 @@
|
||||
import { STOKEY_RULES, DEFAULT_RULES } from "../config";
|
||||
import { STOKEY_RULES, DEFAULT_RULES, KV_RULES_KEY } from "../config";
|
||||
import { useStorage } from "./Storage";
|
||||
import { trySyncRules } from "../libs/sync";
|
||||
import { checkRules } from "../libs/rules";
|
||||
import { useCallback } from "react";
|
||||
import { debounceSyncMeta } from "../libs/storage";
|
||||
|
||||
/**
|
||||
* 规则 hook
|
||||
* @returns
|
||||
*/
|
||||
export function useRules() {
|
||||
const { data: list, save } = useStorage(STOKEY_RULES, DEFAULT_RULES);
|
||||
const { data: list = [], save: saveRules } = useStorage(
|
||||
STOKEY_RULES,
|
||||
DEFAULT_RULES,
|
||||
KV_RULES_KEY
|
||||
);
|
||||
|
||||
const updateRules = useCallback(
|
||||
async (rules) => {
|
||||
await save(rules);
|
||||
trySyncRules();
|
||||
const save = useCallback(
|
||||
(objOrFn) => {
|
||||
saveRules(objOrFn);
|
||||
debounceSyncMeta(KV_RULES_KEY);
|
||||
},
|
||||
[saveRules]
|
||||
);
|
||||
|
||||
const add = useCallback(
|
||||
(rule) => {
|
||||
save((prev) => {
|
||||
if (
|
||||
rule.pattern === "*" ||
|
||||
prev.some((item) => item.pattern === rule.pattern)
|
||||
) {
|
||||
return prev;
|
||||
}
|
||||
return [rule, ...prev];
|
||||
});
|
||||
},
|
||||
[save]
|
||||
);
|
||||
|
||||
const add = useCallback(
|
||||
async (rule) => {
|
||||
const rules = [...list];
|
||||
if (rule.pattern === "*") {
|
||||
return;
|
||||
}
|
||||
if (rules.map((item) => item.pattern).includes(rule.pattern)) {
|
||||
return;
|
||||
}
|
||||
rules.unshift(rule);
|
||||
await updateRules(rules);
|
||||
},
|
||||
[list, updateRules]
|
||||
);
|
||||
|
||||
const del = useCallback(
|
||||
async (pattern) => {
|
||||
let rules = [...list];
|
||||
if (pattern === "*") {
|
||||
return;
|
||||
}
|
||||
rules = rules.filter((item) => item.pattern !== pattern);
|
||||
await updateRules(rules);
|
||||
(pattern) => {
|
||||
save((prev) => {
|
||||
if (pattern === "*") {
|
||||
return prev;
|
||||
}
|
||||
return prev.filter((item) => item.pattern !== pattern);
|
||||
});
|
||||
},
|
||||
[list, updateRules]
|
||||
[save]
|
||||
);
|
||||
|
||||
const clear = useCallback(async () => {
|
||||
let rules = [...list];
|
||||
rules = rules.filter((item) => item.pattern === "*");
|
||||
await updateRules(rules);
|
||||
}, [list, updateRules]);
|
||||
const clear = useCallback(() => {
|
||||
save((prev) => prev.filter((item) => item.pattern === "*"));
|
||||
}, [save]);
|
||||
|
||||
const put = useCallback(
|
||||
async (pattern, obj) => {
|
||||
const rules = [...list];
|
||||
if (pattern === "*") {
|
||||
obj.pattern = "*";
|
||||
}
|
||||
const rule = rules.find((r) => r.pattern === pattern);
|
||||
rule && Object.assign(rule, obj);
|
||||
await updateRules(rules);
|
||||
(pattern, obj) => {
|
||||
save((prev) => {
|
||||
// if (pattern !== obj.pattern) {
|
||||
// return prev;
|
||||
// }
|
||||
return prev.map((item) =>
|
||||
item.pattern === pattern ? { ...item, ...obj } : item
|
||||
);
|
||||
});
|
||||
},
|
||||
[list, updateRules]
|
||||
[save]
|
||||
);
|
||||
|
||||
const merge = useCallback(
|
||||
async (newRules) => {
|
||||
const rules = [...list];
|
||||
newRules = checkRules(newRules);
|
||||
newRules.forEach((newRule) => {
|
||||
const rule = rules.find(
|
||||
(oldRule) => oldRule.pattern === newRule.pattern
|
||||
);
|
||||
if (rule) {
|
||||
Object.assign(rule, newRule);
|
||||
} else {
|
||||
rules.unshift(newRule);
|
||||
(rules) => {
|
||||
save((prev) => {
|
||||
const adds = checkRules(rules);
|
||||
if (adds.length === 0) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
// const map = new Map();
|
||||
// // 不进行深度合并
|
||||
// // [...prev, ...adds].forEach((item) => {
|
||||
// // const k = item.pattern;
|
||||
// // map.set(k, { ...(map.get(k) || {}), ...item });
|
||||
// // });
|
||||
// prev.forEach((item) => map.set(item.pattern, item));
|
||||
// adds.forEach((item) => map.set(item.pattern, item));
|
||||
// return [...map.values()];
|
||||
|
||||
const addsMap = new Map(adds.map((item) => [item.pattern, item]));
|
||||
const prevPatterns = new Set(prev.map((item) => item.pattern));
|
||||
const updatedPrev = prev.map(
|
||||
(prevItem) => addsMap.get(prevItem.pattern) || prevItem
|
||||
);
|
||||
const newItems = adds.filter(
|
||||
(addItem) => !prevPatterns.has(addItem.pattern)
|
||||
);
|
||||
|
||||
return [...newItems, ...updatedPrev];
|
||||
});
|
||||
await updateRules(rules);
|
||||
},
|
||||
[list, updateRules]
|
||||
[save]
|
||||
);
|
||||
|
||||
return { list, add, del, clear, put, merge };
|
||||
|
||||
@@ -1,51 +1,106 @@
|
||||
import { STOKEY_SETTING, DEFAULT_SETTING } from "../config";
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
useEffect,
|
||||
} from "react";
|
||||
import Alert from "@mui/material/Alert";
|
||||
import {
|
||||
STOKEY_SETTING,
|
||||
DEFAULT_SETTING,
|
||||
KV_SETTING_KEY,
|
||||
MSG_SET_LOGLEVEL,
|
||||
} from "../config";
|
||||
import { useStorage } from "./Storage";
|
||||
import { trySyncSetting } from "../libs/sync";
|
||||
import { createContext, useCallback, useContext, useMemo } from "react";
|
||||
import { debounce } from "../libs/utils";
|
||||
import { debounceSyncMeta } from "../libs/storage";
|
||||
import Loading from "./Loading";
|
||||
import { logger } from "../libs/log";
|
||||
import { sendBgMsg } from "../libs/msg";
|
||||
import { isExt } from "../libs/client";
|
||||
|
||||
const SettingContext = createContext({
|
||||
setting: {},
|
||||
updateSetting: async () => {},
|
||||
reloadSetting: async () => {},
|
||||
setting: DEFAULT_SETTING,
|
||||
updateSetting: () => {},
|
||||
reloadSetting: () => {},
|
||||
});
|
||||
|
||||
export function SettingProvider({ children }) {
|
||||
const { data, update, reload, loading } = useStorage(
|
||||
STOKEY_SETTING,
|
||||
DEFAULT_SETTING
|
||||
);
|
||||
const {
|
||||
data: setting,
|
||||
isLoading,
|
||||
update,
|
||||
reload,
|
||||
} = useStorage(STOKEY_SETTING, DEFAULT_SETTING, KV_SETTING_KEY);
|
||||
|
||||
const syncSetting = useMemo(
|
||||
() =>
|
||||
debounce(() => {
|
||||
trySyncSetting();
|
||||
}, [2000]),
|
||||
[]
|
||||
);
|
||||
useEffect(() => {
|
||||
if (typeof setting?.darkMode === "boolean") {
|
||||
update((currentSetting) => ({
|
||||
...currentSetting,
|
||||
darkMode: currentSetting.darkMode ? "dark" : "light",
|
||||
}));
|
||||
}
|
||||
}, [setting?.darkMode, update]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
logger.setLevel(setting?.logLevel);
|
||||
if (isExt) {
|
||||
await sendBgMsg(MSG_SET_LOGLEVEL, setting?.logLevel);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Failed to fetch log level, using default.", error);
|
||||
}
|
||||
})();
|
||||
}, [setting]);
|
||||
|
||||
const updateSetting = useCallback(
|
||||
async (obj) => {
|
||||
await update(obj);
|
||||
syncSetting();
|
||||
(objOrFn) => {
|
||||
update(objOrFn);
|
||||
debounceSyncMeta(KV_SETTING_KEY);
|
||||
},
|
||||
[update, syncSetting]
|
||||
[update]
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return;
|
||||
const updateChild = useCallback(
|
||||
(key) => async (obj) => {
|
||||
updateSetting((prev) => ({
|
||||
...prev,
|
||||
[key]: { ...(prev?.[key] || {}), ...obj },
|
||||
}));
|
||||
},
|
||||
[updateSetting]
|
||||
);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
setting,
|
||||
updateSetting,
|
||||
updateChild,
|
||||
reloadSetting: reload,
|
||||
}),
|
||||
[setting, updateSetting, updateChild, reload]
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (!setting) {
|
||||
<center>
|
||||
<Alert severity="error" sx={{ maxWidth: 600, margin: "60px auto" }}>
|
||||
<p>数据加载出错,请刷新页面或卸载后重新安装。</p>
|
||||
<p>
|
||||
Data loading error, please refresh the page or uninstall and
|
||||
reinstall.
|
||||
</p>
|
||||
</Alert>
|
||||
</center>;
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingContext.Provider
|
||||
value={{
|
||||
setting: data,
|
||||
updateSetting,
|
||||
reloadSetting: reload,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</SettingContext.Provider>
|
||||
<SettingContext.Provider value={value}>{children}</SettingContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,13 +6,14 @@ 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 });
|
||||
(val) => {
|
||||
updateSetting((prev) => ({
|
||||
...prev,
|
||||
shortcuts: { ...(prev?.shortcuts || {}), [action]: val },
|
||||
}));
|
||||
},
|
||||
[action, shortcuts, updateSetting]
|
||||
[action, updateSetting]
|
||||
);
|
||||
|
||||
return { shortcut, setShortcut };
|
||||
|
||||
@@ -1,52 +1,144 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { storage } from "../libs/storage";
|
||||
import { kissLog } from "../libs/log";
|
||||
import { syncData } from "../libs/sync";
|
||||
import { useDebouncedCallback } from "./DebouncedCallback";
|
||||
|
||||
export function useStorage(key, defaultVal = null) {
|
||||
const [loading, setLoading] = useState(true);
|
||||
/**
|
||||
* 用于将组件状态与 Storage 同步
|
||||
*
|
||||
* @param {string} key 用于在 Storage 中存取值的键
|
||||
* @param {*} defaultVal 默认值。建议在组件外定义为常量。
|
||||
* @param {string} [syncKey=""] 用于远端同步的可选键名
|
||||
* @returns {{
|
||||
* data: *,
|
||||
* save: (valueOrFn: any | ((prevData: any) => any)) => void,
|
||||
* update: (partialDataOrFn: object | ((prevData: object) => object)) => void,
|
||||
* remove: () => Promise<void>,
|
||||
* reload: () => Promise<void>
|
||||
* }}
|
||||
*/
|
||||
export function useStorage(key, defaultVal = null, syncKey = "") {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [data, setData] = useState(defaultVal);
|
||||
|
||||
const save = useCallback(
|
||||
async (val) => {
|
||||
setData(val);
|
||||
await storage.setObj(key, val);
|
||||
},
|
||||
[key]
|
||||
);
|
||||
// 首次加载数据
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
const update = useCallback(
|
||||
async (obj) => {
|
||||
setData((pre) => ({ ...pre, ...obj }));
|
||||
await storage.putObj(key, obj);
|
||||
},
|
||||
[key]
|
||||
);
|
||||
const loadInitialData = async () => {
|
||||
try {
|
||||
const storedVal = await storage.getObj(key);
|
||||
if (storedVal === undefined || storedVal === null) {
|
||||
await storage.setObj(key, defaultVal);
|
||||
} else if (isMounted) {
|
||||
setData(storedVal);
|
||||
}
|
||||
} catch (err) {
|
||||
kissLog(`storage load error for key: ${key}`, err);
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadInitialData();
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, [key, defaultVal]);
|
||||
|
||||
// 远端同步
|
||||
const runSync = useCallback(async (keyToSync, valueToSync) => {
|
||||
try {
|
||||
const res = await syncData(keyToSync, valueToSync);
|
||||
if (res?.isNew) {
|
||||
setData(res.value);
|
||||
}
|
||||
} catch (error) {
|
||||
kissLog("Sync failed", keyToSync);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const debouncedSync = useDebouncedCallback(runSync, 3000);
|
||||
|
||||
// 持久化
|
||||
useEffect(() => {
|
||||
if (isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
storage.setObj(key, data).catch((err) => {
|
||||
kissLog(`storage save error for key: ${key}`, err);
|
||||
});
|
||||
|
||||
// 触发远端同步
|
||||
if (syncKey) {
|
||||
debouncedSync(syncKey, data);
|
||||
}
|
||||
}, [key, syncKey, isLoading, data, debouncedSync]);
|
||||
|
||||
/**
|
||||
* 全量替换状态值
|
||||
* @param {any | ((prevData: any) => any)} valueOrFn 新的值或一个返回新值的函数。
|
||||
*/
|
||||
const save = useCallback((valueOrFn) => {
|
||||
// kissLog("save storage:", valueOrFn);
|
||||
setData((prevData) =>
|
||||
typeof valueOrFn === "function" ? valueOrFn(prevData) : valueOrFn
|
||||
);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 合并对象到当前状态(假设状态是一个对象)。
|
||||
* @param {object | ((prevData: object) => object)} partialDataOrFn 要合并的对象或一个返回该对象的函数。
|
||||
*/
|
||||
const update = useCallback((partialDataOrFn) => {
|
||||
// kissLog("update storage:", partialDataOrFn);
|
||||
setData((prevData) => {
|
||||
const partialData =
|
||||
typeof partialDataOrFn === "function"
|
||||
? partialDataOrFn(prevData)
|
||||
: partialDataOrFn;
|
||||
// 确保 preData 是一个对象,避免展开 null 或 undefined
|
||||
const baseObj =
|
||||
typeof prevData === "object" && prevData !== null ? prevData : {};
|
||||
return { ...baseObj, ...partialData };
|
||||
});
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 从 Storage 中删除该值,并将状态重置为 null。
|
||||
*/
|
||||
const remove = useCallback(async () => {
|
||||
setData(null);
|
||||
await storage.del(key);
|
||||
// kissLog("remove storage:");
|
||||
try {
|
||||
await storage.del(key);
|
||||
setData(null);
|
||||
} catch (err) {
|
||||
kissLog(`storage remove error for key: ${key}`, err);
|
||||
}
|
||||
}, [key]);
|
||||
|
||||
/**
|
||||
* 从 Storage 重新加载数据以覆盖当前状态。
|
||||
*/
|
||||
const reload = useCallback(async () => {
|
||||
const val = await storage.getObj(key);
|
||||
if (val) {
|
||||
setData(val);
|
||||
} else if (defaultVal) {
|
||||
await storage.setObj(key, defaultVal);
|
||||
// kissLog("reload storage:");
|
||||
try {
|
||||
const storedVal = await storage.getObj(key);
|
||||
setData(storedVal ?? defaultVal);
|
||||
} catch (err) {
|
||||
kissLog(`storage reload error for key: ${key}`, err);
|
||||
// setData(defaultVal);
|
||||
}
|
||||
}, [key, defaultVal]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
await reload();
|
||||
} catch (err) {
|
||||
//
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
})();
|
||||
}, [reload]);
|
||||
|
||||
return { data, save, update, remove, reload, loading };
|
||||
return { data, save, update, remove, reload, isLoading };
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ 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";
|
||||
|
||||
/**
|
||||
* 订阅规则
|
||||
@@ -18,50 +18,36 @@ export function useSubRules() {
|
||||
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 });
|
||||
(url) => {
|
||||
updateSetting((prev) => ({
|
||||
...prev,
|
||||
subrulesList: prev.subrulesList.map((item) => ({
|
||||
...item,
|
||||
selected: item.url === url,
|
||||
})),
|
||||
}));
|
||||
},
|
||||
[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]
|
||||
[updateSetting]
|
||||
);
|
||||
|
||||
const addSub = useCallback(
|
||||
async (url) => {
|
||||
const subrulesList = [...list];
|
||||
subrulesList.push({ url, selected: false, syncAt: Date.now() });
|
||||
await updateSetting({ subrulesList });
|
||||
(url) => {
|
||||
updateSetting((prev) => ({
|
||||
...prev,
|
||||
subrulesList: [...prev.subrulesList, { url, selected: false }],
|
||||
}));
|
||||
},
|
||||
[list, updateSetting]
|
||||
[updateSetting]
|
||||
);
|
||||
|
||||
const delSub = useCallback(
|
||||
async (url) => {
|
||||
let subrulesList = [...list];
|
||||
subrulesList = subrulesList.filter((item) => item.url !== url);
|
||||
await updateSetting({ subrulesList });
|
||||
await delSubRules(url);
|
||||
(url) => {
|
||||
updateSetting((prev) => ({
|
||||
...prev,
|
||||
subrulesList: prev.subrulesList.filter((item) => item.url !== url),
|
||||
}));
|
||||
},
|
||||
[list, updateSetting]
|
||||
[updateSetting]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -72,7 +58,7 @@ export function useSubRules() {
|
||||
const rules = await loadOrFetchSubRules(selectedUrl);
|
||||
setSelectedRules(rules);
|
||||
} catch (err) {
|
||||
console.log("[loadOrFetchSubRules]", err);
|
||||
kissLog("loadOrFetchSubRules", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -83,7 +69,6 @@ export function useSubRules() {
|
||||
return {
|
||||
subList: list,
|
||||
selectSub,
|
||||
updateSub,
|
||||
addSub,
|
||||
delSub,
|
||||
selectedSub,
|
||||
@@ -99,15 +84,9 @@ export function useSubRules() {
|
||||
* @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]
|
||||
);
|
||||
const { setting, updateChild } = useSetting();
|
||||
const owSubrule = setting?.owSubrule || DEFAULT_OW_RULE;
|
||||
const updateOwSubrule = updateChild("owSubrule");
|
||||
|
||||
return { owSubrule, updateOwSubrule };
|
||||
}
|
||||
|
||||
10
src/hooks/Subtitle.js
Normal file
10
src/hooks/Subtitle.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { DEFAULT_SUBTITLE_SETTING } from "../config";
|
||||
import { useSetting } from "./Setting";
|
||||
|
||||
export function useSubtitle() {
|
||||
const { setting, updateChild } = useSetting();
|
||||
const subtitleSetting = setting?.subtitleSetting || DEFAULT_SUBTITLE_SETTING;
|
||||
const updateSubtitle = updateChild("subtitleSetting");
|
||||
|
||||
return { subtitleSetting, updateSubtitle };
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { STOKEY_SYNC, DEFAULT_SYNC } from "../config";
|
||||
import { useStorage } from "./Storage";
|
||||
|
||||
@@ -6,6 +7,73 @@ import { useStorage } from "./Storage";
|
||||
* @returns
|
||||
*/
|
||||
export function useSync() {
|
||||
const { data, update } = useStorage(STOKEY_SYNC, DEFAULT_SYNC);
|
||||
return { sync: data, updateSync: update };
|
||||
const { data, update, reload } = useStorage(STOKEY_SYNC, DEFAULT_SYNC);
|
||||
return { sync: data, updateSync: update, reloadSync: reload };
|
||||
}
|
||||
|
||||
/**
|
||||
* update syncmeta hook
|
||||
* @returns
|
||||
*/
|
||||
export function useSyncMeta() {
|
||||
const { updateSync } = useSync();
|
||||
|
||||
const updateSyncMeta = useCallback(
|
||||
(key) => {
|
||||
updateSync((prevSync) => {
|
||||
const newSyncMeta = {
|
||||
...(prevSync?.syncMeta || {}),
|
||||
[key]: {
|
||||
...(prevSync?.syncMeta?.[key] || {}),
|
||||
updateAt: Date.now(),
|
||||
},
|
||||
};
|
||||
return { syncMeta: newSyncMeta };
|
||||
});
|
||||
},
|
||||
[updateSync]
|
||||
);
|
||||
|
||||
return { updateSyncMeta };
|
||||
}
|
||||
|
||||
/**
|
||||
* caches sync hook
|
||||
* @param {*} url
|
||||
* @returns
|
||||
*/
|
||||
export function useSyncCaches() {
|
||||
const { sync, updateSync, reloadSync } = useSync();
|
||||
|
||||
const updateDataCache = useCallback(
|
||||
(url) => {
|
||||
updateSync((prevSync) => ({
|
||||
dataCaches: {
|
||||
...(prevSync?.dataCaches || {}),
|
||||
[url]: Date.now(),
|
||||
},
|
||||
}));
|
||||
},
|
||||
[updateSync]
|
||||
);
|
||||
|
||||
const deleteDataCache = useCallback(
|
||||
(url) => {
|
||||
updateSync((prevSync) => {
|
||||
const newDataCaches = { ...(prevSync?.dataCaches || {}) };
|
||||
delete newDataCaches[url];
|
||||
return { dataCaches: newDataCaches };
|
||||
});
|
||||
},
|
||||
[updateSync]
|
||||
);
|
||||
|
||||
const dataCaches = useMemo(() => sync?.dataCaches || {}, [sync?.dataCaches]);
|
||||
|
||||
return {
|
||||
dataCaches,
|
||||
updateDataCache,
|
||||
deleteDataCache,
|
||||
reloadSync,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { ThemeProvider, createTheme } from "@mui/material/styles";
|
||||
import CssBaseline from "@mui/material/CssBaseline";
|
||||
import { CssBaseline, GlobalStyles } from "@mui/material";
|
||||
import { useDarkMode } from "./ColorMode";
|
||||
import { THEME_DARK, THEME_LIGHT } from "../config";
|
||||
|
||||
@@ -9,21 +9,54 @@ import { THEME_DARK, THEME_LIGHT } from "../config";
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
export default function Theme({ children, options }) {
|
||||
export default function Theme({ children, options, styles }) {
|
||||
const { darkMode } = useDarkMode();
|
||||
const [systemMode, setSystemMode] = useState(THEME_LIGHT);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window.matchMedia !== "function") {
|
||||
return;
|
||||
}
|
||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
const handleChange = () => {
|
||||
setSystemMode(mediaQuery.matches ? THEME_DARK : THEME_LIGHT);
|
||||
};
|
||||
handleChange(); // Set initial value
|
||||
mediaQuery.addEventListener("change", handleChange);
|
||||
return () => mediaQuery.removeEventListener("change", handleChange);
|
||||
}, []);
|
||||
|
||||
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) {
|
||||
//
|
||||
}
|
||||
|
||||
const isDarkMode =
|
||||
darkMode === "dark" || (darkMode === "auto" && systemMode === THEME_DARK);
|
||||
|
||||
return createTheme({
|
||||
palette: {
|
||||
mode: darkMode ? THEME_DARK : THEME_LIGHT,
|
||||
mode: isDarkMode ? THEME_DARK : THEME_LIGHT,
|
||||
},
|
||||
typography: {
|
||||
htmlFontSize,
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}, [darkMode, options]);
|
||||
}, [darkMode, options, systemMode]);
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
|
||||
<CssBaseline />
|
||||
<GlobalStyles styles={styles} />
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
10
src/hooks/Tranbox.js
Normal file
10
src/hooks/Tranbox.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { DEFAULT_TRANBOX_SETTING } from "../config";
|
||||
import { useSetting } from "./Setting";
|
||||
|
||||
export function useTranbox() {
|
||||
const { setting, updateChild } = useSetting();
|
||||
const tranboxSetting = setting?.tranboxSetting || DEFAULT_TRANBOX_SETTING;
|
||||
const updateTranbox = updateChild("tranboxSetting");
|
||||
|
||||
return { tranboxSetting, updateTranbox };
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import { useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
import { tryDetectLang } from "../libs";
|
||||
import { apiTranslate } from "../apis";
|
||||
import { DEFAULT_TRANS_APIS } from "../config";
|
||||
|
||||
/**
|
||||
* 翻译hook
|
||||
* @param {*} q
|
||||
* @param {*} rule
|
||||
* @param {*} setting
|
||||
* @returns
|
||||
*/
|
||||
export function useTranslate(q, rule, setting) {
|
||||
const [text, setText] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [sameLang, setSamelang] = useState(false);
|
||||
|
||||
const { translator, fromLang, toLang } = rule;
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const deLang = await tryDetectLang(q);
|
||||
if (deLang && toLang.includes(deLang)) {
|
||||
setSamelang(true);
|
||||
} else {
|
||||
const [trText, isSame] = await apiTranslate({
|
||||
translator,
|
||||
text: q,
|
||||
fromLang,
|
||||
toLang,
|
||||
apiSetting: (setting.transApis || DEFAULT_TRANS_APIS)[translator],
|
||||
});
|
||||
setText(trText);
|
||||
setSamelang(isSame);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("[translate]", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
})();
|
||||
}, [q, translator, fromLang, toLang, setting]);
|
||||
|
||||
return { text, sameLang, loading };
|
||||
}
|
||||
61
src/hooks/ValidationInput.js
Normal file
61
src/hooks/ValidationInput.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import { limitNumber, limitFloat } from "../libs/utils";
|
||||
|
||||
function ValidationInput({
|
||||
value,
|
||||
onChange,
|
||||
name,
|
||||
min,
|
||||
max,
|
||||
isFloat = false,
|
||||
...props
|
||||
}) {
|
||||
const [localValue, setLocalValue] = useState(value);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalValue(value);
|
||||
}, [value]);
|
||||
|
||||
const handleLocalChange = (e) => {
|
||||
setLocalValue(e.target.value);
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
const numValue = Number(localValue);
|
||||
|
||||
if (isNaN(numValue)) {
|
||||
setLocalValue(value);
|
||||
return;
|
||||
}
|
||||
|
||||
const validatedValue = isFloat
|
||||
? limitFloat(numValue, min, max)
|
||||
: limitNumber(numValue, min, max);
|
||||
|
||||
if (validatedValue !== numValue) {
|
||||
setLocalValue(validatedValue);
|
||||
}
|
||||
|
||||
onChange({
|
||||
target: {
|
||||
name: name,
|
||||
value: validatedValue,
|
||||
},
|
||||
preventDefault: () => {},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<TextField
|
||||
{...props}
|
||||
type="number"
|
||||
name={name}
|
||||
value={localValue}
|
||||
onChange={handleLocalChange}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default ValidationInput;
|
||||
29
src/hooks/WindowSize.js
Normal file
29
src/hooks/WindowSize.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useDebouncedCallback } from "./DebouncedCallback";
|
||||
|
||||
function useWindowSize() {
|
||||
const [windowSize, setWindowSize] = useState({
|
||||
w: window.innerWidth,
|
||||
h: window.innerHeight,
|
||||
});
|
||||
|
||||
const debounceWindowResize = useDebouncedCallback(() => {
|
||||
setWindowSize({
|
||||
w: window.innerWidth,
|
||||
h: window.innerHeight,
|
||||
});
|
||||
}, 200);
|
||||
|
||||
useEffect(() => {
|
||||
debounceWindowResize();
|
||||
|
||||
window.addEventListener("resize", debounceWindowResize);
|
||||
return () => {
|
||||
window.removeEventListener("resize", debounceWindowResize);
|
||||
};
|
||||
}, [debounceWindowResize]);
|
||||
|
||||
return windowSize;
|
||||
}
|
||||
|
||||
export default useWindowSize;
|
||||
26
src/index.js
26
src/index.js
@@ -7,14 +7,15 @@ 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 { useGet } from "./hooks/Fetch";
|
||||
import { I18N, URL_RAW_PREFIX } from "./config";
|
||||
|
||||
function App() {
|
||||
const [lang, setLang] = useState("zh");
|
||||
const [data, loading, error] = useFetch(
|
||||
const { data, loading, error } = useGet(
|
||||
`${URL_RAW_PREFIX}/${I18N?.["about_md"]?.[lang]}`
|
||||
);
|
||||
|
||||
return (
|
||||
<Paper sx={{ padding: 2, margin: 2 }}>
|
||||
<Stack spacing={2} direction="row" justifyContent="flex-end">
|
||||
@@ -32,25 +33,14 @@ function App() {
|
||||
href={process.env.REACT_APP_HOMEPAGE}
|
||||
>{`KISS Translator v${process.env.REACT_APP_VERSION}`}</Link>
|
||||
</Divider>
|
||||
<Stack spacing={2} direction="row" useFlexGap flexWrap="wrap">
|
||||
<Stack spacing={2}>
|
||||
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}>
|
||||
Install Userscript 1
|
||||
</Link>
|
||||
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL2}>
|
||||
Install Userscript 2
|
||||
Install/Update Userscript for Tampermonkey/Violentmonkey
|
||||
</Link>
|
||||
<Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL}>
|
||||
Install Userscript Safari 1
|
||||
</Link>
|
||||
<Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL2}>
|
||||
Install Userscript Safari 2
|
||||
</Link>
|
||||
<Link href={process.env.REACT_APP_OPTIONSPAGE}>
|
||||
Open Options Page 1
|
||||
</Link>
|
||||
<Link href={process.env.REACT_APP_OPTIONSPAGE2}>
|
||||
Open Options Page 2
|
||||
Install/Update Userscript for iOS Safari
|
||||
</Link>
|
||||
<Link href={process.env.REACT_APP_OPTIONSPAGE}>Open Options Page</Link>
|
||||
</Stack>
|
||||
|
||||
{loading ? (
|
||||
@@ -58,7 +48,7 @@ function App() {
|
||||
<CircularProgress />
|
||||
</center>
|
||||
) : (
|
||||
<ReactMarkdown children={error ? error.message : data} />
|
||||
<ReactMarkdown children={error || data} />
|
||||
)}
|
||||
</Paper>
|
||||
);
|
||||
|
||||
3
src/injector-shadowroot.js
Normal file
3
src/injector-shadowroot.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import { shadowRootInjector } from "./injectors/shadowroot";
|
||||
|
||||
shadowRootInjector();
|
||||
3
src/injector-subtitle.js
Normal file
3
src/injector-subtitle.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import { XMLHttpRequestInjector } from "./injectors/xmlhttp";
|
||||
|
||||
XMLHttpRequestInjector();
|
||||
27
src/injectors/index.js
Normal file
27
src/injectors/index.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import { browser } from "../libs/browser";
|
||||
import { isExt } from "../libs/client";
|
||||
import { injectExternalJs, injectInlineJs } from "../libs/injector";
|
||||
import { shadowRootInjector } from "./shadowroot";
|
||||
import { XMLHttpRequestInjector } from "./xmlhttp";
|
||||
|
||||
export const INJECTOR = {
|
||||
subtitle: "injector-subtitle.js",
|
||||
shadowroot: "injector-shadowroot.js",
|
||||
};
|
||||
|
||||
const injectorMap = {
|
||||
[INJECTOR.subtitle]: XMLHttpRequestInjector,
|
||||
[INJECTOR.shadowroot]: shadowRootInjector,
|
||||
};
|
||||
|
||||
export function injectJs(name, id = "kiss-translator-inject-js") {
|
||||
const injector = injectorMap[name];
|
||||
if (!injector) return;
|
||||
|
||||
if (isExt) {
|
||||
const src = browser.runtime.getURL(name);
|
||||
injectExternalJs(src, id);
|
||||
} else {
|
||||
injectInlineJs(`(${injector})()`, id);
|
||||
}
|
||||
}
|
||||
12
src/injectors/shadowroot.js
Normal file
12
src/injectors/shadowroot.js
Normal file
@@ -0,0 +1,12 @@
|
||||
export const shadowRootInjector = () => {
|
||||
try {
|
||||
const orig = Element.prototype.attachShadow;
|
||||
Element.prototype.attachShadow = function (...args) {
|
||||
const root = orig.apply(this, args);
|
||||
window.postMessage({ type: "KISS_SHADOW_ROOT_CREATED" }, "*");
|
||||
return root;
|
||||
};
|
||||
} catch (err) {
|
||||
console.log("shadowRootInjector", err);
|
||||
}
|
||||
};
|
||||
23
src/injectors/xmlhttp.js
Normal file
23
src/injectors/xmlhttp.js
Normal file
@@ -0,0 +1,23 @@
|
||||
export const XMLHttpRequestInjector = () => {
|
||||
try {
|
||||
const originalOpen = XMLHttpRequest.prototype.open;
|
||||
XMLHttpRequest.prototype.open = function (...args) {
|
||||
const url = args[1];
|
||||
if (typeof url === "string" && url.includes("timedtext")) {
|
||||
this.addEventListener("load", function () {
|
||||
window.postMessage(
|
||||
{
|
||||
type: "KISS_XHR_DATA_YOUTUBE",
|
||||
url: this.responseURL,
|
||||
response: this.responseText,
|
||||
},
|
||||
window.location.origin
|
||||
);
|
||||
});
|
||||
}
|
||||
return originalOpen.apply(this, args);
|
||||
};
|
||||
} catch (err) {
|
||||
console.log("XMLHttpRequestInjector", err);
|
||||
}
|
||||
};
|
||||
@@ -1,12 +1,12 @@
|
||||
import { getMsauth, setMsauth } from "./storage";
|
||||
import { URL_MICROSOFT_AUTH } from "../config";
|
||||
import { fetchData } from "./fetch";
|
||||
import { kissLog } from "./log";
|
||||
import { apiMsAuth } from "../apis";
|
||||
|
||||
const parseMSToken = (token) => {
|
||||
try {
|
||||
return JSON.parse(atob(token.split(".")[1])).exp;
|
||||
} catch (err) {
|
||||
console.log("[parseMSToken]", err);
|
||||
kissLog("parseMSToken", err);
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
@@ -16,28 +16,55 @@ const parseMSToken = (token) => {
|
||||
* @returns
|
||||
*/
|
||||
const _msAuth = () => {
|
||||
let { token, exp } = {};
|
||||
let tokenPromise = null;
|
||||
const EXPIRATION_MS = 1000;
|
||||
|
||||
const fetchNewToken = async () => {
|
||||
try {
|
||||
const now = Date.now();
|
||||
|
||||
// 1. 查询storage缓存
|
||||
const storageToken = await getMsauth();
|
||||
if (storageToken) {
|
||||
const storageExp = parseMSToken(storageToken);
|
||||
const storageExpiresAt = storageExp * 1000;
|
||||
if (storageExpiresAt > now + EXPIRATION_MS) {
|
||||
return { token: storageToken, expiresAt: storageExpiresAt };
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 缓存没有或失效,查询接口
|
||||
const apiToken = await apiMsAuth();
|
||||
if (!apiToken) {
|
||||
throw new Error("Failed to fetch ms token");
|
||||
}
|
||||
|
||||
const apiExp = parseMSToken(apiToken);
|
||||
const apiExpiresAt = apiExp * 1000;
|
||||
await setMsauth(apiToken);
|
||||
return { token: apiToken, expiresAt: apiExpiresAt };
|
||||
} catch (error) {
|
||||
kissLog("get msauth failed", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
return async () => {
|
||||
// 查询内存缓存
|
||||
const now = Date.now();
|
||||
if (token && exp * 1000 > now + 1000) {
|
||||
return [token, exp];
|
||||
// 检查是否有缓存的 Promise
|
||||
if (tokenPromise) {
|
||||
try {
|
||||
const cachedResult = await tokenPromise;
|
||||
if (cachedResult.expiresAt > Date.now() + EXPIRATION_MS) {
|
||||
return cachedResult.token;
|
||||
}
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
// 查询storage缓存
|
||||
const res = await getMsauth();
|
||||
token = res?.token;
|
||||
exp = res?.exp;
|
||||
if (token && exp * 1000 > now + 1000) {
|
||||
return [token, exp];
|
||||
}
|
||||
|
||||
// 缓存没有或失效,查询接口
|
||||
token = await fetchData(URL_MICROSOFT_AUTH);
|
||||
exp = parseMSToken(token);
|
||||
await setMsauth({ token, exp });
|
||||
return [token, exp];
|
||||
tokenPromise = fetchNewToken();
|
||||
const result = await tokenPromise;
|
||||
return result.token;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
152
src/libs/batchQueue.js
Normal file
152
src/libs/batchQueue.js
Normal file
@@ -0,0 +1,152 @@
|
||||
import {
|
||||
DEFAULT_BATCH_INTERVAL,
|
||||
DEFAULT_BATCH_SIZE,
|
||||
DEFAULT_BATCH_LENGTH,
|
||||
} from "../config";
|
||||
|
||||
/**
|
||||
* 批处理队列
|
||||
* @param {*} args
|
||||
* @param {*} param1
|
||||
* @returns
|
||||
*/
|
||||
const BatchQueue = (
|
||||
taskFn,
|
||||
{
|
||||
batchInterval = DEFAULT_BATCH_INTERVAL,
|
||||
batchSize = DEFAULT_BATCH_SIZE,
|
||||
batchLength = DEFAULT_BATCH_LENGTH,
|
||||
} = {}
|
||||
) => {
|
||||
const queue = [];
|
||||
let isProcessing = false;
|
||||
let timer = null;
|
||||
|
||||
const sendBatchRequest = async (payloads, batchArgs) => {
|
||||
return taskFn(payloads, batchArgs);
|
||||
};
|
||||
|
||||
const processQueue = async () => {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
|
||||
if (queue.length === 0 || isProcessing) {
|
||||
return;
|
||||
}
|
||||
|
||||
isProcessing = true;
|
||||
|
||||
let tasksToProcess = [];
|
||||
let currentBatchLength = 0;
|
||||
let endIndex = 0;
|
||||
|
||||
for (const task of queue) {
|
||||
const textLength = task.payload?.length || 0;
|
||||
if (
|
||||
endIndex >= batchSize ||
|
||||
(currentBatchLength + textLength > batchLength && endIndex > 0)
|
||||
) {
|
||||
break;
|
||||
}
|
||||
currentBatchLength += textLength;
|
||||
endIndex++;
|
||||
}
|
||||
|
||||
if (endIndex > 0) {
|
||||
tasksToProcess = queue.splice(0, endIndex);
|
||||
}
|
||||
|
||||
if (tasksToProcess.length === 0) {
|
||||
isProcessing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const payloads = tasksToProcess.map((item) => item.payload);
|
||||
const batchArgs = tasksToProcess[0].args;
|
||||
const responses = await sendBatchRequest(payloads, batchArgs);
|
||||
if (!Array.isArray(responses)) {
|
||||
throw new Error("responses format error");
|
||||
}
|
||||
|
||||
tasksToProcess.forEach((taskItem, index) => {
|
||||
const response = responses[index];
|
||||
if (response) {
|
||||
taskItem.resolve(response);
|
||||
} else {
|
||||
taskItem.reject(new Error(`No response for item at index ${index}`));
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
tasksToProcess.forEach((taskItem) => taskItem.reject(error));
|
||||
} finally {
|
||||
isProcessing = false;
|
||||
if (queue.length > 0) {
|
||||
if (queue.length >= batchSize) {
|
||||
setTimeout(processQueue, 0);
|
||||
} else {
|
||||
scheduleProcessing();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const scheduleProcessing = () => {
|
||||
if (!isProcessing && !timer && queue.length > 0) {
|
||||
timer = setTimeout(processQueue, batchInterval);
|
||||
}
|
||||
};
|
||||
|
||||
const addTask = (data, args) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const payload = data;
|
||||
queue.push({ payload, resolve, reject, args });
|
||||
|
||||
if (queue.length >= batchSize) {
|
||||
processQueue();
|
||||
} else {
|
||||
scheduleProcessing();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const destroy = () => {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
queue.forEach((task) =>
|
||||
task.reject(new Error("Queue instance was destroyed."))
|
||||
);
|
||||
queue.length = 0;
|
||||
};
|
||||
|
||||
return { addTask, destroy };
|
||||
};
|
||||
|
||||
// 实例字典
|
||||
const queueMap = new Map();
|
||||
|
||||
/**
|
||||
* 获取批处理实例
|
||||
*/
|
||||
export const getBatchQueue = (key, taskFn, options) => {
|
||||
if (queueMap.has(key)) {
|
||||
return queueMap.get(key);
|
||||
}
|
||||
|
||||
const queue = BatchQueue(taskFn, options);
|
||||
queueMap.set(key, queue);
|
||||
return queue;
|
||||
};
|
||||
|
||||
/**
|
||||
* 清除所有任务
|
||||
*/
|
||||
export const clearAllBatchQueue = () => {
|
||||
for (const queue of queueMap.values()) {
|
||||
queue.destroy();
|
||||
}
|
||||
};
|
||||
10
src/libs/blacklist.js
Normal file
10
src/libs/blacklist.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { isMatch } from "./utils";
|
||||
|
||||
/**
|
||||
* 检查是否在黑名单中
|
||||
* @param {*} href
|
||||
* @param {*} param1
|
||||
* @returns
|
||||
*/
|
||||
export const isInBlacklist = (href, { blacklist }) =>
|
||||
blacklist.split(/\n|,/).some((url) => isMatch(href, url.trim()));
|
||||
@@ -8,8 +8,13 @@ function _browser() {
|
||||
try {
|
||||
return require("webextension-polyfill");
|
||||
} catch (err) {
|
||||
// console.log("[browser]", err.message);
|
||||
// kissLog("browser", err);
|
||||
}
|
||||
}
|
||||
|
||||
export const browser = _browser();
|
||||
|
||||
export const isBg = () => globalThis?.ContextType === "BACKGROUND";
|
||||
|
||||
export const isBuiltinAIAvailable =
|
||||
"LanguageDetector" in globalThis && "Translator" in globalThis;
|
||||
|
||||
168
src/libs/builtinAI.js
Normal file
168
src/libs/builtinAI.js
Normal file
@@ -0,0 +1,168 @@
|
||||
import { kissLog, logger } from "./log";
|
||||
|
||||
/**
|
||||
* Chrome 浏览器内置翻译
|
||||
*/
|
||||
class ChromeTranslator {
|
||||
#translatorMap = new Map();
|
||||
#detectorPromise = null;
|
||||
|
||||
constructor(options = {}) {
|
||||
this.onProgress = options.onProgress || this.#defaultProgressHandler;
|
||||
}
|
||||
|
||||
#defaultProgressHandler(type, progress) {
|
||||
kissLog(`Downloading ${type} model: ${progress}%`);
|
||||
}
|
||||
|
||||
#getDetectorPromise() {
|
||||
if (!this.#detectorPromise) {
|
||||
this.#detectorPromise = (async () => {
|
||||
try {
|
||||
const availability = await LanguageDetector.availability();
|
||||
if (availability === "unavailable") {
|
||||
throw new Error("LanguageDetector unavailable");
|
||||
}
|
||||
|
||||
return await LanguageDetector.create({
|
||||
monitor: (m) => this._monitorProgress(m, "detector"),
|
||||
});
|
||||
} catch (error) {
|
||||
this.#detectorPromise = null;
|
||||
throw error;
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
return this.#detectorPromise;
|
||||
}
|
||||
|
||||
#createTranslator(sourceLanguage, targetLanguage) {
|
||||
const key = `${sourceLanguage}_${targetLanguage}`;
|
||||
if (this.#translatorMap.has(key)) {
|
||||
return this.#translatorMap.get(key);
|
||||
}
|
||||
|
||||
const translatorPromise = (async () => {
|
||||
try {
|
||||
const avail = await Translator.availability({
|
||||
sourceLanguage,
|
||||
targetLanguage,
|
||||
});
|
||||
if (avail === "unavailable") {
|
||||
throw new Error(
|
||||
`Translator ${sourceLanguage}_${targetLanguage} unavailable`
|
||||
);
|
||||
}
|
||||
|
||||
const translator = await Translator.create({
|
||||
sourceLanguage,
|
||||
targetLanguage,
|
||||
monitor: (m) => this._monitorProgress(m, `translator (${key})`),
|
||||
});
|
||||
this.#translatorMap.set(key, translator);
|
||||
|
||||
return translator;
|
||||
} catch (error) {
|
||||
this.#translatorMap.delete(key);
|
||||
throw error;
|
||||
}
|
||||
})();
|
||||
|
||||
this.#translatorMap.set(key, translatorPromise);
|
||||
return translatorPromise;
|
||||
}
|
||||
|
||||
_monitorProgress(monitorable, type) {
|
||||
monitorable.addEventListener("downloadprogress", (e) => {
|
||||
const progress = e.total > 0 ? Math.round((e.loaded / e.total) * 100) : 0;
|
||||
this.onProgress(type, progress);
|
||||
});
|
||||
}
|
||||
|
||||
async detectLanguage(text, confidenceThreshold = 0.4) {
|
||||
if (!text) {
|
||||
return ["", "Input text cannot be empty."];
|
||||
}
|
||||
|
||||
try {
|
||||
const detector = await this.#getDetectorPromise();
|
||||
const results = await detector.detect(text);
|
||||
|
||||
if (!results || results.length === 0) {
|
||||
return ["", "No language could be detected."];
|
||||
}
|
||||
|
||||
const { detectedLanguage, confidence } = results[0];
|
||||
if (confidence < confidenceThreshold) {
|
||||
return [
|
||||
"",
|
||||
`Confidence of test results (${detectedLanguage} ${confidence.toFixed(
|
||||
2
|
||||
)}) below the set threshold ${confidenceThreshold}。`,
|
||||
];
|
||||
}
|
||||
|
||||
return [detectedLanguage, ""];
|
||||
} catch (error) {
|
||||
kissLog("detectLanguage", error, `(${text})`);
|
||||
return ["", error.message];
|
||||
}
|
||||
}
|
||||
|
||||
async translateText(text, targetLanguage, sourceLanguage = "auto") {
|
||||
if (!text || !targetLanguage || typeof text !== "string") {
|
||||
return ["", sourceLanguage, "Input text cannot be empty."];
|
||||
}
|
||||
|
||||
try {
|
||||
let finalSourceLanguage = sourceLanguage;
|
||||
if (sourceLanguage === "auto") {
|
||||
const [detectedLanguage, detectionError] =
|
||||
await this.detectLanguage(text);
|
||||
if (detectionError || !detectedLanguage) {
|
||||
const reason =
|
||||
detectionError || "Unable to determine source language.";
|
||||
return [
|
||||
"",
|
||||
finalSourceLanguage,
|
||||
`Automatic detection of source language failed: ${reason}`,
|
||||
];
|
||||
}
|
||||
finalSourceLanguage = detectedLanguage;
|
||||
}
|
||||
|
||||
if (finalSourceLanguage === targetLanguage) {
|
||||
return ["", finalSourceLanguage, "Same lang"];
|
||||
}
|
||||
|
||||
const translator = await this.#createTranslator(
|
||||
finalSourceLanguage,
|
||||
targetLanguage
|
||||
);
|
||||
const translatedText = await translator.translate(text);
|
||||
|
||||
return [translatedText, finalSourceLanguage, ""];
|
||||
} catch (error) {
|
||||
kissLog("translateText", error, `(${text})`);
|
||||
|
||||
if (
|
||||
error &&
|
||||
error.message &&
|
||||
error.message.includes("Other generic failures occurred")
|
||||
) {
|
||||
logger.info("Generic failure detected, resetting translator cache.");
|
||||
this.#translatorMap.clear();
|
||||
}
|
||||
|
||||
return ["", sourceLanguage, error.message];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const chromeTranslator = new ChromeTranslator();
|
||||
|
||||
export const chromeDetect = (args) =>
|
||||
chromeTranslator.detectLanguage(args.text);
|
||||
export const chromeTranslate = (args) =>
|
||||
chromeTranslator.translateText(args.text, args.to, args.from);
|
||||
159
src/libs/cache.js
Normal file
159
src/libs/cache.js
Normal file
@@ -0,0 +1,159 @@
|
||||
import {
|
||||
CACHE_NAME,
|
||||
DEFAULT_CACHE_TIMEOUT,
|
||||
MSG_CLEAR_CACHES,
|
||||
MSG_GET_HTTPCACHE,
|
||||
MSG_PUT_HTTPCACHE,
|
||||
} from "../config";
|
||||
import { kissLog } from "./log";
|
||||
import { isExt } from "./client";
|
||||
import { isBg } from "./browser";
|
||||
import { sendBgMsg } from "./msg";
|
||||
import { blobToBase64 } from "./utils";
|
||||
|
||||
/**
|
||||
* 清除缓存数据
|
||||
*/
|
||||
export const tryClearCaches = async () => {
|
||||
try {
|
||||
if (isExt && !isBg) {
|
||||
await sendBgMsg(MSG_CLEAR_CACHES);
|
||||
} else {
|
||||
await caches.delete(CACHE_NAME);
|
||||
}
|
||||
} catch (err) {
|
||||
kissLog("clean caches", err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 构造缓存 request
|
||||
* @param {*} input
|
||||
* @param {*} init
|
||||
* @returns
|
||||
*/
|
||||
const newCacheReq = async (input, init) => {
|
||||
let request = new Request(input, init);
|
||||
if (request.method !== "GET") {
|
||||
const body = await request.text();
|
||||
const cacheUrl = new URL(request.url);
|
||||
cacheUrl.pathname += body;
|
||||
request = new Request(cacheUrl.toString(), { method: "GET" });
|
||||
}
|
||||
|
||||
return request;
|
||||
};
|
||||
|
||||
/**
|
||||
* 查询 caches
|
||||
* @param {*} input
|
||||
* @param {*} init
|
||||
* @returns
|
||||
*/
|
||||
export const getHttpCache = async ({ input, init }) => {
|
||||
try {
|
||||
const request = await newCacheReq(input, init);
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
const response = await cache.match(request);
|
||||
if (response) {
|
||||
const res = await parseResponse(response);
|
||||
return res;
|
||||
}
|
||||
} catch (err) {
|
||||
kissLog("get cache", err);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* 插入 caches
|
||||
* @param {*} input
|
||||
* @param {*} init
|
||||
* @param {*} data
|
||||
*/
|
||||
export const putHttpCache = async ({
|
||||
input,
|
||||
init,
|
||||
data,
|
||||
maxAge = DEFAULT_CACHE_TIMEOUT, // todo: 从设置里面读取最大缓存时间
|
||||
}) => {
|
||||
try {
|
||||
const req = await newCacheReq(input, init);
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
const res = new Response(JSON.stringify(data), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Cache-Control": `max-age=${maxAge}`,
|
||||
},
|
||||
});
|
||||
// res.headers.set("Cache-Control", `max-age=${maxAge}`);
|
||||
await cache.put(req, res);
|
||||
} catch (err) {
|
||||
kissLog("put cache", err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析 response
|
||||
* @param {*} res
|
||||
* @returns
|
||||
*/
|
||||
export const parseResponse = async (res) => {
|
||||
if (!res) {
|
||||
throw new Error("Response object does not exist");
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
const contentType = res.headers.get("Content-Type");
|
||||
if (contentType?.includes("json")) {
|
||||
return res.json();
|
||||
} else if (contentType?.includes("audio")) {
|
||||
const blob = await res.blob();
|
||||
return blobToBase64(blob);
|
||||
}
|
||||
return res.text();
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 });
|
||||
};
|
||||
|
||||
/**
|
||||
* putHttpCache 兼容性封装
|
||||
* @param {*} input
|
||||
* @param {*} init
|
||||
* @param {*} data
|
||||
* @returns
|
||||
*/
|
||||
export const putHttpCachePolyfill = (input, init, data) => {
|
||||
// 插件
|
||||
if (isExt && !isBg()) {
|
||||
return sendBgMsg(MSG_PUT_HTTPCACHE, { input, init, data });
|
||||
}
|
||||
|
||||
// 油猴/网页/BackgroundPage
|
||||
return putHttpCache({ input, init, data });
|
||||
};
|
||||
@@ -1,6 +1,12 @@
|
||||
import { CLIENT_EXTS, CLIENT_USERSCRIPT, CLIENT_WEB } from "../config";
|
||||
import {
|
||||
CLIENT_EXTS,
|
||||
CLIENT_USERSCRIPT,
|
||||
CLIENT_WEB,
|
||||
CLIENT_FIREFOX,
|
||||
} 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;
|
||||
export const isFirefox = client === CLIENT_FIREFOX;
|
||||
|
||||
65
src/libs/detect.js
Normal file
65
src/libs/detect.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import {
|
||||
OPT_TRANS_GOOGLE,
|
||||
OPT_TRANS_MICROSOFT,
|
||||
OPT_TRANS_BAIDU,
|
||||
OPT_TRANS_TENCENT,
|
||||
OPT_LANGS_TO_CODE,
|
||||
OPT_LANGS_MAP,
|
||||
OPT_TRANS_BUILTINAI,
|
||||
OPT_LANGDETECTOR_MAP,
|
||||
} from "../config";
|
||||
import { browser } from "./browser";
|
||||
import {
|
||||
apiGoogleLangdetect,
|
||||
apiMicrosoftLangdetect,
|
||||
apiBaiduLangdetect,
|
||||
apiTencentLangdetect,
|
||||
apiBuiltinAIDetect,
|
||||
} from "../apis";
|
||||
import { kissLog } from "./log";
|
||||
|
||||
const langdetectFns = {
|
||||
[OPT_TRANS_GOOGLE]: apiGoogleLangdetect,
|
||||
[OPT_TRANS_MICROSOFT]: apiMicrosoftLangdetect,
|
||||
[OPT_TRANS_BAIDU]: apiBaiduLangdetect,
|
||||
[OPT_TRANS_TENCENT]: apiTencentLangdetect,
|
||||
[OPT_TRANS_BUILTINAI]: apiBuiltinAIDetect,
|
||||
};
|
||||
|
||||
/**
|
||||
* 语言识别
|
||||
* @param {*} text
|
||||
* @returns
|
||||
*/
|
||||
export const tryDetectLang = async (text, langDetector = "-") => {
|
||||
let deLang = "";
|
||||
|
||||
// 内置AI/远程识别
|
||||
if (OPT_LANGDETECTOR_MAP.has(langDetector)) {
|
||||
try {
|
||||
const lang = await langdetectFns[langDetector](text);
|
||||
if (lang) {
|
||||
deLang = OPT_LANGS_TO_CODE[langDetector].get(lang) || "";
|
||||
}
|
||||
} catch (err) {
|
||||
kissLog("detect lang remote", err);
|
||||
}
|
||||
}
|
||||
|
||||
// 本地识别
|
||||
if (!deLang) {
|
||||
try {
|
||||
const res = await browser?.i18n?.detectLanguage(text);
|
||||
const lang = res?.languages?.[0]?.language;
|
||||
if (lang && OPT_LANGS_MAP.has(lang)) {
|
||||
deLang = lang;
|
||||
} else if (lang?.startsWith("zh")) {
|
||||
deLang = "zh-CN";
|
||||
}
|
||||
} catch (err) {
|
||||
kissLog("detect lang local", err);
|
||||
}
|
||||
}
|
||||
|
||||
return deLang;
|
||||
};
|
||||
18
src/libs/fabManager.js
Normal file
18
src/libs/fabManager.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import ShadowDomManager from "./shadowDomManager";
|
||||
import { APP_CONSTS } from "../config";
|
||||
import ContentFab from "../views/Action/ContentFab";
|
||||
|
||||
export class FabManager extends ShadowDomManager {
|
||||
constructor({ processActions, fabConfig }) {
|
||||
super({
|
||||
id: APP_CONSTS.fabID,
|
||||
className: "notranslate",
|
||||
reactComponent: ContentFab,
|
||||
props: { processActions, fabConfig },
|
||||
});
|
||||
|
||||
if (!fabConfig?.isHide) {
|
||||
this.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,11 @@
|
||||
import { isExt, isGm } from "./client";
|
||||
import { sendBgMsg } from "./msg";
|
||||
import { taskPool } from "./pool";
|
||||
import {
|
||||
MSG_FETCH,
|
||||
MSG_FETCH_LIMIT,
|
||||
MSG_FETCH_CLEAR,
|
||||
CACHE_NAME,
|
||||
OPT_TRANS_MICROSOFT,
|
||||
OPT_TRANS_DEEPL,
|
||||
OPT_TRANS_OPENAI,
|
||||
DEFAULT_FETCH_INTERVAL,
|
||||
DEFAULT_FETCH_LIMIT,
|
||||
} from "../config";
|
||||
import { msAuth } from "./auth";
|
||||
import { getSettingWithDefault } from "./storage";
|
||||
import { MSG_FETCH, DEFAULT_HTTP_TIMEOUT } from "../config";
|
||||
import { isBg } from "./browser";
|
||||
import { kissLog } from "./log";
|
||||
import { getFetchPool } from "./pool";
|
||||
import { getHttpCachePolyfill, parseResponse } from "./cache";
|
||||
|
||||
/**
|
||||
* 油猴脚本的请求封装
|
||||
@@ -20,206 +13,140 @@ import { msAuth } from "./auth";
|
||||
* @param {*} init
|
||||
* @returns
|
||||
*/
|
||||
export const fetchGM = async (input, { method = "GET", headers, body } = {}) =>
|
||||
export const fetchGM = async (
|
||||
input,
|
||||
{ method = "GET", headers, body, timeout } = {}
|
||||
) =>
|
||||
new Promise((resolve, reject) => {
|
||||
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}`));
|
||||
}
|
||||
// withCredentials: true,
|
||||
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,
|
||||
onabort: () => {
|
||||
reject(new Error("GM request onabort."));
|
||||
},
|
||||
ontimeout: () => {
|
||||
reject(new Error("GM request timeout."));
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 构造缓存 request
|
||||
* @param {*} request
|
||||
* @returns
|
||||
*/
|
||||
const newCacheReq = async (request) => {
|
||||
if (request.method !== "GET") {
|
||||
const body = await request.text();
|
||||
const cacheUrl = new URL(request.url);
|
||||
cacheUrl.pathname += body;
|
||||
request = new Request(cacheUrl.toString(), { method: "GET" });
|
||||
}
|
||||
|
||||
return request;
|
||||
};
|
||||
|
||||
/**
|
||||
* 发起请求
|
||||
* @param {*} param0
|
||||
* @param {*} input
|
||||
* @param {*} init
|
||||
* @param {*} opts
|
||||
* @returns
|
||||
*/
|
||||
const fetchApi = async ({ input, init = {}, translator, token }) => {
|
||||
if (token) {
|
||||
if (translator === OPT_TRANS_DEEPL) {
|
||||
init.headers["Authorization"] = `DeepL-Auth-Key ${token}`; // DeepL
|
||||
} else if (translator === OPT_TRANS_OPENAI) {
|
||||
init.headers["Authorization"] = `Bearer ${token}`; // OpenAI
|
||||
init.headers["api-key"] = token; // Azure OpenAI
|
||||
} else {
|
||||
init.headers["Authorization"] = `Bearer ${token}`; // Microsoft & others
|
||||
export const fetchPatcher = async (input, init = {}, opts) => {
|
||||
let timeout = opts?.httpTimeout;
|
||||
if (!timeout) {
|
||||
try {
|
||||
timeout = (await getSettingWithDefault()).httpTimeout;
|
||||
} catch (err) {
|
||||
kissLog("getSettingWithDefault", err);
|
||||
}
|
||||
}
|
||||
if (!timeout) {
|
||||
timeout = DEFAULT_HTTP_TIMEOUT;
|
||||
}
|
||||
|
||||
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) {
|
||||
if (window.KISS_GM) {
|
||||
return window.KISS_GM.fetch(input, init);
|
||||
} else {
|
||||
return fetchGM(input, init);
|
||||
}
|
||||
}
|
||||
// todo: 自定义接口 init 可能包含了 signal
|
||||
Object.assign(init, { timeout });
|
||||
|
||||
const { body, headers, status, statusText } = window.KISS_GM
|
||||
? await window.KISS_GM.fetch(input, init)
|
||||
: await fetchGM(input, init);
|
||||
|
||||
return new Response(body, {
|
||||
headers: new Headers(headers),
|
||||
status,
|
||||
statusText,
|
||||
});
|
||||
}
|
||||
|
||||
if (AbortSignal?.timeout && !init.signal) {
|
||||
Object.assign(init, { signal: AbortSignal.timeout(timeout) });
|
||||
}
|
||||
|
||||
return fetch(input, init);
|
||||
};
|
||||
|
||||
/**
|
||||
* 请求池实例
|
||||
*/
|
||||
export const fetchPool = taskPool(
|
||||
fetchApi,
|
||||
async ({ translator }) => {
|
||||
if (translator === OPT_TRANS_MICROSOFT) {
|
||||
const [token] = await msAuth();
|
||||
return { token };
|
||||
}
|
||||
return {};
|
||||
},
|
||||
DEFAULT_FETCH_INTERVAL,
|
||||
DEFAULT_FETCH_LIMIT
|
||||
);
|
||||
|
||||
/**
|
||||
* 请求数据统一接口
|
||||
* @param {*} input
|
||||
* @param {*} opts
|
||||
* 处理请求
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
export const fetchData = async (
|
||||
input,
|
||||
{ useCache, usePool, translator, token, ...init } = {}
|
||||
) => {
|
||||
const cacheReq = await newCacheReq(new Request(input, init));
|
||||
let res;
|
||||
|
||||
// 查询缓存
|
||||
if (useCache) {
|
||||
try {
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
res = await cache.match(cacheReq);
|
||||
} catch (err) {
|
||||
console.log("[cache match]", err.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (!res) {
|
||||
// 发送请求
|
||||
if (usePool) {
|
||||
res = await fetchPool.push({ input, init, translator, token });
|
||||
} else {
|
||||
res = await fetchApi({ input, init, translator, token });
|
||||
}
|
||||
|
||||
if (!res?.ok) {
|
||||
throw new Error(`response: ${res.statusText}`);
|
||||
}
|
||||
|
||||
// 插入缓存
|
||||
if (useCache) {
|
||||
try {
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
await cache.put(cacheReq, res.clone());
|
||||
} catch (err) {
|
||||
console.log("[cache put]", err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const contentType = res.headers.get("Content-Type");
|
||||
if (contentType?.includes("json")) {
|
||||
return await res.json();
|
||||
}
|
||||
return await res.text();
|
||||
export const fetchHandle = async ({ input, init, opts }) => {
|
||||
const res = await fetchPatcher(input, init, opts);
|
||||
return parseResponse(res);
|
||||
};
|
||||
|
||||
/**
|
||||
* fetch 兼容性封装
|
||||
* @param {*} input
|
||||
* @param {*} opts
|
||||
* @param {*} args
|
||||
* @returns
|
||||
*/
|
||||
export const fetchPolyfill = async (input, { isBg = false, ...opts } = {}) => {
|
||||
if (!input.trim()) {
|
||||
throw new Error("URL is empty");
|
||||
}
|
||||
|
||||
export const fnPolyfill = ({ fn, msg = MSG_FETCH, ...args }) => {
|
||||
// 插件
|
||||
if (isExt && !isBg) {
|
||||
const res = await sendBgMsg(MSG_FETCH, { input, opts });
|
||||
if (res.error) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
return res.data;
|
||||
if (isExt && !isBg()) {
|
||||
return sendBgMsg(msg, { ...args });
|
||||
}
|
||||
|
||||
// 油猴/网页/BackgroundPage
|
||||
return await fetchData(input, opts);
|
||||
return fn({ ...args });
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新 fetch pool 参数
|
||||
* @param {*} interval
|
||||
* @param {*} limit
|
||||
* 数据请求
|
||||
* @param {*} input
|
||||
* @param {*} init
|
||||
* @param {*} param1
|
||||
* @returns
|
||||
*/
|
||||
export const updateFetchPool = async (interval, limit) => {
|
||||
if (isExt) {
|
||||
const res = await sendBgMsg(MSG_FETCH_LIMIT, { interval, limit });
|
||||
if (res.error) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
} else {
|
||||
fetchPool.update(interval, limit);
|
||||
export const fetchData = async (
|
||||
input,
|
||||
init,
|
||||
{ useCache, usePool, fetchInterval, fetchLimit, ...opts } = {}
|
||||
) => {
|
||||
if (!input?.trim()) {
|
||||
throw new Error("URL is empty");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 清空任务池
|
||||
*/
|
||||
export const clearFetchPool = async () => {
|
||||
if (isExt) {
|
||||
const res = await sendBgMsg(MSG_FETCH_CLEAR);
|
||||
if (res.error) {
|
||||
throw new Error(res.error);
|
||||
// 使用缓存数据
|
||||
if (useCache) {
|
||||
const resCache = await getHttpCachePolyfill(input, init);
|
||||
if (resCache) {
|
||||
return resCache;
|
||||
}
|
||||
} else {
|
||||
fetchPool.clear();
|
||||
}
|
||||
|
||||
// 通过任务池发送请求
|
||||
if (usePool) {
|
||||
const fetchPool = getFetchPool(fetchInterval, fetchLimit);
|
||||
return fetchPool.push(fnPolyfill, { fn: fetchHandle, input, init, opts });
|
||||
}
|
||||
|
||||
// 直接请求
|
||||
return fnPolyfill({ fn: fetchHandle, input, init, opts });
|
||||
};
|
||||
|
||||
@@ -5,3 +5,7 @@ export const sendIframeMsg = (action, args) => {
|
||||
iframe.contentWindow.postMessage({ action, args }, "*");
|
||||
});
|
||||
};
|
||||
|
||||
export const sendParentMsg = (action, args) => {
|
||||
window.parent.postMessage({ action, args }, "*");
|
||||
};
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { CACHE_NAME } from "../config";
|
||||
import { browser } from "./browser";
|
||||
|
||||
/**
|
||||
* 清除缓存数据
|
||||
*/
|
||||
export const tryClearCaches = async () => {
|
||||
try {
|
||||
caches.delete(CACHE_NAME);
|
||||
} catch (err) {
|
||||
console.log("[clean caches]", err.message);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 本地语言识别
|
||||
* @param {*} q
|
||||
* @returns
|
||||
*/
|
||||
export const tryDetectLang = async (q) => {
|
||||
try {
|
||||
const res = await browser?.i18n?.detectLanguage(q);
|
||||
return res?.languages?.[0]?.language;
|
||||
} catch (err) {
|
||||
console.log("[detect lang]", err.message);
|
||||
}
|
||||
};
|
||||
57
src/libs/injector.js
Normal file
57
src/libs/injector.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import { trustedTypesHelper } from "./trustedTypes";
|
||||
|
||||
// Function to inject inline JavaScript code
|
||||
export const injectInlineJs = (code, id = "kiss-translator-inline-js") => {
|
||||
if (document.getElementById(id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const el = document.createElement("script");
|
||||
el.type = "text/javascript";
|
||||
el.id = id;
|
||||
el.textContent = trustedTypesHelper.createScript(code);
|
||||
(document.head || document.documentElement).appendChild(el);
|
||||
};
|
||||
|
||||
export const injectInlineJsBg = (code, id = "kiss-translator-inline-js") => {
|
||||
if (document.getElementById(id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const el = document.createElement("script");
|
||||
el.type = "text/javascript";
|
||||
el.id = id;
|
||||
el.textContent = code;
|
||||
(document.head || document.documentElement).appendChild(el);
|
||||
};
|
||||
|
||||
// Function to inject external JavaScript file
|
||||
export const injectExternalJs = (src, id = "kiss-translator-external-js") => {
|
||||
if (document.getElementById(id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const el = document.createElement("script");
|
||||
el.type = "text/javascript";
|
||||
el.id = id;
|
||||
el.src = trustedTypesHelper.createScriptURL(src);
|
||||
(document.head || document.documentElement).appendChild(el);
|
||||
};
|
||||
|
||||
// Function to inject internal CSS code
|
||||
export const injectInternalCss = (styles) => {
|
||||
const el = document.createElement("style");
|
||||
el.setAttribute("data-source", "kiss-inject 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-inject injectExternalCss");
|
||||
el.setAttribute("rel", "stylesheet");
|
||||
el.setAttribute("type", "text/css");
|
||||
el.setAttribute("href", href);
|
||||
document.head?.appendChild(el);
|
||||
};
|
||||
324
src/libs/inputTranslate.js
Normal file
324
src/libs/inputTranslate.js
Normal file
@@ -0,0 +1,324 @@
|
||||
import {
|
||||
DEFAULT_INPUT_RULE,
|
||||
DEFAULT_INPUT_SHORTCUT,
|
||||
OPT_LANGS_LIST,
|
||||
DEFAULT_API_SETTING,
|
||||
} from "../config";
|
||||
import { genEventName, removeEndchar, matchInputStr, sleep } from "./utils";
|
||||
import { stepShortcutRegister } from "./shortcut";
|
||||
import { apiTranslate } from "../apis";
|
||||
import { createLoadingSVG } from "./svg";
|
||||
import { logger } from "./log";
|
||||
|
||||
function isInputNode(node) {
|
||||
return node.nodeName === "INPUT" || node.nodeName === "TEXTAREA";
|
||||
}
|
||||
|
||||
function isEditAbleNode(node) {
|
||||
return node.hasAttribute("contenteditable");
|
||||
}
|
||||
|
||||
async function replaceContentEditableText(node, newText) {
|
||||
try {
|
||||
logger.debug("try replace editable 1: pasteEvent");
|
||||
|
||||
node.focus();
|
||||
|
||||
const selection = window.getSelection();
|
||||
if (!selection) throw new Error("window.getSelection() is not available.");
|
||||
|
||||
const targetNode = node.querySelector("p") || node;
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(targetNode);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.setData("text/plain", newText);
|
||||
|
||||
const pasteEvent = new ClipboardEvent("paste", {
|
||||
clipboardData: dataTransfer,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
node.dispatchEvent(pasteEvent);
|
||||
|
||||
await sleep(50);
|
||||
if (node.innerText.trim() === newText) {
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new Error("Strategy 1 failed to replace text correctly.");
|
||||
} catch (error) {
|
||||
logger.debug("Strategy 1 Failed:", error.message);
|
||||
}
|
||||
|
||||
try {
|
||||
logger.debug("try replace editable 2: execCommand");
|
||||
|
||||
node.focus();
|
||||
|
||||
const selection = window.getSelection();
|
||||
if (!selection) throw new Error("window.getSelection() is not available.");
|
||||
|
||||
const targetNode = node.querySelector("p") || node;
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(targetNode);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
|
||||
document.execCommand("insertText", false, newText);
|
||||
|
||||
await sleep(50);
|
||||
if (node.innerText.trim() === newText) {
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new Error("Strategy 2 failed to replace text correctly.");
|
||||
} catch (error) {
|
||||
logger.debug("Strategy 2 Failed:", error.message);
|
||||
}
|
||||
|
||||
try {
|
||||
logger.debug("try replace editable 3: textContent");
|
||||
|
||||
node.focus();
|
||||
|
||||
const targetNode = node.querySelector("p") || node;
|
||||
const textSpan = targetNode.querySelector('span[data-lexical-text="true"]');
|
||||
|
||||
if (textSpan) {
|
||||
textSpan.textContent = newText;
|
||||
} else {
|
||||
targetNode.textContent = newText;
|
||||
}
|
||||
|
||||
node.dispatchEvent(new Event("input", { bubbles: true, cancelable: true }));
|
||||
|
||||
await sleep(50);
|
||||
if (node.innerText.trim() === newText) {
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new Error("Strategy 3 failed to replace text correctly.");
|
||||
} catch (error) {
|
||||
logger.debug("Strategy 3 Failed:", error.message);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function getNodeText(node) {
|
||||
if (isInputNode(node)) {
|
||||
return node.value;
|
||||
}
|
||||
return node.innerText || node.textContent || "";
|
||||
}
|
||||
|
||||
function addLoading(node, loadingId) {
|
||||
const rect = node.getBoundingClientRect();
|
||||
const div = document.createElement("div");
|
||||
div.id = loadingId;
|
||||
div.appendChild(createLoadingSVG());
|
||||
div.style.cssText = `
|
||||
position: fixed;
|
||||
left: ${rect.left}px;
|
||||
top: ${rect.top}px;
|
||||
width: ${rect.width}px;
|
||||
height: ${rect.height}px;
|
||||
line-height: ${rect.height}px;
|
||||
text-align: center;
|
||||
z-index: 2147483647;
|
||||
pointer-events: none; /* 允许点击穿透 */
|
||||
`;
|
||||
document.body.appendChild(div);
|
||||
}
|
||||
|
||||
function removeLoading(loadingId) {
|
||||
const div = document.getElementById(loadingId);
|
||||
if (div) div.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* 输入框翻译
|
||||
*/
|
||||
export class InputTranslator {
|
||||
#config;
|
||||
#unregisterShortcut = null;
|
||||
#isEnabled = false;
|
||||
#triggerShortcut; // 用于缓存快捷键
|
||||
|
||||
constructor({ inputRule = DEFAULT_INPUT_RULE, transApis = [] } = {}) {
|
||||
this.#config = { inputRule, transApis };
|
||||
|
||||
const { triggerShortcut: initialTriggerShortcut } = this.#config.inputRule;
|
||||
if (initialTriggerShortcut && initialTriggerShortcut.length > 0) {
|
||||
this.#triggerShortcut = initialTriggerShortcut;
|
||||
} else {
|
||||
this.#triggerShortcut = DEFAULT_INPUT_SHORTCUT;
|
||||
}
|
||||
|
||||
if (this.#config.inputRule.transOpen) {
|
||||
this.enable();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用输入翻译功能
|
||||
*/
|
||||
enable() {
|
||||
if (this.#isEnabled || !this.#config.inputRule.transOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { triggerCount, triggerTime } = this.#config.inputRule;
|
||||
this.#unregisterShortcut = stepShortcutRegister(
|
||||
this.#triggerShortcut,
|
||||
this.#handleTranslate.bind(this),
|
||||
triggerCount,
|
||||
triggerTime
|
||||
);
|
||||
|
||||
this.#isEnabled = true;
|
||||
logger.info("Input Translator enabled.");
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用输入翻译功能
|
||||
*/
|
||||
disable() {
|
||||
if (!this.#isEnabled) {
|
||||
return;
|
||||
}
|
||||
if (this.#unregisterShortcut) {
|
||||
this.#unregisterShortcut();
|
||||
this.#unregisterShortcut = null;
|
||||
}
|
||||
this.#isEnabled = false;
|
||||
logger.info("Input Translator disabled.");
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换启用/禁用状态
|
||||
*/
|
||||
toggle() {
|
||||
if (this.#isEnabled) {
|
||||
this.disable();
|
||||
} else {
|
||||
this.enable();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译核心逻辑
|
||||
* @private
|
||||
*/
|
||||
async #handleTranslate() {
|
||||
let node = document.activeElement;
|
||||
if (!node) return;
|
||||
|
||||
while (node.shadowRoot && node.shadowRoot.activeElement) {
|
||||
node = node.shadowRoot.activeElement;
|
||||
}
|
||||
|
||||
if (!isInputNode(node) && !isEditAbleNode(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { apiSlug, transSign, triggerCount } = this.#config.inputRule;
|
||||
let { fromLang, toLang } = this.#config.inputRule;
|
||||
|
||||
let initText = getNodeText(node);
|
||||
|
||||
if (
|
||||
this.#triggerShortcut.length === 1 &&
|
||||
this.#triggerShortcut[0].length === 1
|
||||
) {
|
||||
initText = removeEndchar(
|
||||
initText,
|
||||
this.#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];
|
||||
}
|
||||
}
|
||||
|
||||
const apiSetting =
|
||||
this.#config.transApis.find((api) => api.apiSlug === apiSlug) ||
|
||||
DEFAULT_API_SETTING;
|
||||
const loadingId = "kiss-loading-" + genEventName();
|
||||
|
||||
try {
|
||||
addLoading(node, loadingId);
|
||||
|
||||
const { trText, isSame } = await apiTranslate({
|
||||
text,
|
||||
fromLang,
|
||||
toLang,
|
||||
apiSetting,
|
||||
});
|
||||
|
||||
const newText = trText?.trim() || "";
|
||||
if (!newText || isSame) return;
|
||||
|
||||
if (isInputNode(node)) {
|
||||
node.value = newText;
|
||||
node.dispatchEvent(
|
||||
new Event("input", { bubbles: true, cancelable: true })
|
||||
);
|
||||
} else {
|
||||
const success = await replaceContentEditableText(node, newText);
|
||||
if (!success) {
|
||||
// todo: 提示可以黏贴
|
||||
logger.info("Replace editable text failed");
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.info("Translate input error:", err);
|
||||
} finally {
|
||||
removeLoading(loadingId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新配置
|
||||
*/
|
||||
updateConfig({ inputRule, transApis }) {
|
||||
const wasEnabled = this.#isEnabled;
|
||||
if (wasEnabled) {
|
||||
this.disable();
|
||||
}
|
||||
|
||||
if (inputRule) {
|
||||
this.#config.inputRule = inputRule;
|
||||
}
|
||||
if (transApis) {
|
||||
this.#config.transApis = transApis;
|
||||
}
|
||||
|
||||
const { triggerShortcut: initialTriggerShortcut } = this.#config.inputRule;
|
||||
this.#triggerShortcut =
|
||||
initialTriggerShortcut && initialTriggerShortcut.length > 0
|
||||
? initialTriggerShortcut
|
||||
: DEFAULT_INPUT_SHORTCUT;
|
||||
|
||||
if (wasEnabled) {
|
||||
this.enable();
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/libs/interpreter.js
Normal file
14
src/libs/interpreter.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import Sval from "sval";
|
||||
|
||||
export 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,
|
||||
});
|
||||
161
src/libs/log.js
Normal file
161
src/libs/log.js
Normal file
@@ -0,0 +1,161 @@
|
||||
// 定义日志级别
|
||||
export const LogLevel = {
|
||||
DEBUG: { value: 0, name: "DEBUG", color: "#6495ED" }, // 宝蓝色
|
||||
INFO: { value: 1, name: "INFO", color: "#4CAF50" }, // 绿色
|
||||
WARN: { value: 2, name: "WARN", color: "#FFC107" }, // 琥珀色
|
||||
ERROR: { value: 3, name: "ERROR", color: "#F44336" }, // 红色
|
||||
SILENT: { value: 4, name: "SILENT" }, // 特殊级别,用于关闭所有日志
|
||||
};
|
||||
|
||||
function findLogLevelByValue(value) {
|
||||
return Object.values(LogLevel).find((level) => level.value === value);
|
||||
}
|
||||
|
||||
function findLogLevelByName(name) {
|
||||
if (typeof name !== "string" || name.length === 0) return undefined;
|
||||
const upperCaseName = name.toUpperCase();
|
||||
return Object.values(LogLevel).find((level) => level.name === upperCaseName);
|
||||
}
|
||||
|
||||
class Logger {
|
||||
/**
|
||||
* @param {object} [options={}] 配置选项
|
||||
* @param {LogLevel} [options.level=LogLevel.INFO] 要显示的最低日志级别
|
||||
* @param {string} [options.prefix='App'] 日志前缀,用于区分模块
|
||||
*/
|
||||
constructor(options = {}) {
|
||||
this.config = {
|
||||
level: options.level || LogLevel.INFO,
|
||||
prefix: options.prefix || "KISS-Translator",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态设置日志级别
|
||||
* @param {LogLevel} level - 新的日志级别
|
||||
*/
|
||||
setLevel(level) {
|
||||
let newLevelObject;
|
||||
|
||||
if (typeof level === "string") {
|
||||
newLevelObject = findLogLevelByName(level);
|
||||
if (!newLevelObject) {
|
||||
this.warn(
|
||||
`Invalid log level name provided: "${level}". Keeping current level.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else if (typeof level === "number") {
|
||||
newLevelObject = findLogLevelByValue(level);
|
||||
if (!newLevelObject) {
|
||||
this.warn(
|
||||
`Invalid log level value provided: ${level}. Keeping current level.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else if (level && typeof level.value === "number") {
|
||||
newLevelObject = level;
|
||||
} else {
|
||||
this.warn(
|
||||
"Invalid argument passed to setLevel. Must be a LogLevel object, number, or string."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.config.level = newLevelObject;
|
||||
console.log(
|
||||
`[${this.config.prefix}] Log level dynamically set to ${this.config.level.name}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心日志记录方法
|
||||
* @private
|
||||
* @param {LogLevel} level - 当前消息的日志级别
|
||||
* @param {...any} args - 要记录的多个参数,可以是任何类型
|
||||
*/
|
||||
_log(level, ...args) {
|
||||
// 如果当前级别低于配置的最低级别,则不打印
|
||||
if (level.value < this.config.level.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timestamp = new Date().toISOString();
|
||||
const prefixStr = `[${this.config.prefix}]`;
|
||||
const levelStr = `[${level.name}]`;
|
||||
|
||||
// 判断是否在浏览器环境并且浏览器支持 console 样式
|
||||
const isBrowser =
|
||||
typeof window !== "undefined" && typeof window.document !== "undefined";
|
||||
|
||||
if (isBrowser) {
|
||||
// 在浏览器中使用颜色高亮
|
||||
const consoleMethod = this._getConsoleMethod(level);
|
||||
consoleMethod(
|
||||
`%c${timestamp} %c${prefixStr} %c${levelStr}`,
|
||||
"color: gray; font-weight: lighter;", // 时间戳样式
|
||||
"color: #7c57e0; font-weight: bold;", // 前缀样式 (紫色)
|
||||
`color: ${level.color}; font-weight: bold;`, // 日志级别样式
|
||||
...args
|
||||
);
|
||||
} else {
|
||||
// 在 Node.js 或不支持样式的环境中,输出纯文本
|
||||
const consoleMethod = this._getConsoleMethod(level);
|
||||
consoleMethod(timestamp, prefixStr, levelStr, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据日志级别获取对应的 console 方法
|
||||
* @private
|
||||
*/
|
||||
_getConsoleMethod(level) {
|
||||
switch (level) {
|
||||
case LogLevel.ERROR:
|
||||
return console.error;
|
||||
case LogLevel.WARN:
|
||||
return console.warn;
|
||||
case LogLevel.INFO:
|
||||
return console.info;
|
||||
default:
|
||||
return console.log;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录 DEBUG 级别的日志
|
||||
* @param {...any} args
|
||||
*/
|
||||
debug(...args) {
|
||||
this._log(LogLevel.DEBUG, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录 INFO 级别的日志
|
||||
* @param {...any} args
|
||||
*/
|
||||
info(...args) {
|
||||
this._log(LogLevel.INFO, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录 WARN 级别的日志
|
||||
* @param {...any} args
|
||||
*/
|
||||
warn(...args) {
|
||||
this._log(LogLevel.WARN, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录 ERROR 级别的日志
|
||||
* @param {...any} args
|
||||
*/
|
||||
error(...args) {
|
||||
this._log(LogLevel.ERROR, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
export const logger = new Logger();
|
||||
export const kissLog = logger.info.bind(logger);
|
||||
|
||||
// todo:debug日志埋点
|
||||
@@ -1,5 +1,22 @@
|
||||
import { browser } from "./browser";
|
||||
|
||||
/**
|
||||
* 获取当前tab信息
|
||||
* @returns
|
||||
*/
|
||||
export const getCurTab = async () => {
|
||||
const [tab] = await browser.tabs.query({
|
||||
active: true,
|
||||
lastFocusedWindow: true,
|
||||
});
|
||||
return tab;
|
||||
};
|
||||
|
||||
export const getCurTabId = async () => {
|
||||
const tab = await getCurTab();
|
||||
return tab.id;
|
||||
};
|
||||
|
||||
/**
|
||||
* 发送消息给background
|
||||
* @param {*} action
|
||||
@@ -16,6 +33,6 @@ export const sendBgMsg = (action, args) =>
|
||||
* @returns
|
||||
*/
|
||||
export const sendTabMsg = async (action, args) => {
|
||||
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
|
||||
return browser.tabs.sendMessage(tabs[0].id, { action, args });
|
||||
const tabId = await getCurTabId();
|
||||
return browser.tabs.sendMessage(tabId, { action, args });
|
||||
};
|
||||
|
||||
213
src/libs/pool.js
213
src/libs/pool.js
@@ -1,77 +1,170 @@
|
||||
import { DEFAULT_FETCH_INTERVAL, DEFAULT_FETCH_LIMIT } from "../config";
|
||||
import { kissLog } from "./log";
|
||||
|
||||
/**
|
||||
* 任务池
|
||||
* @param {*} fn
|
||||
* @param {*} preFn
|
||||
* @param {*} _interval
|
||||
* @param {*} _limit
|
||||
* @returns
|
||||
*/
|
||||
export const taskPool = (fn, preFn, _interval = 100, _limit = 100) => {
|
||||
const pool = [];
|
||||
const maxRetry = 2; // 最大重试次数
|
||||
let maxCount = _limit; // 最大数量
|
||||
let curCount = 0; // 当前数量
|
||||
let interval = _interval; // 间隔时间
|
||||
let timer = null;
|
||||
class TaskPool {
|
||||
#pool = [];
|
||||
|
||||
#maxRetry = 2; // 最大重试次数
|
||||
#retryInterval = 1000; // 重试间隔时间
|
||||
#limit; // 最大并发数
|
||||
#interval; // 任务最小启动间隔
|
||||
|
||||
#currentConcurrent = 0; // 当前正在执行的任务数
|
||||
#lastExecutionTime = 0; // 上一个任务的启动时间
|
||||
#schedulerTimer = null; // 用于调度下一个任务的定时器
|
||||
|
||||
constructor(
|
||||
interval = DEFAULT_FETCH_INTERVAL,
|
||||
limit = DEFAULT_FETCH_LIMIT,
|
||||
retryInterval = 1000
|
||||
) {
|
||||
this.#interval = interval;
|
||||
this.#limit = limit;
|
||||
this.#retryInterval = retryInterval;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调度器
|
||||
*/
|
||||
#scheduleNext() {
|
||||
if (this.#schedulerTimer) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.#currentConcurrent >= this.#limit || this.#pool.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const timeSinceLast = now - this.#lastExecutionTime;
|
||||
const delay = Math.max(0, this.#interval - timeSinceLast);
|
||||
|
||||
this.#schedulerTimer = setTimeout(() => {
|
||||
this.#schedulerTimer = null;
|
||||
if (this.#currentConcurrent < this.#limit && this.#pool.length > 0) {
|
||||
const task = this.#pool.shift();
|
||||
if (task) {
|
||||
this.#lastExecutionTime = Date.now();
|
||||
this.#execute(task);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.#pool.length > 0) {
|
||||
this.#scheduleNext();
|
||||
}
|
||||
}, delay);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行单个任务
|
||||
* @param {object} task - 任务对象
|
||||
*/
|
||||
async #execute(task) {
|
||||
this.#currentConcurrent++;
|
||||
const { fn, args, resolve, reject, retry } = task;
|
||||
|
||||
const handleTask = async (item, preArgs) => {
|
||||
curCount++;
|
||||
const { args, resolve, reject, retry } = item;
|
||||
try {
|
||||
const res = await fn({ ...args, ...preArgs });
|
||||
const res = await fn(args);
|
||||
resolve(res);
|
||||
} catch (err) {
|
||||
if (retry < maxRetry) {
|
||||
pool.push({ args, resolve, reject, retry: retry + 1 });
|
||||
kissLog("task pool", err);
|
||||
if (retry < this.#maxRetry) {
|
||||
setTimeout(() => {
|
||||
this.#pool.unshift({ ...task, retry: retry + 1 }); // unshift 保证重试任务优先
|
||||
this.#scheduleNext();
|
||||
}, this.#retryInterval);
|
||||
} else {
|
||||
reject(err);
|
||||
}
|
||||
} finally {
|
||||
curCount--;
|
||||
this.#currentConcurrent--;
|
||||
this.#scheduleNext();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const run = async () => {
|
||||
// console.log("timer", timer);
|
||||
timer && clearTimeout(timer);
|
||||
timer = setTimeout(run, interval);
|
||||
/**
|
||||
* 向任务池中添加一个新任务
|
||||
* @param {Function} fn - 要执行的异步函数
|
||||
* @param {*} args - 函数的参数
|
||||
* @returns {Promise}
|
||||
*/
|
||||
push(fn, args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.#pool.push({ fn, args, resolve, reject, retry: 0 });
|
||||
this.#scheduleNext();
|
||||
});
|
||||
}
|
||||
|
||||
if (curCount < maxCount) {
|
||||
const item = pool.shift();
|
||||
if (item) {
|
||||
try {
|
||||
const preArgs = await preFn(item.args);
|
||||
handleTask(item, preArgs);
|
||||
} catch (err) {
|
||||
console.log("[preFn]", err);
|
||||
pool.push(item);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 更新任务池的配置
|
||||
* @param {number} interval - 新的最小任务间隔
|
||||
* @param {number} limit - 新的最大并发数
|
||||
*/
|
||||
update(interval, limit) {
|
||||
if (interval >= 0) {
|
||||
this.#interval = interval;
|
||||
}
|
||||
if (limit >= 1) {
|
||||
this.#limit = limit;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
push: async (args) => {
|
||||
if (!timer) {
|
||||
run();
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
pool.push({ args, resolve, reject, retry: 0 });
|
||||
});
|
||||
},
|
||||
update: (_interval = 100, _limit = 100) => {
|
||||
if (_interval >= 0 && _interval <= 5000 && _interval !== interval) {
|
||||
interval = _interval;
|
||||
}
|
||||
if (_limit >= 1 && _limit <= 100 && _limit !== maxCount) {
|
||||
maxCount = _limit;
|
||||
}
|
||||
},
|
||||
clear: () => {
|
||||
pool.length = 0;
|
||||
curCount = 0;
|
||||
timer && clearTimeout(timer);
|
||||
timer = null;
|
||||
},
|
||||
};
|
||||
this.#scheduleNext();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空任务池
|
||||
*/
|
||||
clear() {
|
||||
for (const task of this.#pool) {
|
||||
task.reject("the task pool was cleared");
|
||||
}
|
||||
|
||||
this.#pool.length = 0;
|
||||
if (this.#schedulerTimer) {
|
||||
clearTimeout(this.#schedulerTimer);
|
||||
this.#schedulerTimer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求池实例
|
||||
*/
|
||||
let fetchPool;
|
||||
|
||||
/**
|
||||
* 获取请求池实例
|
||||
* @param interval
|
||||
* @param limit
|
||||
* @returns
|
||||
*/
|
||||
export const getFetchPool = (interval, limit) => {
|
||||
if (!fetchPool) {
|
||||
fetchPool = new TaskPool(
|
||||
interval ?? DEFAULT_FETCH_INTERVAL,
|
||||
limit ?? DEFAULT_FETCH_LIMIT
|
||||
);
|
||||
} else if (interval && limit) {
|
||||
updateFetchPool(interval, limit);
|
||||
}
|
||||
return fetchPool;
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新请求池参数
|
||||
* @param {*} interval
|
||||
* @param {*} limit
|
||||
*/
|
||||
export const updateFetchPool = (interval, limit) => {
|
||||
fetchPool?.update(interval, limit);
|
||||
};
|
||||
|
||||
/**
|
||||
* 清空请求池
|
||||
*/
|
||||
export const clearFetchPool = () => {
|
||||
fetchPool?.clear();
|
||||
};
|
||||
|
||||
26
src/libs/popupManager.js
Normal file
26
src/libs/popupManager.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import ShadowDomManager from "./shadowDomManager";
|
||||
import { APP_CONSTS, EVENT_KISS, MSG_POPUP_TOGGLE } from "../config";
|
||||
import Action from "../views/Action";
|
||||
|
||||
export class PopupManager extends ShadowDomManager {
|
||||
constructor({ translator, processActions }) {
|
||||
super({
|
||||
id: APP_CONSTS.popupID,
|
||||
className: "notranslate",
|
||||
reactComponent: Action,
|
||||
props: { translator, processActions },
|
||||
});
|
||||
}
|
||||
|
||||
toggle(props) {
|
||||
if (this.isVisible) {
|
||||
document.dispatchEvent(
|
||||
new CustomEvent(EVENT_KISS, {
|
||||
detail: { action: MSG_POPUP_TOGGLE },
|
||||
})
|
||||
);
|
||||
} else {
|
||||
this.show(props || this._props);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,20 @@
|
||||
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,
|
||||
DEFAULT_RULE,
|
||||
GLOBLA_RULE,
|
||||
DEFAULT_SUBRULES_LIST,
|
||||
DEFAULT_OW_RULE,
|
||||
OPT_SPLIT_PARAGRAPH_ALL,
|
||||
OPT_HIGHLIGHT_WORDS_ALL,
|
||||
} 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匹配规则
|
||||
@@ -18,55 +22,87 @@ import { loadOrFetchSubRules } from "./subRules";
|
||||
* @param {string} href
|
||||
* @returns
|
||||
*/
|
||||
export const matchRule = async (
|
||||
rules,
|
||||
href,
|
||||
{
|
||||
injectRules = true,
|
||||
subrulesList = DEFAULT_SUBRULES_LIST,
|
||||
owSubrule = DEFAULT_OW_RULE,
|
||||
}
|
||||
) => {
|
||||
rules = [...rules];
|
||||
export const matchRule = async (href, { injectRules, subrulesList }) => {
|
||||
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;
|
||||
});
|
||||
|
||||
const subRules = (await loadOrFetchSubRules(selectedSub.url)).map(
|
||||
(item) => ({ ...item, ...mixRule })
|
||||
);
|
||||
const subRules = await loadOrFetchSubRules(selectedSub.url);
|
||||
rules.splice(-1, 0, ...subRules);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("[load injectRules]", err);
|
||||
kissLog("load injectRules", err);
|
||||
}
|
||||
}
|
||||
|
||||
const rule = rules.find((r) =>
|
||||
r.pattern.split(",").some((p) => isMatch(href, p.trim()))
|
||||
);
|
||||
const globalRule = rules.find((r) => r.pattern === GLOBAL_KEY) || GLOBLA_RULE;
|
||||
const globalRule = {
|
||||
...GLOBLA_RULE,
|
||||
...(rules.find((r) => r.pattern === GLOBAL_KEY) || {}),
|
||||
};
|
||||
if (!rule) {
|
||||
return globalRule;
|
||||
}
|
||||
|
||||
rule.selector = rule.selector?.trim() || globalRule.selector;
|
||||
if (rule.textStyle === GLOBAL_KEY) {
|
||||
[
|
||||
"selector",
|
||||
"keepSelector",
|
||||
"rootsSelector",
|
||||
"ignoreSelector",
|
||||
"terms",
|
||||
"aiTerms",
|
||||
"termsStyle",
|
||||
"highlightStyle",
|
||||
"selectStyle",
|
||||
"parentStyle",
|
||||
"grandStyle",
|
||||
"injectJs",
|
||||
// "injectCss",
|
||||
// "fixerSelector",
|
||||
"transStartHook",
|
||||
"transEndHook",
|
||||
// "transRemoveHook",
|
||||
].forEach((key) => {
|
||||
if (!rule[key]?.trim()) {
|
||||
rule[key] = globalRule[key];
|
||||
}
|
||||
});
|
||||
|
||||
[
|
||||
"apiSlug",
|
||||
"fromLang",
|
||||
"toLang",
|
||||
"transOpen",
|
||||
"transOnly",
|
||||
// "transTiming",
|
||||
"autoScan",
|
||||
"hasRichText",
|
||||
"hasShadowroot",
|
||||
"transTag",
|
||||
"transTitle",
|
||||
// "detectRemote",
|
||||
// "fixerFunc",
|
||||
"splitParagraph",
|
||||
"highlightWords",
|
||||
].forEach((key) => {
|
||||
if (!rule[key] || rule[key] === GLOBAL_KEY) {
|
||||
rule[key] = globalRule[key];
|
||||
}
|
||||
});
|
||||
|
||||
["splitLength"].forEach((key) => {
|
||||
if (!rule[key]) {
|
||||
rule[key] = globalRule[key];
|
||||
}
|
||||
});
|
||||
|
||||
// if (!rule.skipLangs || rule.skipLangs.length === 0) {
|
||||
// rule.skipLangs = globalRule.skipLangs;
|
||||
// }
|
||||
if (!rule.textStyle || rule.textStyle === GLOBAL_KEY) {
|
||||
rule.textStyle = globalRule.textStyle;
|
||||
rule.bgColor = globalRule.bgColor;
|
||||
rule.textDiyStyle = globalRule.textDiyStyle;
|
||||
@@ -74,11 +110,6 @@ export const matchRule = async (
|
||||
rule.bgColor = rule.bgColor?.trim() || globalRule.bgColor;
|
||||
rule.textDiyStyle = rule.textDiyStyle?.trim() || globalRule.textDiyStyle;
|
||||
}
|
||||
["translator", "fromLang", "toLang", "transOpen"].forEach((key) => {
|
||||
if (rule[key] === GLOBAL_KEY) {
|
||||
rule[key] = globalRule[key];
|
||||
}
|
||||
});
|
||||
|
||||
return rule;
|
||||
};
|
||||
@@ -112,25 +143,121 @@ export const checkRules = (rules) => {
|
||||
({
|
||||
pattern,
|
||||
selector,
|
||||
translator,
|
||||
keepSelector,
|
||||
rootsSelector,
|
||||
ignoreSelector,
|
||||
terms,
|
||||
aiTerms,
|
||||
termsStyle,
|
||||
highlightStyle,
|
||||
selectStyle,
|
||||
parentStyle,
|
||||
grandStyle,
|
||||
injectJs,
|
||||
// injectCss,
|
||||
apiSlug,
|
||||
fromLang,
|
||||
toLang,
|
||||
textStyle,
|
||||
transOpen,
|
||||
bgColor,
|
||||
textDiyStyle,
|
||||
transOnly,
|
||||
autoScan,
|
||||
hasRichText,
|
||||
hasShadowroot,
|
||||
// transTiming,
|
||||
transTag,
|
||||
transTitle,
|
||||
// detectRemote,
|
||||
// skipLangs,
|
||||
// fixerSelector,
|
||||
// fixerFunc,
|
||||
transStartHook,
|
||||
transEndHook,
|
||||
// transRemoveHook,
|
||||
splitParagraph,
|
||||
splitLength,
|
||||
highlightWords,
|
||||
}) => ({
|
||||
pattern: pattern.trim(),
|
||||
selector: type(selector) === "string" ? selector : "",
|
||||
keepSelector: type(keepSelector) === "string" ? keepSelector : "",
|
||||
rootsSelector: type(rootsSelector) === "string" ? rootsSelector : "",
|
||||
ignoreSelector: type(ignoreSelector) === "string" ? ignoreSelector : "",
|
||||
terms: type(terms) === "string" ? terms : "",
|
||||
aiTerms: type(aiTerms) === "string" ? aiTerms : "",
|
||||
termsStyle: type(termsStyle) === "string" ? termsStyle : "",
|
||||
highlightStyle: type(highlightStyle) === "string" ? highlightStyle : "",
|
||||
selectStyle: type(selectStyle) === "string" ? selectStyle : "",
|
||||
parentStyle: type(parentStyle) === "string" ? parentStyle : "",
|
||||
grandStyle: type(grandStyle) === "string" ? grandStyle : "",
|
||||
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),
|
||||
apiSlug:
|
||||
type(apiSlug) === "string" && apiSlug.trim() !== ""
|
||||
? apiSlug.trim()
|
||||
: GLOBAL_KEY,
|
||||
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),
|
||||
autoScan: matchValue([GLOBAL_KEY, "true", "false"], autoScan),
|
||||
hasRichText: matchValue([GLOBAL_KEY, "true", "false"], hasRichText),
|
||||
hasShadowroot: matchValue([GLOBAL_KEY, "true", "false"], hasShadowroot),
|
||||
// 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),
|
||||
splitParagraph: matchValue(
|
||||
[GLOBAL_KEY, ...OPT_SPLIT_PARAGRAPH_ALL],
|
||||
splitParagraph
|
||||
),
|
||||
splitLength: Number.isInteger(splitLength) ? splitLength : 0,
|
||||
highlightWords: matchValue(
|
||||
[GLOBAL_KEY, ...OPT_HIGHLIGHT_WORDS_ALL],
|
||||
highlightWords
|
||||
),
|
||||
})
|
||||
);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
/**
|
||||
* 保存或更新rule
|
||||
* @param {*} curRule
|
||||
*/
|
||||
export const saveRule = async (curRule) => {
|
||||
const rules = await getRulesWithDefault();
|
||||
|
||||
const index = rules.findIndex(
|
||||
(item) =>
|
||||
item.pattern !== GLOBAL_KEY && isMatch(curRule.pattern, item.pattern)
|
||||
);
|
||||
if (index !== -1) {
|
||||
const rule = rules.splice(index, 1)[0];
|
||||
curRule = { ...rule, ...curRule, pattern: rule.pattern };
|
||||
}
|
||||
|
||||
const newRule = {};
|
||||
Object.entries(GLOBLA_RULE).forEach(([key, val]) => {
|
||||
newRule[key] =
|
||||
!curRule[key] || curRule[key] === val ? DEFAULT_RULE[key] : curRule[key];
|
||||
});
|
||||
|
||||
rules.unshift(newRule);
|
||||
await setRules(rules);
|
||||
|
||||
trySyncRules();
|
||||
};
|
||||
|
||||
128
src/libs/shadowDomManager.js
Normal file
128
src/libs/shadowDomManager.js
Normal file
@@ -0,0 +1,128 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { CacheProvider } from "@emotion/react";
|
||||
import createCache from "@emotion/cache";
|
||||
import { logger } from "./log";
|
||||
|
||||
export default class ShadowDomManager {
|
||||
#hostElement = null;
|
||||
#reactRoot = null;
|
||||
#isVisible = false;
|
||||
#isProcessing = false;
|
||||
|
||||
_id;
|
||||
_className;
|
||||
_ReactComponent;
|
||||
_props;
|
||||
|
||||
constructor({ id, className = "", reactComponent, props = {} }) {
|
||||
if (!id || !reactComponent) {
|
||||
throw new Error("ID and a React Component must be provided.");
|
||||
}
|
||||
this._id = id;
|
||||
this._className = className;
|
||||
this._ReactComponent = reactComponent;
|
||||
this._props = props;
|
||||
}
|
||||
|
||||
get isVisible() {
|
||||
return this.#isVisible;
|
||||
}
|
||||
|
||||
show(props) {
|
||||
if (this.#isVisible || this.#isProcessing) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.#hostElement) {
|
||||
this.#isProcessing = true;
|
||||
try {
|
||||
this.#mount(props || this._props);
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to mount component with id "${this._id}":`, error);
|
||||
this.#isProcessing = false;
|
||||
return;
|
||||
} finally {
|
||||
this.#isProcessing = false;
|
||||
}
|
||||
}
|
||||
|
||||
this.#hostElement.style.display = "";
|
||||
this.#isVisible = true;
|
||||
}
|
||||
|
||||
hide() {
|
||||
if (!this.#isVisible || !this.#hostElement) {
|
||||
return;
|
||||
}
|
||||
this.#hostElement.style.display = "none";
|
||||
this.#isVisible = false;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (!this.#hostElement) {
|
||||
return;
|
||||
}
|
||||
this.#isProcessing = true;
|
||||
|
||||
if (this.#reactRoot) {
|
||||
this.#reactRoot.unmount();
|
||||
}
|
||||
|
||||
this.#hostElement.remove();
|
||||
|
||||
this.#hostElement = null;
|
||||
this.#reactRoot = null;
|
||||
this.#isVisible = false;
|
||||
this.#isProcessing = false;
|
||||
logger.info(`Component with id "${this._id}" has been destroyed.`);
|
||||
}
|
||||
|
||||
toggle(props) {
|
||||
if (this.#isVisible) {
|
||||
this.hide();
|
||||
} else {
|
||||
this.show(props || this._props);
|
||||
}
|
||||
}
|
||||
|
||||
#mount(props) {
|
||||
const host = document.createElement("div");
|
||||
host.id = this._id;
|
||||
if (this._className) {
|
||||
host.className = this._className;
|
||||
}
|
||||
host.style.display = "none";
|
||||
document.body.parentElement.appendChild(host);
|
||||
this.#hostElement = host;
|
||||
|
||||
const shadowContainer = host.attachShadow({ mode: "closed" });
|
||||
const emotionRoot = document.createElement("style");
|
||||
const appRoot = document.createElement("div");
|
||||
appRoot.className = `${this._id}_wrapper`;
|
||||
|
||||
shadowContainer.appendChild(emotionRoot);
|
||||
shadowContainer.appendChild(appRoot);
|
||||
|
||||
const cache = createCache({
|
||||
key: this._id,
|
||||
prepend: true,
|
||||
container: emotionRoot,
|
||||
});
|
||||
|
||||
const enhancedProps = {
|
||||
...props,
|
||||
onClose: this.hide.bind(this),
|
||||
};
|
||||
|
||||
const ComponentToRender = this._ReactComponent;
|
||||
this.#reactRoot = ReactDOM.createRoot(appRoot);
|
||||
this.#reactRoot.render(
|
||||
<React.StrictMode>
|
||||
<CacheProvider value={cache}>
|
||||
<ComponentToRender {...enhancedProps} />
|
||||
</CacheProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,67 +1,120 @@
|
||||
import { isSameSet } from "./utils";
|
||||
|
||||
/**
|
||||
* 键盘快捷键监听
|
||||
* @param {*} fn
|
||||
* @param {*} target
|
||||
* @param {*} timeout
|
||||
* @returns
|
||||
* 键盘快捷键监听器
|
||||
* @param {(pressedKeys: Set<string>, event: KeyboardEvent) => void} onKeyDown - Keydown 回调
|
||||
* @param {(pressedKeys: Set<string>, event: KeyboardEvent) => void} onKeyUp - Keyup 回调
|
||||
* @param {EventTarget} target - 监听的目标元素
|
||||
* @returns {() => void} - 用于注销监听的函数
|
||||
*/
|
||||
export const shortcutListener = (fn, target = document, timeout = 3000) => {
|
||||
const allkeys = new Set();
|
||||
const curkeys = new Set();
|
||||
let timer = null;
|
||||
export const shortcutListener = (
|
||||
onKeyDown = () => {},
|
||||
onKeyUp = () => {},
|
||||
target = document
|
||||
) => {
|
||||
const pressedKeys = new Set();
|
||||
|
||||
const handleKeydown = (e) => {
|
||||
timer && clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
allkeys.clear();
|
||||
curkeys.clear();
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}, timeout);
|
||||
|
||||
if (e.code) {
|
||||
allkeys.add(e.key);
|
||||
curkeys.add(e.key);
|
||||
fn([...curkeys], [...allkeys]);
|
||||
const handleKeyDown = (e) => {
|
||||
if (!e.code) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pressedKeys.has(e.code)) return;
|
||||
pressedKeys.add(e.code);
|
||||
onKeyDown(new Set(pressedKeys), e);
|
||||
};
|
||||
|
||||
const handleKeyup = (e) => {
|
||||
curkeys.delete(e.key);
|
||||
if (curkeys.size === 0) {
|
||||
fn([...curkeys], [...allkeys]);
|
||||
allkeys.clear();
|
||||
const handleKeyUp = (e) => {
|
||||
if (!e.code) {
|
||||
return;
|
||||
}
|
||||
|
||||
// onKeyUp 应该在 key 从集合中移除前触发,以便判断组合键
|
||||
onKeyUp(new Set(pressedKeys), e);
|
||||
pressedKeys.delete(e.code);
|
||||
};
|
||||
|
||||
target.addEventListener("keydown", handleKeydown);
|
||||
target.addEventListener("keyup", handleKeyup);
|
||||
const handleBlur = () => {
|
||||
pressedKeys.clear();
|
||||
};
|
||||
|
||||
target.addEventListener("keydown", handleKeyDown);
|
||||
target.addEventListener("keyup", handleKeyUp);
|
||||
window.addEventListener("blur", handleBlur);
|
||||
|
||||
return () => {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
target.removeEventListener("keydown", handleKeydown);
|
||||
target.removeEventListener("keyup", handleKeyup);
|
||||
target.removeEventListener("keydown", handleKeyDown);
|
||||
target.removeEventListener("keyup", handleKeyUp);
|
||||
window.removeEventListener("blur", handleBlur);
|
||||
pressedKeys.clear();
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 注册键盘快捷键
|
||||
* @param {*} targetKeys
|
||||
* @param {*} fn
|
||||
* @param {*} target
|
||||
* @returns
|
||||
* @param {string[]} targetKeys - 目标快捷键数组
|
||||
* @param {() => void} fn - 匹配成功后执行的回调
|
||||
* @param {EventTarget} target - 监听目标
|
||||
* @returns {() => void} - 注销函数
|
||||
*/
|
||||
export const shortcutRegister = (targetKeys = [], fn, target = document) => {
|
||||
return shortcutListener((curkeys) => {
|
||||
if (
|
||||
targetKeys.length > 0 &&
|
||||
isSameSet(new Set(targetKeys), new Set(curkeys))
|
||||
) {
|
||||
if (targetKeys.length === 0) return () => {};
|
||||
|
||||
const targetKeySet = new Set(targetKeys);
|
||||
const onKeyDown = (pressedKeys, event) => {
|
||||
if (isSameSet(targetKeySet, pressedKeys)) {
|
||||
// event.preventDefault(); // 阻止浏览器的默认行为
|
||||
// event.stopPropagation(); // 阻止事件继续(向父元素)冒泡
|
||||
fn();
|
||||
}
|
||||
}, target);
|
||||
};
|
||||
const onKeyUp = () => {};
|
||||
|
||||
return shortcutListener(onKeyDown, onKeyUp, target);
|
||||
};
|
||||
|
||||
/**
|
||||
* 高阶函数:为目标函数增加计次和超时重置功能
|
||||
* @param {() => void} fn - 需要被包装的函数
|
||||
* @param {number} step - 需要触发的次数
|
||||
* @param {number} timeout - 超时毫秒数
|
||||
* @returns {() => void} - 包装后的新函数
|
||||
*/
|
||||
const withStepCounter = (fn, step, timeout) => {
|
||||
let count = 0;
|
||||
let timer = null;
|
||||
|
||||
return () => {
|
||||
timer && clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
count = 0;
|
||||
}, timeout);
|
||||
|
||||
count++;
|
||||
if (count === step) {
|
||||
count = 0;
|
||||
clearTimeout(timer);
|
||||
fn();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 注册连续快捷键
|
||||
* @param {string[]} targetKeys - 目标快捷键数组
|
||||
* @param {() => void} fn - 成功回调
|
||||
* @param {number} step - 连续触发次数
|
||||
* @param {number} timeout - 每次触发的间隔超时
|
||||
* @param {EventTarget} target - 监听目标
|
||||
* @returns {() => void} - 注销函数
|
||||
*/
|
||||
export const stepShortcutRegister = (
|
||||
targetKeys = [],
|
||||
fn,
|
||||
step = 2,
|
||||
timeout = 500,
|
||||
target = document
|
||||
) => {
|
||||
const steppedFn = withStepCounter(fn, step, timeout);
|
||||
return shortcutRegister(targetKeys, steppedFn, target);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import {
|
||||
STOKEY_SETTING,
|
||||
STOKEY_SETTING_OLD,
|
||||
STOKEY_RULES,
|
||||
STOKEY_RULES_OLD,
|
||||
STOKEY_WORDS,
|
||||
STOKEY_FAB,
|
||||
STOKEY_TRANBOX,
|
||||
STOKEY_SYNC,
|
||||
STOKEY_MSAUTH,
|
||||
STOKEY_BDAUTH,
|
||||
STOKEY_RULESCACHE_PREFIX,
|
||||
STOKEY_WEBFIXCACHE_PREFIX,
|
||||
DEFAULT_SETTING,
|
||||
DEFAULT_RULES,
|
||||
DEFAULT_SYNC,
|
||||
@@ -13,6 +17,8 @@ import {
|
||||
} from "../config";
|
||||
import { isExt, isGm } from "./client";
|
||||
import { browser } from "./browser";
|
||||
import { kissLog } from "./log";
|
||||
import { debounce } from "./utils";
|
||||
|
||||
async function set(key, val) {
|
||||
if (isExt) {
|
||||
@@ -57,7 +63,13 @@ async function trySetObj(key, obj) {
|
||||
|
||||
async function getObj(key) {
|
||||
const val = await get(key);
|
||||
return val && JSON.parse(val);
|
||||
if (val === null || val === undefined) return null;
|
||||
try {
|
||||
return JSON.parse(val);
|
||||
} catch (err) {
|
||||
kissLog("parse json in storage err: ", key);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function putObj(key, obj) {
|
||||
@@ -83,19 +95,30 @@ export const storage = {
|
||||
* 设置信息
|
||||
*/
|
||||
export const getSetting = () => getObj(STOKEY_SETTING);
|
||||
export const getSettingWithDefault = async () =>
|
||||
(await getSetting()) || DEFAULT_SETTING;
|
||||
export const getSettingOld = () => getObj(STOKEY_SETTING_OLD);
|
||||
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 putSetting = (obj) => putObj(STOKEY_SETTING, obj);
|
||||
|
||||
/**
|
||||
* 规则列表
|
||||
*/
|
||||
export const getRules = () => getObj(STOKEY_RULES);
|
||||
export const getRulesOld = () => getObj(STOKEY_RULES_OLD);
|
||||
export const getRulesWithDefault = async () =>
|
||||
(await getRules()) || DEFAULT_RULES;
|
||||
export const setRules = (val) => setObj(STOKEY_RULES, val);
|
||||
|
||||
/**
|
||||
* 词汇列表
|
||||
*/
|
||||
export const getWords = () => getObj(STOKEY_WORDS);
|
||||
export const getWordsWithDefault = async () => (await getWords()) || {};
|
||||
export const setWords = (val) => setObj(STOKEY_WORDS, val);
|
||||
|
||||
/**
|
||||
* 订阅规则
|
||||
*/
|
||||
@@ -105,27 +128,33 @@ export const delSubRules = (url) => del(STOKEY_RULESCACHE_PREFIX + url);
|
||||
export const setSubRules = (url, val) =>
|
||||
setObj(STOKEY_RULESCACHE_PREFIX + url, val);
|
||||
|
||||
/**
|
||||
* 修复站点
|
||||
*/
|
||||
export const getWebfix = (url) => getObj(STOKEY_WEBFIXCACHE_PREFIX + url);
|
||||
export const getWebfixWithDefault = async () => (await getWebfix()) || [];
|
||||
export const setWebfix = (url, val) =>
|
||||
setObj(STOKEY_WEBFIXCACHE_PREFIX + url, val);
|
||||
|
||||
/**
|
||||
* fab位置
|
||||
*/
|
||||
export const getFab = () => getObj(STOKEY_FAB);
|
||||
export const getFabWithDefault = async () => (await getFab()) || {};
|
||||
export const setFab = (obj) => setObj(STOKEY_FAB, obj);
|
||||
export const putFab = (obj) => putObj(STOKEY_FAB, obj);
|
||||
|
||||
/**
|
||||
* tranbox位置大小
|
||||
*/
|
||||
export const getTranBox = () => getObj(STOKEY_TRANBOX);
|
||||
export const putTranBox = (obj) => putObj(STOKEY_TRANBOX, obj);
|
||||
export const debouncePutTranBox = debounce(putTranBox, 300);
|
||||
|
||||
/**
|
||||
* 数据同步
|
||||
*/
|
||||
export const getSync = () => getObj(STOKEY_SYNC);
|
||||
export const getSyncWithDefault = async () => (await getSync()) || DEFAULT_SYNC;
|
||||
export const updateSync = (obj) => putObj(STOKEY_SYNC, obj);
|
||||
export const putSync = (obj) => putObj(STOKEY_SYNC, obj);
|
||||
export const putSyncMeta = async (key) => {
|
||||
const { syncMeta = {} } = await getSyncWithDefault();
|
||||
syncMeta[key] = { ...(syncMeta[key] || {}), updateAt: Date.now() };
|
||||
await putSync({ syncMeta });
|
||||
};
|
||||
export const debounceSyncMeta = debounce(putSyncMeta, 300);
|
||||
|
||||
/**
|
||||
* ms auth
|
||||
@@ -133,6 +162,12 @@ export const updateSync = (obj) => putObj(STOKEY_SYNC, obj);
|
||||
export const getMsauth = () => getObj(STOKEY_MSAUTH);
|
||||
export const setMsauth = (val) => setObj(STOKEY_MSAUTH, val);
|
||||
|
||||
/**
|
||||
* baidu auth
|
||||
*/
|
||||
export const getBdauth = () => getObj(STOKEY_BDAUTH);
|
||||
export const setBdauth = (val) => setObj(STOKEY_BDAUTH, val);
|
||||
|
||||
/**
|
||||
* 存入默认数据
|
||||
*/
|
||||
@@ -146,6 +181,6 @@ export const tryInitDefaultData = async () => {
|
||||
BUILTIN_RULES
|
||||
);
|
||||
} catch (err) {
|
||||
console.log("[init default]", err);
|
||||
kissLog("init default", err);
|
||||
}
|
||||
};
|
||||
|
||||
166
src/libs/style.js
Normal file
166
src/libs/style.js
Normal file
@@ -0,0 +1,166 @@
|
||||
import { css, keyframes } from "@emotion/css";
|
||||
import {
|
||||
OPT_STYLE_NONE,
|
||||
OPT_STYLE_LINE,
|
||||
OPT_STYLE_DOTLINE,
|
||||
OPT_STYLE_DASHLINE,
|
||||
OPT_STYLE_WAVYLINE,
|
||||
OPT_STYLE_DASHBOX,
|
||||
OPT_STYLE_FUZZY,
|
||||
OPT_STYLE_HIGHLIGHT,
|
||||
OPT_STYLE_BLOCKQUOTE,
|
||||
OPT_STYLE_GRADIENT,
|
||||
OPT_STYLE_BLINK,
|
||||
OPT_STYLE_GLOW,
|
||||
OPT_STYLE_DIY,
|
||||
DEFAULT_DIY_STYLE,
|
||||
DEFAULT_COLOR,
|
||||
} from "../config";
|
||||
|
||||
const gradientFlow = keyframes`
|
||||
to {
|
||||
background-position: 200% center;
|
||||
}
|
||||
`;
|
||||
|
||||
const blink = keyframes`
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const glow = keyframes`
|
||||
from {
|
||||
text-shadow: 0 0 10px #fff,
|
||||
0 0 20px #fff,
|
||||
0 0 30px #0073e6,
|
||||
0 0 40px #0073e6;
|
||||
}
|
||||
to {
|
||||
text-shadow: 0 0 20px #fff,
|
||||
0 0 30px #ff4da6,
|
||||
0 0 40px #ff4da6,
|
||||
0 0 50px #ff4da6;
|
||||
}
|
||||
`;
|
||||
|
||||
const genLineStyle = (style, color) => `
|
||||
text-decoration-line: underline;
|
||||
text-decoration-style: ${style};
|
||||
text-decoration-color: ${color};
|
||||
text-decoration-thickness: 2px;
|
||||
text-underline-offset: 0.3em;
|
||||
-webkit-text-decoration-line: underline;
|
||||
-webkit-text-decoration-style: ${style};
|
||||
-webkit-text-decoration-color: ${color};
|
||||
-webkit-text-decoration-thickness: 2px;
|
||||
-webkit-text-underline-offset: 0.3em;
|
||||
|
||||
/* opacity: 0.8;
|
||||
-webkit-opacity: 0.8;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
-webkit-opacity: 1;
|
||||
} */
|
||||
`;
|
||||
|
||||
const genStyles = ({
|
||||
textDiyStyle = DEFAULT_DIY_STYLE,
|
||||
bgColor = DEFAULT_COLOR,
|
||||
} = {}) => ({
|
||||
// 无样式
|
||||
[OPT_STYLE_NONE]: ``,
|
||||
// 下划线
|
||||
[OPT_STYLE_LINE]: genLineStyle("solid", bgColor),
|
||||
// 点状线
|
||||
[OPT_STYLE_DOTLINE]: genLineStyle("dotted", bgColor),
|
||||
// 虚线
|
||||
[OPT_STYLE_DASHLINE]: genLineStyle("dashed", bgColor),
|
||||
// 波浪线
|
||||
[OPT_STYLE_WAVYLINE]: genLineStyle("wavy", bgColor),
|
||||
// 虚线框
|
||||
[OPT_STYLE_DASHBOX]: `
|
||||
border: 2px dashed ${bgColor || DEFAULT_COLOR};
|
||||
display: block;
|
||||
padding: 0.2em 0.4em;
|
||||
box-sizing: border-box;
|
||||
`,
|
||||
// 模糊
|
||||
[OPT_STYLE_FUZZY]: `
|
||||
filter: blur(0.2em);
|
||||
-webkit-filter: blur(0.2em);
|
||||
&:hover {
|
||||
filter: none;
|
||||
-webkit-filter: none;
|
||||
}
|
||||
`,
|
||||
// 高亮
|
||||
[OPT_STYLE_HIGHLIGHT]: `
|
||||
color: #fff;
|
||||
background-color: ${bgColor || DEFAULT_COLOR};
|
||||
`,
|
||||
// 引用
|
||||
[OPT_STYLE_BLOCKQUOTE]: `
|
||||
opacity: 0.8;
|
||||
-webkit-opacity: 0.8;
|
||||
display: block;
|
||||
padding: 0.25em 0.5em;
|
||||
border-left: 0.5em solid ${bgColor || DEFAULT_COLOR};
|
||||
background: rgb(32, 156, 238, 0.2);
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
-webkit-opacity: 1;
|
||||
}
|
||||
`,
|
||||
// 渐变
|
||||
[OPT_STYLE_GRADIENT]: `
|
||||
background-image: linear-gradient(
|
||||
90deg,
|
||||
#3b82f6,
|
||||
#9333ea,
|
||||
#ec4899,
|
||||
#3b82f6
|
||||
);
|
||||
background-size: 200% auto;
|
||||
color: transparent;
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
animation: ${gradientFlow} 4s linear infinite;
|
||||
`,
|
||||
// 闪现
|
||||
[OPT_STYLE_BLINK]: `
|
||||
animation: ${blink} 1s infinite;
|
||||
`,
|
||||
// 发光
|
||||
[OPT_STYLE_GLOW]: `
|
||||
animation: ${glow} 2s ease-in-out infinite alternate;
|
||||
`,
|
||||
// 自定义
|
||||
[OPT_STYLE_DIY]: `
|
||||
${textDiyStyle}
|
||||
`,
|
||||
});
|
||||
|
||||
export const genTextClass = ({ textDiyStyle, bgColor = DEFAULT_COLOR }) => {
|
||||
const styles = genStyles({ textDiyStyle, bgColor });
|
||||
const textClass = {};
|
||||
let textStyles = "";
|
||||
Object.entries(styles).forEach(([k, v]) => {
|
||||
textClass[k] = css`
|
||||
${v}
|
||||
`;
|
||||
});
|
||||
Object.entries(styles).forEach(([k, v]) => {
|
||||
textStyles += `
|
||||
.${textClass[k]} {
|
||||
${v}
|
||||
}
|
||||
`;
|
||||
});
|
||||
return [textClass, textStyles];
|
||||
};
|
||||
|
||||
export const defaultStyles = genStyles();
|
||||
@@ -1,22 +1,32 @@
|
||||
import { GLOBAL_KEY } from "../config";
|
||||
import {
|
||||
getSyncWithDefault,
|
||||
updateSync,
|
||||
putSync,
|
||||
setSubRules,
|
||||
getSubRules,
|
||||
updateSetting,
|
||||
} from "./storage";
|
||||
import { apiFetch } from "../apis";
|
||||
import { checkRules } from "./rules";
|
||||
import { isAllchar } from "./utils";
|
||||
import { kissLog } from "./log";
|
||||
|
||||
/**
|
||||
* 更新缓存同步时间
|
||||
* @param {*} url
|
||||
*/
|
||||
const updateSyncDataCache = async (url) => {
|
||||
const { dataCaches = {} } = await getSyncWithDefault();
|
||||
dataCaches[url] = Date.now();
|
||||
await putSync({ dataCaches });
|
||||
};
|
||||
|
||||
/**
|
||||
* 同步订阅规则
|
||||
* @param {*} url
|
||||
* @returns
|
||||
*/
|
||||
export const syncSubRules = async (url, isBg = false) => {
|
||||
const res = await apiFetch(url, isBg);
|
||||
export const syncSubRules = async (url) => {
|
||||
const res = await apiFetch(url);
|
||||
const rules = checkRules(res).filter(
|
||||
({ pattern }) => !isAllchar(pattern, GLOBAL_KEY)
|
||||
);
|
||||
@@ -31,12 +41,13 @@ export const syncSubRules = async (url, isBg = false) => {
|
||||
* @param {*} url
|
||||
* @returns
|
||||
*/
|
||||
export const syncAllSubRules = async (subrulesList, isBg = false) => {
|
||||
for (let subrules of subrulesList) {
|
||||
export const syncAllSubRules = async (subrulesList) => {
|
||||
for (const subrules of subrulesList) {
|
||||
try {
|
||||
await syncSubRules(subrules.url, isBg);
|
||||
await syncSubRules(subrules.url);
|
||||
await updateSyncDataCache(subrules.url);
|
||||
} catch (err) {
|
||||
console.log(`[sync subrule error]: ${subrules.url}`, err);
|
||||
kissLog(`sync subrule error: ${subrules.url}`, err);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -46,21 +57,18 @@ export const syncAllSubRules = async (subrulesList, isBg = false) => {
|
||||
* @param {*} url
|
||||
* @returns
|
||||
*/
|
||||
export const trySyncAllSubRules = async ({ subrulesList }, isBg = false) => {
|
||||
export const trySyncAllSubRules = async ({ subrulesList }) => {
|
||||
try {
|
||||
const { subRulesSyncAt } = await getSyncWithDefault();
|
||||
const now = Date.now();
|
||||
const interval = 24 * 60 * 60 * 1000; // 间隔一天
|
||||
if (now - subRulesSyncAt > interval) {
|
||||
await syncAllSubRules(subrulesList, isBg);
|
||||
await updateSync({ subRulesSyncAt: now });
|
||||
// 同步订阅规则
|
||||
await syncAllSubRules(subrulesList);
|
||||
await putSync({ subRulesSyncAt: now });
|
||||
}
|
||||
subrulesList.forEach((item) => {
|
||||
item.syncAt = now;
|
||||
});
|
||||
await updateSetting({ subrulesList });
|
||||
} catch (err) {
|
||||
console.log("[try sync all subrules]", err);
|
||||
kissLog("try sync all subrules", err);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -70,9 +78,10 @@ export const trySyncAllSubRules = async ({ subrulesList }, isBg = false) => {
|
||||
* @returns
|
||||
*/
|
||||
export const loadOrFetchSubRules = async (url) => {
|
||||
const rules = await getSubRules(url);
|
||||
if (rules?.length) {
|
||||
return rules;
|
||||
let rules = await getSubRules(url);
|
||||
if (!rules || rules.length === 0) {
|
||||
rules = await syncSubRules(url);
|
||||
await updateSyncDataCache(url);
|
||||
}
|
||||
return syncSubRules(url);
|
||||
return rules || [];
|
||||
};
|
||||
|
||||
105
src/libs/svg.js
Normal file
105
src/libs/svg.js
Normal file
@@ -0,0 +1,105 @@
|
||||
export const loadingSvg = `<svg viewBox="-20 0 100 100"
|
||||
style="display: inline-block; width: 1em; height: 1em; vertical-align: middle;">
|
||||
<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>
|
||||
`;
|
||||
|
||||
function createSVGElement(tag, attributes) {
|
||||
const svgNS = "http://www.w3.org/2000/svg";
|
||||
const el = document.createElementNS(svgNS, tag);
|
||||
for (const key in attributes) {
|
||||
el.setAttribute(key, attributes[key]);
|
||||
}
|
||||
return el;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建loding动画
|
||||
* @returns
|
||||
*/
|
||||
export function createLoadingSVG() {
|
||||
const svg = createSVGElement("svg", {
|
||||
viewBox: "-20 0 100 100",
|
||||
style:
|
||||
"display: inline-block; width: 1em; height: 1em; vertical-align: middle;",
|
||||
});
|
||||
|
||||
const circleData = [
|
||||
{ cx: "6", begin: "0.1", values: "0 15 ; 0 -15; 0 15" },
|
||||
{ cx: "30", begin: "0.2", values: "0 10 ; 0 -10; 0 10" },
|
||||
{ cx: "54", begin: "0.3", values: "0 5 ; 0 -5; 0 5" },
|
||||
];
|
||||
|
||||
circleData.forEach((data) => {
|
||||
const circle = createSVGElement("circle", {
|
||||
fill: "#209CEE",
|
||||
stroke: "none",
|
||||
cx: data.cx,
|
||||
cy: "50",
|
||||
r: "6",
|
||||
});
|
||||
const animation = createSVGElement("animateTransform", {
|
||||
attributeName: "transform",
|
||||
dur: "1s",
|
||||
type: "translate",
|
||||
values: data.values,
|
||||
repeatCount: "indefinite",
|
||||
begin: data.begin,
|
||||
});
|
||||
circle.appendChild(animation);
|
||||
svg.appendChild(circle);
|
||||
});
|
||||
|
||||
return svg;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建logo
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
export function createLogoSVG({
|
||||
width = "24",
|
||||
height = "24",
|
||||
viewBox = "-5 -5 40 40",
|
||||
isSelected = false,
|
||||
} = {}) {
|
||||
const svg = createSVGElement("svg", {
|
||||
xmlns: "http://www.w3.org/2000/svg",
|
||||
width,
|
||||
height,
|
||||
viewBox,
|
||||
version: "1.1",
|
||||
});
|
||||
|
||||
const primaryColor = "#209CEE";
|
||||
const secondaryColor = "#E9F5FD";
|
||||
|
||||
const path1Fill = isSelected ? primaryColor : secondaryColor;
|
||||
const path2Fill = isSelected ? secondaryColor : primaryColor;
|
||||
|
||||
const path1 = createSVGElement("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: path1Fill,
|
||||
transform: "translate(0,0)",
|
||||
});
|
||||
|
||||
const path2 = createSVGElement("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: path2Fill,
|
||||
transform: "translate(4,5)",
|
||||
});
|
||||
|
||||
svg.appendChild(path1);
|
||||
svg.appendChild(path2);
|
||||
|
||||
return svg;
|
||||
}
|
||||
215
src/libs/sync.js
215
src/libs/sync.js
@@ -1,58 +1,133 @@
|
||||
import {
|
||||
APP_LCNAME,
|
||||
KV_SETTING_KEY,
|
||||
KV_RULES_KEY,
|
||||
KV_WORDS_KEY,
|
||||
KV_RULES_SHARE_KEY,
|
||||
KV_SALT_SHARE,
|
||||
OPT_SYNCTYPE_WEBDAV,
|
||||
} from "../config";
|
||||
import {
|
||||
getSyncWithDefault,
|
||||
updateSync,
|
||||
putSync,
|
||||
getSettingWithDefault,
|
||||
getRulesWithDefault,
|
||||
getWordsWithDefault,
|
||||
setSetting,
|
||||
setRules,
|
||||
setWords,
|
||||
} from "./storage";
|
||||
import { apiSyncData } from "../apis";
|
||||
import { sha256 } from "./utils";
|
||||
import { sha256, removeEndchar } from "./utils";
|
||||
import { createClient, getPatcher } from "webdav";
|
||||
import { fetchPatcher } from "./fetch";
|
||||
import { kissLog } from "./log";
|
||||
|
||||
getPatcher().patch("request", (opts) => {
|
||||
return fetchPatcher(opts.url, {
|
||||
method: opts.method,
|
||||
headers: opts.headers,
|
||||
body: opts.data,
|
||||
});
|
||||
});
|
||||
|
||||
const syncByWebdav = async (data, { syncUrl, syncUser, syncKey }) => {
|
||||
const client = createClient(syncUrl, {
|
||||
username: syncUser,
|
||||
password: syncKey,
|
||||
});
|
||||
const pathname = `/${APP_LCNAME}`;
|
||||
const filename = `/${APP_LCNAME}/${data.key}`;
|
||||
|
||||
if ((await client.exists(pathname)) === false) {
|
||||
await client.createDirectory(pathname);
|
||||
}
|
||||
|
||||
const isExist = await client.exists(filename);
|
||||
if (isExist) {
|
||||
const cont = await client.getFileContents(filename, { format: "text" });
|
||||
const webData = JSON.parse(cont);
|
||||
if (webData.updateAt >= data.updateAt) {
|
||||
return webData;
|
||||
}
|
||||
}
|
||||
|
||||
await client.putFileContents(filename, JSON.stringify(data, null, 2));
|
||||
return data;
|
||||
};
|
||||
|
||||
const syncByWorker = async (data, { syncUrl, syncKey }) => {
|
||||
syncUrl = removeEndchar(syncUrl, "/");
|
||||
return await apiSyncData(`${syncUrl}/sync`, syncKey, data);
|
||||
};
|
||||
|
||||
export const syncData = async (key, value) => {
|
||||
const {
|
||||
syncType,
|
||||
syncUrl,
|
||||
syncUser,
|
||||
syncKey,
|
||||
syncMeta = {},
|
||||
} = await getSyncWithDefault();
|
||||
if (!syncUrl || !syncKey || (syncType === OPT_SYNCTYPE_WEBDAV && !syncUser)) {
|
||||
// throw new Error("sync args err");
|
||||
return;
|
||||
}
|
||||
|
||||
let { updateAt = 0, syncAt = 0 } = syncMeta[key] || {};
|
||||
if (syncAt === 0) {
|
||||
updateAt = 0; // 没有同步过,更新时间置零
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (!res) {
|
||||
throw new Error("sync data got err", key);
|
||||
}
|
||||
|
||||
const newVal = JSON.parse(res.value);
|
||||
const isNew = res.updateAt > updateAt;
|
||||
|
||||
syncMeta[key] = {
|
||||
updateAt: res.updateAt,
|
||||
syncAt: Date.now(),
|
||||
};
|
||||
await putSync({ syncMeta });
|
||||
|
||||
return { value: newVal, isNew };
|
||||
};
|
||||
|
||||
/**
|
||||
* 同步设置
|
||||
* @returns
|
||||
*/
|
||||
const syncSetting = async (isBg = false) => {
|
||||
const { syncUrl, syncKey, settingUpdateAt = 0 } = await getSyncWithDefault();
|
||||
if (!syncUrl || !syncKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const setting = await getSettingWithDefault();
|
||||
const res = await apiSyncData(
|
||||
syncUrl,
|
||||
syncKey,
|
||||
{
|
||||
key: KV_SETTING_KEY,
|
||||
value: setting,
|
||||
updateAt: settingUpdateAt,
|
||||
},
|
||||
isBg
|
||||
);
|
||||
|
||||
if (res.updateAt > settingUpdateAt) {
|
||||
const syncSetting = async () => {
|
||||
const value = await getSettingWithDefault();
|
||||
const res = await syncData(KV_SETTING_KEY, value);
|
||||
if (res?.isNew) {
|
||||
await setSetting(res.value);
|
||||
}
|
||||
await updateSync({
|
||||
settingUpdateAt: res.updateAt,
|
||||
settingSyncAt: Date.now(),
|
||||
});
|
||||
|
||||
return res.value;
|
||||
};
|
||||
|
||||
export const trySyncSetting = async (isBg = false) => {
|
||||
export const trySyncSetting = async () => {
|
||||
try {
|
||||
return await syncSetting(isBg);
|
||||
await syncSetting();
|
||||
} catch (err) {
|
||||
console.log("[sync setting]", err);
|
||||
kissLog("sync setting", err.message);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -60,40 +135,39 @@ export const trySyncSetting = async (isBg = false) => {
|
||||
* 同步规则
|
||||
* @returns
|
||||
*/
|
||||
const syncRules = async (isBg = false) => {
|
||||
const { syncUrl, syncKey, rulesUpdateAt } = await getSyncWithDefault();
|
||||
if (!syncUrl || !syncKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rules = await getRulesWithDefault();
|
||||
const res = await apiSyncData(
|
||||
syncUrl,
|
||||
syncKey,
|
||||
{
|
||||
key: KV_RULES_KEY,
|
||||
value: rules,
|
||||
updateAt: rulesUpdateAt,
|
||||
},
|
||||
isBg
|
||||
);
|
||||
|
||||
if (res.updateAt > rulesUpdateAt) {
|
||||
const syncRules = async () => {
|
||||
const value = await getRulesWithDefault();
|
||||
const res = await syncData(KV_RULES_KEY, value);
|
||||
if (res?.isNew) {
|
||||
await setRules(res.value);
|
||||
}
|
||||
await updateSync({
|
||||
rulesUpdateAt: res.updateAt,
|
||||
rulesSyncAt: Date.now(),
|
||||
});
|
||||
|
||||
return res.value;
|
||||
};
|
||||
|
||||
export const trySyncRules = async (isBg = false) => {
|
||||
export const trySyncRules = async () => {
|
||||
try {
|
||||
return await syncRules(isBg);
|
||||
await syncRules();
|
||||
} catch (err) {
|
||||
console.log("[sync user rules]", err);
|
||||
kissLog("sync user rules", err.message);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 同步词汇
|
||||
* @returns
|
||||
*/
|
||||
const syncWords = async () => {
|
||||
const value = await getWordsWithDefault();
|
||||
const res = await syncData(KV_WORDS_KEY, value);
|
||||
if (res?.isNew) {
|
||||
await setWords(res.value);
|
||||
}
|
||||
};
|
||||
|
||||
export const trySyncWords = async () => {
|
||||
try {
|
||||
await syncWords();
|
||||
} catch (err) {
|
||||
kissLog("sync fav words", err.message);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -103,13 +177,18 @@ export const trySyncRules = async (isBg = false) => {
|
||||
* @returns
|
||||
*/
|
||||
export const syncShareRules = async ({ rules, syncUrl, syncKey }) => {
|
||||
await apiSyncData(syncUrl, syncKey, {
|
||||
const data = {
|
||||
key: KV_RULES_SHARE_KEY,
|
||||
value: rules,
|
||||
value: JSON.stringify(rules, null, 2),
|
||||
updateAt: Date.now(),
|
||||
});
|
||||
};
|
||||
const args = {
|
||||
syncUrl,
|
||||
syncKey,
|
||||
};
|
||||
await syncByWorker(data, args);
|
||||
const psk = await sha256(syncKey, KV_SALT_SHARE);
|
||||
const shareUrl = `${syncUrl}?psk=${psk}`;
|
||||
const shareUrl = `${syncUrl}/rules?psk=${psk}`;
|
||||
return shareUrl;
|
||||
};
|
||||
|
||||
@@ -117,10 +196,14 @@ export const syncShareRules = async ({ rules, syncUrl, syncKey }) => {
|
||||
* 同步个人设置和规则
|
||||
* @returns
|
||||
*/
|
||||
export const syncSettingAndRules = async (isBg = false) => {
|
||||
return [await syncSetting(isBg), await syncRules(isBg)];
|
||||
export const syncSettingAndRules = async () => {
|
||||
await syncSetting();
|
||||
await syncRules();
|
||||
await syncWords();
|
||||
};
|
||||
|
||||
export const trySyncSettingAndRules = async (isBg = false) => {
|
||||
return [await trySyncSetting(isBg), await trySyncRules(isBg)];
|
||||
export const trySyncSettingAndRules = async () => {
|
||||
await trySyncSetting();
|
||||
await trySyncRules();
|
||||
await trySyncWords();
|
||||
};
|
||||
|
||||
47
src/libs/touch.js
Normal file
47
src/libs/touch.js
Normal file
@@ -0,0 +1,47 @@
|
||||
export function touchTapListener(fn, options = {}) {
|
||||
const config = {
|
||||
taps: 2,
|
||||
fingers: 1,
|
||||
delay: 300,
|
||||
...options,
|
||||
};
|
||||
|
||||
let maxTouches = 0;
|
||||
let tapCount = 0;
|
||||
let tapTimer = null;
|
||||
|
||||
const handleTouchStart = (e) => {
|
||||
maxTouches = Math.max(maxTouches, e.touches.length);
|
||||
};
|
||||
|
||||
const handleTouchend = (e) => {
|
||||
if (e.touches.length === 0) {
|
||||
if (maxTouches === config.fingers) {
|
||||
tapCount++;
|
||||
clearTimeout(tapTimer);
|
||||
|
||||
if (tapCount === config.taps) {
|
||||
fn(e);
|
||||
tapCount = 0;
|
||||
} else {
|
||||
tapTimer = setTimeout(() => {
|
||||
tapCount = 0;
|
||||
}, config.delay);
|
||||
}
|
||||
} else {
|
||||
tapCount = 0;
|
||||
clearTimeout(tapTimer);
|
||||
}
|
||||
maxTouches = 0;
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("touchstart", handleTouchStart, { passive: true });
|
||||
document.addEventListener("touchend", handleTouchend, { passive: true });
|
||||
|
||||
return () => {
|
||||
clearTimeout(tapTimer);
|
||||
document.removeEventListener("touchstart", handleTouchStart);
|
||||
document.removeEventListener("touchend", handleTouchend);
|
||||
};
|
||||
}
|
||||
96
src/libs/tranbox.js
Normal file
96
src/libs/tranbox.js
Normal file
@@ -0,0 +1,96 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import createCache from "@emotion/cache";
|
||||
import { CacheProvider } from "@emotion/react";
|
||||
import Slection from "../views/Selection";
|
||||
import { DEFAULT_TRANBOX_SETTING, APP_CONSTS } from "../config";
|
||||
|
||||
export class TransboxManager {
|
||||
#container = null;
|
||||
#reactRoot = null;
|
||||
#shadowContainer = null;
|
||||
#props = {};
|
||||
|
||||
constructor(initialProps = {}) {
|
||||
this.#props = initialProps;
|
||||
|
||||
const { tranboxSetting = DEFAULT_TRANBOX_SETTING } = this.#props;
|
||||
if (tranboxSetting?.transOpen) {
|
||||
this.enable();
|
||||
}
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
return (
|
||||
!!this.#container && document.body.parentElement.contains(this.#container)
|
||||
);
|
||||
}
|
||||
|
||||
enable() {
|
||||
if (!this.isEnabled()) {
|
||||
this.#container = document.createElement("div");
|
||||
this.#container.id = APP_CONSTS.boxID;
|
||||
this.#container.className = "notranslate";
|
||||
this.#container.style.cssText =
|
||||
"font-size: 0; width: 0; height: 0; border: 0; padding: 0; margin: 0;";
|
||||
document.body.parentElement.appendChild(this.#container);
|
||||
|
||||
this.#shadowContainer = this.#container.attachShadow({ mode: "closed" });
|
||||
const emotionRoot = document.createElement("style");
|
||||
const shadowRootElement = document.createElement("div");
|
||||
shadowRootElement.className = `${APP_CONSTS.boxID}_warpper notranslate`;
|
||||
this.#shadowContainer.appendChild(emotionRoot);
|
||||
this.#shadowContainer.appendChild(shadowRootElement);
|
||||
const cache = createCache({
|
||||
key: APP_CONSTS.boxID,
|
||||
prepend: true,
|
||||
container: emotionRoot,
|
||||
});
|
||||
|
||||
this.#reactRoot = ReactDOM.createRoot(shadowRootElement);
|
||||
this.CacheProvider = ({ children }) => (
|
||||
<CacheProvider value={cache}>{children}</CacheProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const AppProvider = this.CacheProvider;
|
||||
this.#reactRoot.render(
|
||||
<React.StrictMode>
|
||||
<AppProvider>
|
||||
<Slection {...this.#props} />
|
||||
</AppProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
}
|
||||
|
||||
disable() {
|
||||
if (!this.isEnabled() || !this.#reactRoot) {
|
||||
return;
|
||||
}
|
||||
this.#reactRoot.unmount();
|
||||
this.#container.remove();
|
||||
this.#container = null;
|
||||
this.#reactRoot = null;
|
||||
this.#shadowContainer = null;
|
||||
this.CacheProvider = null;
|
||||
}
|
||||
|
||||
toggle() {
|
||||
if (this.isEnabled()) {
|
||||
this.disable();
|
||||
} else {
|
||||
this.enable();
|
||||
}
|
||||
}
|
||||
|
||||
update(newProps) {
|
||||
this.#props = { ...this.#props, ...newProps };
|
||||
if (this.isEnabled()) {
|
||||
if (!this.#props.tranboxSetting?.transOpen) {
|
||||
this.disable();
|
||||
} else {
|
||||
this.enable();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
291
src/libs/translatorManager.js
Normal file
291
src/libs/translatorManager.js
Normal file
@@ -0,0 +1,291 @@
|
||||
import { browser } from "./browser";
|
||||
import { Translator } from "./translator";
|
||||
import { InputTranslator } from "./inputTranslate";
|
||||
import { TransboxManager } from "./tranbox";
|
||||
import { shortcutRegister } from "./shortcut";
|
||||
import { sendIframeMsg } from "./iframe";
|
||||
import { EVENT_KISS, newI18n } from "../config";
|
||||
import { touchTapListener } from "./touch";
|
||||
import { PopupManager } from "./popupManager";
|
||||
import { FabManager } from "./fabManager";
|
||||
import {
|
||||
OPT_SHORTCUT_TRANSLATE,
|
||||
OPT_SHORTCUT_STYLE,
|
||||
OPT_SHORTCUT_POPUP,
|
||||
OPT_SHORTCUT_SETTING,
|
||||
MSG_TRANS_TOGGLE,
|
||||
MSG_TRANS_TOGGLE_STYLE,
|
||||
MSG_TRANS_GETRULE,
|
||||
MSG_TRANS_PUTRULE,
|
||||
MSG_OPEN_TRANBOX,
|
||||
MSG_TRANSBOX_TOGGLE,
|
||||
MSG_POPUP_TOGGLE,
|
||||
MSG_MOUSEHOVER_TOGGLE,
|
||||
MSG_TRANSINPUT_TOGGLE,
|
||||
} from "../config";
|
||||
import { logger } from "./log";
|
||||
|
||||
export default class TranslatorManager {
|
||||
#clearShortcuts = [];
|
||||
#menuCommandIds = [];
|
||||
#clearTouchListeners = [];
|
||||
#isActive = false;
|
||||
#isUserscript;
|
||||
#isIframe;
|
||||
|
||||
#windowMessageHandler = null;
|
||||
#browserMessageHandler = null;
|
||||
|
||||
_translator;
|
||||
_transboxManager;
|
||||
_inputTranslator;
|
||||
_popupManager;
|
||||
_fabManager;
|
||||
|
||||
constructor({ setting, rule, fabConfig, favWords, isIframe, isUserscript }) {
|
||||
this.#isIframe = isIframe;
|
||||
this.#isUserscript = isUserscript;
|
||||
|
||||
this._translator = new Translator({
|
||||
rule,
|
||||
setting,
|
||||
favWords,
|
||||
isUserscript,
|
||||
isIframe,
|
||||
});
|
||||
|
||||
this._transboxManager = new TransboxManager(setting);
|
||||
|
||||
if (!isIframe) {
|
||||
this._inputTranslator = new InputTranslator(setting);
|
||||
this._popupManager = new PopupManager({
|
||||
translator: this._translator,
|
||||
processActions: this.#processActions.bind(this),
|
||||
});
|
||||
this._fabManager = new FabManager({
|
||||
processActions: this.#processActions.bind(this),
|
||||
fabConfig,
|
||||
});
|
||||
}
|
||||
|
||||
this.#windowMessageHandler = this.#handleWindowMessage.bind(this);
|
||||
this.#browserMessageHandler = this.#handleBrowserMessage.bind(this);
|
||||
}
|
||||
|
||||
start() {
|
||||
if (this.#isActive) {
|
||||
logger.info("TranslatorManager is already started.");
|
||||
return;
|
||||
}
|
||||
|
||||
this.#setupMessageListeners();
|
||||
this.#setupTouchOperations();
|
||||
|
||||
if (!this.#isIframe && this.#isUserscript) {
|
||||
this.#registerShortcuts();
|
||||
this.#registerMenus();
|
||||
}
|
||||
|
||||
this.#isActive = true;
|
||||
logger.info("TranslatorManager started.");
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (!this.#isActive) {
|
||||
logger.info("TranslatorManager is not running.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 移除消息监听器
|
||||
if (this.#isUserscript) {
|
||||
window.removeEventListener("message", this.#windowMessageHandler);
|
||||
} else if (
|
||||
browser.runtime.onMessage.hasListener(this.#browserMessageHandler)
|
||||
) {
|
||||
browser.runtime.onMessage.removeListener(this.#browserMessageHandler);
|
||||
}
|
||||
|
||||
// 已注册的快捷键
|
||||
this.#clearShortcuts.forEach((clear) => clear());
|
||||
this.#clearShortcuts = [];
|
||||
|
||||
// 触屏
|
||||
this.#clearTouchListeners.forEach((clear) => clear());
|
||||
this.#clearTouchListeners = [];
|
||||
|
||||
// 油猴菜单
|
||||
if (globalThis.GM && this.#menuCommandIds.length > 0) {
|
||||
this.#menuCommandIds.forEach((id) =>
|
||||
globalThis.GM.unregisterMenuCommand(id)
|
||||
);
|
||||
this.#menuCommandIds = [];
|
||||
}
|
||||
|
||||
// 子模块
|
||||
this._popupManager?.destroy();
|
||||
this._fabManager?.destroy();
|
||||
this._transboxManager?.disable();
|
||||
this._inputTranslator?.disable();
|
||||
this._translator.stop();
|
||||
|
||||
this.#isActive = false;
|
||||
logger.info("TranslatorManager stopped.");
|
||||
}
|
||||
|
||||
#setupMessageListeners() {
|
||||
if (this.#isUserscript) {
|
||||
window.addEventListener("message", this.#windowMessageHandler);
|
||||
} else {
|
||||
browser.runtime.onMessage.addListener(this.#browserMessageHandler);
|
||||
if (this.#isIframe) {
|
||||
window.addEventListener("message", this.#windowMessageHandler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#setupTouchOperations() {
|
||||
if (this.#isIframe) return;
|
||||
|
||||
const { touchModes = [2] } = this._translator.setting;
|
||||
if (touchModes.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleTap = () => {
|
||||
this.#processActions({ action: MSG_TRANS_TOGGLE });
|
||||
};
|
||||
|
||||
const handleListener = (mode) => {
|
||||
let options = null;
|
||||
switch (mode) {
|
||||
case 2:
|
||||
case 3:
|
||||
case 4:
|
||||
options = { taps: 1, fingers: mode };
|
||||
break;
|
||||
case 5:
|
||||
options = { taps: 2, fingers: 1 };
|
||||
break;
|
||||
case 6:
|
||||
options = { taps: 3, fingers: 1 };
|
||||
break;
|
||||
case 7:
|
||||
options = { taps: 2, fingers: 2 };
|
||||
break;
|
||||
default:
|
||||
}
|
||||
if (options) {
|
||||
this.#clearTouchListeners.push(touchTapListener(handleTap, options));
|
||||
}
|
||||
};
|
||||
|
||||
touchModes.forEach((mode) => handleListener(mode));
|
||||
}
|
||||
|
||||
#handleWindowMessage(event) {
|
||||
this.#processActions(event.data);
|
||||
}
|
||||
|
||||
#handleBrowserMessage(message, sender, sendResponse) {
|
||||
const result = this.#processActions(message, true);
|
||||
const response = result || {
|
||||
rule: this._translator.rule,
|
||||
setting: this._translator.setting,
|
||||
};
|
||||
sendResponse(response);
|
||||
return true;
|
||||
}
|
||||
|
||||
#registerShortcuts() {
|
||||
const { shortcuts } = this._translator.setting;
|
||||
this.#clearShortcuts = [
|
||||
shortcutRegister(shortcuts[OPT_SHORTCUT_TRANSLATE], () =>
|
||||
this.#processActions({ action: MSG_TRANS_TOGGLE })
|
||||
),
|
||||
shortcutRegister(shortcuts[OPT_SHORTCUT_STYLE], () =>
|
||||
this.#processActions({ action: MSG_TRANS_TOGGLE_STYLE })
|
||||
),
|
||||
shortcutRegister(shortcuts[OPT_SHORTCUT_POPUP], () =>
|
||||
this.#processActions({ action: MSG_POPUP_TOGGLE })
|
||||
),
|
||||
shortcutRegister(shortcuts[OPT_SHORTCUT_SETTING], () =>
|
||||
window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank")
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
#registerMenus() {
|
||||
if (!globalThis.GM) return;
|
||||
const { contextMenuType, uiLang } = this._translator.setting;
|
||||
if (contextMenuType === 0) return;
|
||||
|
||||
const i18n = newI18n(uiLang || "zh");
|
||||
const GM = globalThis.GM;
|
||||
this.#menuCommandIds = [
|
||||
GM.registerMenuCommand(
|
||||
i18n("translate_switch"),
|
||||
() => this.#processActions({ action: MSG_TRANS_TOGGLE }),
|
||||
"Q"
|
||||
),
|
||||
GM.registerMenuCommand(
|
||||
i18n("toggle_style"),
|
||||
() => this.#processActions({ action: MSG_TRANS_TOGGLE_STYLE }),
|
||||
"C"
|
||||
),
|
||||
GM.registerMenuCommand(
|
||||
i18n("open_menu"),
|
||||
() => this.#processActions({ action: MSG_POPUP_TOGGLE }),
|
||||
"K"
|
||||
),
|
||||
GM.registerMenuCommand(
|
||||
i18n("open_setting"),
|
||||
() => window.open(process.env.REACT_APP_OPTIONSPAGE, "_blank"),
|
||||
"O"
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
#processActions({ action, args } = {}, fromExt = false) {
|
||||
if (!fromExt) {
|
||||
sendIframeMsg(action, args);
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case MSG_TRANS_TOGGLE:
|
||||
this._translator.toggle();
|
||||
break;
|
||||
case MSG_TRANS_TOGGLE_STYLE:
|
||||
this._translator.toggleStyle();
|
||||
break;
|
||||
case MSG_TRANS_GETRULE:
|
||||
break;
|
||||
case MSG_TRANS_PUTRULE:
|
||||
this._translator.updateRule(args);
|
||||
break;
|
||||
case MSG_OPEN_TRANBOX:
|
||||
document.dispatchEvent(
|
||||
new CustomEvent(EVENT_KISS, {
|
||||
detail: { action: MSG_OPEN_TRANBOX },
|
||||
})
|
||||
);
|
||||
break;
|
||||
case MSG_POPUP_TOGGLE:
|
||||
this._popupManager?.toggle();
|
||||
break;
|
||||
case MSG_TRANSBOX_TOGGLE:
|
||||
this._transboxManager?.toggle();
|
||||
this._translator.toggleTransbox();
|
||||
break;
|
||||
case MSG_MOUSEHOVER_TOGGLE:
|
||||
this._translator.toggleMouseHover();
|
||||
break;
|
||||
case MSG_TRANSINPUT_TOGGLE:
|
||||
this._inputTranslator?.toggle();
|
||||
this._translator.toggleInputTranslate();
|
||||
break;
|
||||
default:
|
||||
logger.info(`Message action is unavailable: ${action}`);
|
||||
return { error: `Message action is unavailable: ${action}` };
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user