Compare commits
1081 Commits
v0.0.27
...
dependenci
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3271c96531 | ||
|
|
05fa6b9aba | ||
|
|
7edd7f27cb | ||
|
|
4a6b12eda1 | ||
|
|
a354e1a0eb | ||
|
|
c2c419522a | ||
|
|
7a3cc7d242 | ||
|
|
bd6f02f6af | ||
|
|
bd33728fd7 | ||
|
|
8400ee8a3d | ||
|
|
80ee7aef8e | ||
|
|
41762be3a5 | ||
|
|
605dda7b76 | ||
|
|
8d6e3b5a58 | ||
|
|
53920fc368 | ||
|
|
96d10423d9 | ||
|
|
42be9b5b51 | ||
|
|
0add041516 | ||
|
|
dee76cac8d | ||
|
|
9646fab22c | ||
|
|
a54e9cb244 | ||
|
|
f8339c7a9a | ||
|
|
9e2c864117 | ||
|
|
e1ccd71455 | ||
|
|
0e82426ea1 | ||
|
|
6c7afd168a | ||
|
|
e014fdf3da | ||
|
|
2074da05c8 | ||
|
|
84cd87b70a | ||
|
|
75f87044f6 | ||
|
|
f71b51e64a | ||
|
|
9d741cdd63 | ||
|
|
56be17000a | ||
|
|
f6aacbc31d | ||
|
|
ad4b57c327 | ||
|
|
3794b2f0de | ||
|
|
e0e8412728 | ||
|
|
05b8175aad | ||
|
|
339c89dd1c | ||
|
|
402299e9c8 | ||
|
|
d0e8f450fd | ||
|
|
e97b1ccd7a | ||
|
|
68691b91ef | ||
|
|
213d417481 | ||
|
|
2ec841eb61 | ||
|
|
60b5c54be1 | ||
|
|
448412a191 | ||
|
|
e06327936e | ||
|
|
5f044e1aed | ||
|
|
d90fb6fc9b | ||
|
|
1c583d2ea9 | ||
|
|
ddab131102 | ||
|
|
3b06cf8a2a | ||
|
|
c5d7c29f3d | ||
|
|
26af860eac | ||
|
|
ec9852eb98 | ||
|
|
f2b8c6d0ca | ||
|
|
2cf7a048cf | ||
|
|
3dbb71a076 | ||
|
|
ddf6760543 | ||
|
|
a6fa323f33 | ||
|
|
2df8f1acdb | ||
|
|
74a3290abc | ||
|
|
ab306d21f3 | ||
|
|
1bd7795851 | ||
|
|
302c142346 | ||
|
|
7c254c9b45 | ||
|
|
b1c458359d | ||
|
|
2d925c62f5 | ||
|
|
ef4711987f | ||
|
|
8910dfaecf | ||
|
|
e8b2e79c7d | ||
|
|
333ee1e1f7 | ||
|
|
ba013bbe96 | ||
|
|
636e66fe7a | ||
|
|
372a45e667 | ||
|
|
ab6b796ce2 | ||
|
|
71e6c02897 | ||
|
|
3edeaa7038 | ||
|
|
f2f75d4015 | ||
|
|
69adade738 | ||
|
|
1bd88828e4 | ||
|
|
977fcbe6cd | ||
|
|
aa76a5c436 | ||
|
|
ea5fad428d | ||
|
|
0e1e27b35a | ||
|
|
d41f67a156 | ||
|
|
6f10dec808 | ||
|
|
9b79da2cdf | ||
|
|
c11b8519f1 | ||
|
|
b9e23a0b59 | ||
|
|
75d41b6fe5 | ||
|
|
4256590735 | ||
|
|
462fb05ea8 | ||
|
|
812dbfd836 | ||
|
|
cfca837777 | ||
|
|
ac5fb1948a | ||
|
|
22d8b73625 | ||
|
|
edde40c298 | ||
|
|
50ade92238 | ||
|
|
b598d00aef | ||
|
|
97bee17e4e | ||
|
|
6aa5c79332 | ||
|
|
7c5ce756f9 | ||
|
|
43d53a9ffa | ||
|
|
e6589bee5b | ||
|
|
9f94cad615 | ||
|
|
7778d9f773 | ||
|
|
564bd55147 | ||
|
|
a7ff806522 | ||
|
|
ac3b074d66 | ||
|
|
da949c2604 | ||
|
|
20a9775dc6 | ||
|
|
8ec19e058f | ||
|
|
a5c4549722 | ||
|
|
c7d302aa16 | ||
|
|
77ac565ff3 | ||
|
|
2e6f834a50 | ||
|
|
5b044cca9e | ||
|
|
2ab5623809 | ||
|
|
2d07f0d77c | ||
|
|
8ba7f9f702 | ||
|
|
460ac7a86b | ||
|
|
f93a1ab3eb | ||
|
|
6b02696aa8 | ||
|
|
25f20d6a85 | ||
|
|
8387964bae | ||
|
|
926278617f | ||
|
|
1933737a0c | ||
|
|
b3e5288bde | ||
|
|
ed6e966b2f | ||
|
|
9315fe36b6 | ||
|
|
41fbf5ba36 | ||
|
|
40db481453 | ||
|
|
1b5e295744 | ||
|
|
b42a52f7f6 | ||
|
|
8268df84d0 | ||
|
|
eb6fa5f1f1 | ||
|
|
025c8856ed | ||
|
|
ed421445e9 | ||
|
|
0cda07106b | ||
|
|
f335941b62 | ||
|
|
f0719f8bde | ||
|
|
27fe28661c | ||
|
|
bb13b12de6 | ||
|
|
1117e28b2e | ||
|
|
0f48873c25 | ||
|
|
faf61b4af3 | ||
|
|
e29ce93d32 | ||
|
|
38ab68492e | ||
|
|
53b1576e5f | ||
|
|
2835e79973 | ||
|
|
8e4d7e989b | ||
|
|
603c6b826c | ||
|
|
51bcc77cb3 | ||
|
|
a30d07b924 | ||
|
|
11faa5fe39 | ||
|
|
ce8383b26f | ||
|
|
bd8f07c90a | ||
|
|
fee077bebd | ||
|
|
558f4d93ba | ||
|
|
e77d126349 | ||
|
|
cd8667d6de | ||
|
|
8a2d06d010 | ||
|
|
92741fc733 | ||
|
|
1cff162649 | ||
|
|
f59be465ea | ||
|
|
27305317ce | ||
|
|
8378320e50 | ||
|
|
86a69952db | ||
|
|
e212ebfdb9 | ||
|
|
f235125d48 | ||
|
|
8efa815eb9 | ||
|
|
e54d42576b | ||
|
|
1e18539862 | ||
|
|
673fdf4721 | ||
|
|
57d6bfba51 | ||
|
|
8fa23aecda | ||
|
|
f9c10533e8 | ||
|
|
c891c3723e | ||
|
|
a0625ef2e7 | ||
|
|
b2774abb54 | ||
|
|
fa089bbc40 | ||
|
|
8437d1763f | ||
|
|
0532b0f9df | ||
|
|
5f8eb853f0 | ||
|
|
9125a85382 | ||
|
|
5b3dc44c72 | ||
|
|
ee5147f111 | ||
|
|
f67137fd5e | ||
|
|
b67db4a896 | ||
|
|
067018828c | ||
|
|
e990530ada | ||
|
|
45f863a72a | ||
|
|
519febbeb2 | ||
|
|
854b1a4caf | ||
|
|
08eb95e73a | ||
|
|
66f8fbf08c | ||
|
|
86a30bbb29 | ||
|
|
966c453c27 | ||
|
|
c7bcaf0193 | ||
|
|
0ac91e36a0 | ||
|
|
6f1299ce9e | ||
|
|
83ca29d649 | ||
|
|
d0206044dc | ||
|
|
d4623e4175 | ||
|
|
68ef03377d | ||
|
|
d3f9d3d033 | ||
|
|
a6ccd04471 | ||
|
|
0fd8174e77 | ||
|
|
ebc63f479a | ||
|
|
570c39c20a | ||
|
|
1d57257cf5 | ||
|
|
82d7baee0b | ||
|
|
1d91e1f1f7 | ||
|
|
15e8894614 | ||
|
|
0ae96918e9 | ||
|
|
e970880059 | ||
|
|
69ae86aba8 | ||
|
|
8142e1be49 | ||
|
|
cbc7612451 | ||
|
|
09e6a7b7cc | ||
|
|
082e35668a | ||
|
|
c9e78c837b | ||
|
|
2d171db672 | ||
|
|
a8b11abec8 | ||
|
|
b08333dccd | ||
|
|
98bad48971 | ||
|
|
53375bb536 | ||
|
|
88798093e1 | ||
|
|
45a28751af | ||
|
|
1670c44464 | ||
|
|
a286ac85dc | ||
|
|
1606adc0ab | ||
|
|
b4ce557d92 | ||
|
|
2ba7fff8d4 | ||
|
|
98efc93610 | ||
|
|
2680507eae | ||
|
|
df72392ad2 | ||
|
|
a6acb15a00 | ||
|
|
ba7242a815 | ||
|
|
99740c1324 | ||
|
|
c04b20d1fa | ||
|
|
e127067878 | ||
|
|
6f03b72368 | ||
|
|
7bd3ee9340 | ||
|
|
c94db606e5 | ||
|
|
4d2474226b | ||
|
|
1ffc4f538b | ||
|
|
6702ac957b | ||
|
|
ba42b2e77d | ||
|
|
fba18ca40a | ||
|
|
3a2a7a1476 | ||
|
|
3c6d6b90bc | ||
|
|
47dc9ee304 | ||
|
|
0045bf206c | ||
|
|
669a1a6953 | ||
|
|
e28452cc7b | ||
|
|
5ceac03db1 | ||
|
|
96215e5950 | ||
|
|
7a030b9224 | ||
|
|
e743478a4d | ||
|
|
bc29c80d44 | ||
|
|
82b8a474d7 | ||
|
|
99851b297d | ||
|
|
e3a500e12c | ||
|
|
378666e3cc | ||
|
|
b65ad1ebd7 | ||
|
|
933a821b5f | ||
|
|
159337ddbd | ||
|
|
2e25a4333a | ||
|
|
64fafb0795 | ||
|
|
5927e0bb3b | ||
|
|
e21596db83 | ||
|
|
8a608c3c3e | ||
|
|
76f9db8516 | ||
|
|
90f4809b7c | ||
|
|
3ff1af9fd6 | ||
|
|
711dd520f7 | ||
|
|
99503c836a | ||
|
|
2c8bc51862 | ||
|
|
5e7aa15232 | ||
|
|
d0761869b6 | ||
|
|
bb99b89228 | ||
|
|
7b88beb0b5 | ||
|
|
82c630bd0e | ||
|
|
bb4f11e1b8 | ||
|
|
3182d20b7c | ||
|
|
a6f15e2474 | ||
|
|
0c3f03680c | ||
|
|
a66e8dfc9f | ||
|
|
556232aac4 | ||
|
|
7fcfaadd91 | ||
|
|
18d388e1e2 | ||
|
|
7de6622d74 | ||
|
|
9af1f90990 | ||
|
|
15ab43963d | ||
|
|
6b7465a4b0 | ||
|
|
d64bdf02de | ||
|
|
4a2c94993a | ||
|
|
86c318d86b | ||
|
|
bffc1ad41d | ||
|
|
2a685c116f | ||
|
|
0e271d2924 | ||
|
|
7127c4d590 | ||
|
|
a82929996b | ||
|
|
3f01f52c7b | ||
|
|
9c94494622 | ||
|
|
c8a508f35c | ||
|
|
4ec73ef1c3 | ||
|
|
3465b79e5b | ||
|
|
726a0a33ae | ||
|
|
ba05810d80 | ||
|
|
10bb53e7de | ||
|
|
dd8a083546 | ||
|
|
84131a71c9 | ||
|
|
4909a11896 | ||
|
|
4069357b81 | ||
|
|
e6e87bcc40 | ||
|
|
7ba22045e8 | ||
|
|
d4040b61c4 | ||
|
|
7534f8fc37 | ||
|
|
7ced009e92 | ||
|
|
cc13c71e40 | ||
|
|
d2f09209a7 | ||
|
|
a241b04d99 | ||
|
|
4f35d907cb | ||
|
|
9e50d144a3 | ||
|
|
8514bb48c4 | ||
|
|
3ae633a2a1 | ||
|
|
2799cb9118 | ||
|
|
d20346b6ac | ||
|
|
2f00666a68 | ||
|
|
80f4570093 | ||
|
|
3f0a2ba48f | ||
|
|
fc90094e8a | ||
|
|
20d580ade8 | ||
|
|
290a024a9e | ||
|
|
64a283c3a6 | ||
|
|
3e93f0ecbc | ||
|
|
b12bcc1c49 | ||
|
|
05d7313dcc | ||
|
|
c0b6e549f0 | ||
|
|
31f9bf219e | ||
|
|
4906ca7059 | ||
|
|
b963a7a0e5 | ||
|
|
5d6dadda76 | ||
|
|
77f69fd223 | ||
|
|
7f5052bc87 | ||
|
|
3009c1f4f6 | ||
|
|
5425872bba | ||
|
|
0759e17295 | ||
|
|
56a59d25df | ||
|
|
887f92babe | ||
|
|
197f942b3f | ||
|
|
a5be7a35e9 | ||
|
|
e347675265 | ||
|
|
02a084d571 | ||
|
|
7fbcc23d94 | ||
|
|
bda87167a3 | ||
|
|
34f53c287e | ||
|
|
9ab07f37d7 | ||
|
|
36399b39fb | ||
|
|
1e015287a1 | ||
|
|
a590aa8485 | ||
|
|
c0582fde66 | ||
|
|
d8d75b4afa | ||
|
|
9d4942723c | ||
|
|
e5b1ece5c0 | ||
|
|
1b25bfbf5a | ||
|
|
7c64d9f42a | ||
|
|
61f9f4ef58 | ||
|
|
b8eac56213 | ||
|
|
ab5fb5749b | ||
|
|
e0fb9a1b25 | ||
|
|
97e717f646 | ||
|
|
caa82ad1e6 | ||
|
|
7ec251ea6d | ||
|
|
675b0c3cca | ||
|
|
05e54d4b7f | ||
|
|
b4527c90e5 | ||
|
|
dbc626734d | ||
|
|
3f2ce3cb80 | ||
|
|
0820d6d6fb | ||
|
|
a8c74e39c2 | ||
|
|
d58c29907d | ||
|
|
2322733427 | ||
|
|
d76e8fe85a | ||
|
|
0c2e4fe34a | ||
|
|
30b6c87a49 | ||
|
|
540221467a | ||
|
|
d44d331a78 | ||
|
|
595554f18a | ||
|
|
84d3c3f7eb | ||
|
|
890f55c9dc | ||
|
|
575a14e1f3 | ||
|
|
91074bebd6 | ||
|
|
3459a16b48 | ||
|
|
88c9b6849f | ||
|
|
d87bb25baf | ||
|
|
34cb796505 | ||
|
|
1eb34e0662 | ||
|
|
01d631033f | ||
|
|
84b2c07340 | ||
|
|
a86eeb636d | ||
|
|
417a5a8214 | ||
|
|
bcf40dde8c | ||
|
|
cad87484c7 | ||
|
|
b8ad641ed6 | ||
|
|
ae8197be8b | ||
|
|
eafb0274a7 | ||
|
|
71a23a4e02 | ||
|
|
26fd90dfa3 | ||
|
|
e393ebede2 | ||
|
|
2981bb3f19 | ||
|
|
6e9f05abb1 | ||
|
|
9df1115380 | ||
|
|
f22e360cbb | ||
|
|
67769af6f4 | ||
|
|
b1a9a1d6d9 | ||
|
|
cf0606ecb7 | ||
|
|
7287edcd6f | ||
|
|
e0d26203dd | ||
|
|
7e3a85e9da | ||
|
|
5a0fed9c93 | ||
|
|
1f1e743912 | ||
|
|
b4301ed0d5 | ||
|
|
b5391560fc | ||
|
|
718989cbcf | ||
|
|
d0aee76962 | ||
|
|
fb08af96bd | ||
|
|
510a0c5e70 | ||
|
|
89bdc8ec75 | ||
|
|
ae25ade318 | ||
|
|
dd5e46a8a7 | ||
|
|
f7218aaa9e | ||
|
|
ee79bcfc44 | ||
|
|
2c2c174874 | ||
|
|
f57c49ce3a | ||
|
|
06121acfac | ||
|
|
2078ce7446 | ||
|
|
49f41abfdb | ||
|
|
70fcfe6d6c | ||
|
|
b060b4b9bf | ||
|
|
ee2135bfb3 | ||
|
|
7daa322441 | ||
|
|
74252cb66b | ||
|
|
5fe2be031f | ||
|
|
2ba3aaba47 | ||
|
|
ad8903991c | ||
|
|
3e5624c570 | ||
|
|
88d3bba300 | ||
|
|
f5ee6f3537 | ||
|
|
afc77e7adc | ||
|
|
024f42fce6 | ||
|
|
8a5f12b97c | ||
|
|
954b21cf39 | ||
|
|
74d095774d | ||
|
|
17a2722e6d | ||
|
|
c843bddbfe | ||
|
|
3f22a49755 | ||
|
|
7af2ffcebf | ||
|
|
de90c959e0 | ||
|
|
9987dc1eb4 | ||
|
|
3efd575dd2 | ||
|
|
f4c7b17a87 | ||
|
|
16d80718cb | ||
|
|
ad228d53b7 | ||
|
|
15ee1e531b | ||
|
|
1c8fb3392a | ||
|
|
8647866a32 | ||
|
|
23351c4f1c | ||
|
|
1367c304cf | ||
|
|
26d6bcb074 | ||
|
|
b0d651ece1 | ||
|
|
b6d50ba6a4 | ||
|
|
b3ab6a9166 | ||
|
|
f39a5ac9c2 | ||
|
|
38a9a9240d | ||
|
|
241b22a465 | ||
|
|
741abc0366 | ||
|
|
7854775de5 | ||
|
|
e62eaa6b4b | ||
|
|
b4cce23ef4 | ||
|
|
2bcaf90fc8 | ||
|
|
96ffbe2f84 | ||
|
|
6f5acee1c3 | ||
|
|
54e491d8bf | ||
|
|
ab6374e278 | ||
|
|
2fda4c9f67 | ||
|
|
5138a45b0f | ||
|
|
b224d4fa8a | ||
|
|
a552e44483 | ||
|
|
0cf3bba118 | ||
|
|
2c48ea3508 | ||
|
|
b9b6212b75 | ||
|
|
b978aaec21 | ||
|
|
af704681d9 | ||
|
|
1443ddfe6c | ||
|
|
54457a3e1b | ||
|
|
bf180e6a2c | ||
|
|
864a5820c9 | ||
|
|
4d3ca49c3f | ||
|
|
c49c3cf7f0 | ||
|
|
5d5ab57469 | ||
|
|
31978d8de0 | ||
|
|
e8eb68bf24 | ||
|
|
9ea08f4fed | ||
|
|
fe078a5c5b | ||
|
|
61933954f3 | ||
|
|
4c243638cb | ||
|
|
02ba04b5d8 | ||
|
|
4f158a4829 | ||
|
|
177a22df59 | ||
|
|
6b0ca2966e | ||
|
|
aadfaf7150 | ||
|
|
b307b9a66b | ||
|
|
6c1ab6002d | ||
|
|
9638eefc91 | ||
|
|
9e9c4ad587 | ||
|
|
ce231431b9 | ||
|
|
06e1e14e02 | ||
|
|
416e7884f5 | ||
|
|
d579222007 | ||
|
|
30243c84cd | ||
|
|
3557a77645 | ||
|
|
97be28638b | ||
|
|
aba0826c38 | ||
|
|
f032228d0e | ||
|
|
6cf174c5ed | ||
|
|
c2109d245f | ||
|
|
6a9745171e | ||
|
|
f9a68e8b23 | ||
|
|
6e391df5ee | ||
|
|
f5edca94d3 | ||
|
|
60046abec3 | ||
|
|
cafc2060b8 | ||
|
|
b1f45752cf | ||
|
|
ed17551170 | ||
|
|
ef5adab638 | ||
|
|
fb653ff99d | ||
|
|
78fc47a9c4 | ||
|
|
2a124cea61 | ||
|
|
4c7cc563dc | ||
|
|
6114af4f93 | ||
|
|
8c31629655 | ||
|
|
03c8a8edb2 | ||
|
|
3eeaee154f | ||
|
|
8cf8fa7c80 | ||
|
|
6b4f6fc71e | ||
|
|
30c2680b6f | ||
|
|
fb7b1800cc | ||
|
|
ff573bf377 | ||
|
|
0a33bb861e | ||
|
|
728756289b | ||
|
|
56ccd3a0ac | ||
|
|
66f3f0ba07 | ||
|
|
af5e0d589e | ||
|
|
533dc99e7d | ||
|
|
fc5ca965ba | ||
|
|
9c4a46bcdb | ||
|
|
52658886e7 | ||
|
|
8174ab7616 | ||
|
|
2b6acedae1 | ||
|
|
d00fe9c5f4 | ||
|
|
88aa270728 | ||
|
|
4ae409c7f4 | ||
|
|
9a29c9abdd | ||
|
|
66d93ea037 | ||
|
|
6c0066dbfb | ||
|
|
4fde644733 | ||
|
|
e7841c60df | ||
|
|
94f647b24a | ||
|
|
630249d22a | ||
|
|
db99b4cb54 | ||
|
|
c77db23586 | ||
|
|
daf66bcec4 | ||
|
|
8caf36349f | ||
|
|
6934de58e5 | ||
|
|
54a5007c01 | ||
|
|
e25a455698 | ||
|
|
ab429dfeb6 | ||
|
|
c5289dc0e8 | ||
|
|
d191877002 | ||
|
|
4d979160c2 | ||
|
|
d00e8f6e19 | ||
|
|
91b77e5237 | ||
|
|
5b8c246d53 | ||
|
|
b374b9b91c | ||
|
|
403717117e | ||
|
|
027295d995 | ||
|
|
c9b7eccbc1 | ||
|
|
2b6d9348cd | ||
|
|
692f8c8454 | ||
|
|
6783355c4d | ||
|
|
fb9cca1e99 | ||
|
|
eb770ede1a | ||
|
|
2643e853af | ||
|
|
b79456e91b | ||
|
|
ac66c086f8 | ||
|
|
ebccf401dd | ||
|
|
66494845b7 | ||
|
|
e6c36ad602 | ||
|
|
26379182db | ||
|
|
bba03d14d4 | ||
|
|
23b728a762 | ||
|
|
819c5207d2 | ||
|
|
311358544e | ||
|
|
4480ecc96d | ||
|
|
6c5f70a205 | ||
|
|
99adfb4a9e | ||
|
|
7909cf4067 | ||
|
|
780ab20aeb | ||
|
|
73119bb7c5 | ||
|
|
f20f0f064e | ||
|
|
1b44ae098c | ||
|
|
453c230716 | ||
|
|
439d885ee1 | ||
|
|
43dee3ef76 | ||
|
|
c71ba6ff8d | ||
|
|
fb7a36eb73 | ||
|
|
e7f294a065 | ||
|
|
d5037f180e | ||
|
|
e90158809a | ||
|
|
0cb802ed9a | ||
|
|
d0b47204f4 | ||
|
|
351cb391e5 | ||
|
|
051be927cd | ||
|
|
8bad2c2113 | ||
|
|
2bcf6fb3eb | ||
|
|
d1ba0ed2b2 | ||
|
|
6e421e60c5 | ||
|
|
8385050804 | ||
|
|
bfe4f08232 | ||
|
|
132f914b0d | ||
|
|
97d82b03ab | ||
|
|
f06fa3f9b7 | ||
|
|
6337788a22 | ||
|
|
024db4358b | ||
|
|
173f35487e | ||
|
|
74cbe82dd1 | ||
|
|
a8c30d30a9 | ||
|
|
1b7a52d5af | ||
|
|
e5109789bf | ||
|
|
4d2b35e09d | ||
|
|
5c5177ec57 | ||
|
|
d93b00cd15 | ||
|
|
39ade59174 | ||
|
|
0309c815b9 | ||
|
|
ce613098db | ||
|
|
ab34044196 | ||
|
|
17f724748f | ||
|
|
f3a917b5e7 | ||
|
|
376011ea08 | ||
|
|
2ce7624c14 | ||
|
|
c62dddd5b9 | ||
|
|
490ba9f140 | ||
|
|
f76890cc56 | ||
|
|
e1c8f1fed9 | ||
|
|
6e19a4ab8b | ||
|
|
b156523a7f | ||
|
|
d20b745ae5 | ||
|
|
c51e9e6b2c | ||
|
|
1a31fa9067 | ||
|
|
558b8499af | ||
|
|
986c162988 | ||
|
|
770d5cd11c | ||
|
|
446d2ab3af | ||
|
|
2f9bf7f063 | ||
|
|
72ff9c0964 | ||
|
|
33b1a11d85 | ||
|
|
06dabf1e4e | ||
|
|
28d3691e0b | ||
|
|
ffa21fbfd2 | ||
|
|
db028665fd | ||
|
|
6bc83d9f27 | ||
|
|
790d832155 | ||
|
|
f477cecdeb | ||
|
|
e031389021 | ||
|
|
e00f826eb8 | ||
|
|
24f4e8ab99 | ||
|
|
1550d528bd | ||
|
|
40c041031e | ||
|
|
3e555ec9f1 | ||
|
|
5098f14aab | ||
|
|
a355a9c85e | ||
|
|
e7db2a8573 | ||
|
|
9b18bd0b48 | ||
|
|
f95ddd594e | ||
|
|
fe8168784f | ||
|
|
4046f143f6 | ||
|
|
e4e16999c8 | ||
|
|
10f3ba4ff4 | ||
|
|
3cd2be5081 | ||
|
|
c9359978f9 | ||
|
|
781c67b31a | ||
|
|
020bd129fb | ||
|
|
8086b6d78c | ||
|
|
48e14b36b8 | ||
|
|
b3c1c56579 | ||
|
|
bd0e932910 | ||
|
|
525e5f88ae | ||
|
|
005eeb0e0b | ||
|
|
d21bb015e8 | ||
|
|
7338838b0e | ||
|
|
8bb4803ff9 | ||
|
|
892b919cf3 | ||
|
|
572d81ecef | ||
|
|
a4ce7a4037 | ||
|
|
6eafb15cf9 | ||
|
|
e19fe5ce1c | ||
|
|
9d2017e598 | ||
|
|
f33c419ed9 | ||
|
|
f425fbaf9d | ||
|
|
bcc5ec897a | ||
|
|
f5f2fe3472 | ||
|
|
b7c3863882 | ||
|
|
d759f48ee8 | ||
|
|
3a37075e71 | ||
|
|
58366c0b87 | ||
|
|
2667ed13f1 | ||
|
|
34daffbc96 | ||
|
|
be81cd72af | ||
|
|
4ae00714d2 | ||
|
|
f24cbb6692 | ||
|
|
5a35c5b928 | ||
|
|
1880da6351 | ||
|
|
df93cb103c | ||
|
|
63b474a32c | ||
|
|
abdbf158d1 | ||
|
|
ee68d80d0a | ||
|
|
c8e6f3a627 | ||
|
|
dc941575fe | ||
|
|
e64103e5f2 | ||
|
|
f0ab03a9fb | ||
|
|
09965f1cc6 | ||
|
|
d2852bb34a | ||
|
|
b03c52a501 | ||
|
|
fd6633f536 | ||
|
|
320ac81f48 | ||
|
|
70bcd2428f | ||
|
|
71f5ada0a3 | ||
|
|
aab5141404 | ||
|
|
ff6b119f27 | ||
|
|
7ef4b7eeb8 | ||
|
|
4668be6e24 | ||
|
|
a211fc7c97 | ||
|
|
54af0b675d | ||
|
|
8c8171e774 | ||
|
|
0bb1790206 | ||
|
|
45fc84d8be | ||
|
|
f7500f4cad | ||
|
|
0cfd718d8a | ||
|
|
5f486d0f51 | ||
|
|
6e5a2f85a1 | ||
|
|
e66a89208d | ||
|
|
7f65c501c6 | ||
|
|
22c2382765 | ||
|
|
38e1a4febf | ||
|
|
8db554b377 | ||
|
|
5f5cc55331 | ||
|
|
2f8b39186f | ||
|
|
a3a724e2e6 | ||
|
|
73235c8699 | ||
|
|
7e5999e862 | ||
|
|
afa244dcb0 | ||
|
|
4ea5bb2390 | ||
|
|
e545d552f6 | ||
|
|
56fe7b3596 | ||
|
|
eb28ec866a | ||
|
|
4649454282 | ||
|
|
a45dc6efda | ||
|
|
9e56b9fbb5 | ||
|
|
5504994cb9 | ||
|
|
acff6d0432 | ||
|
|
eea9cb7c5b | ||
|
|
c0ddddfb1f | ||
|
|
f7dab3ca56 | ||
|
|
e11b4038a3 | ||
|
|
b635e64803 | ||
|
|
20a194b49a | ||
|
|
33a5fb8837 | ||
|
|
59dae640db | ||
|
|
a6ac75e97b | ||
|
|
cc5b33a8ec | ||
|
|
6e1a627b84 | ||
|
|
62d4c65e1c | ||
|
|
600134a3ac | ||
|
|
df14af7337 | ||
|
|
2f740b570d | ||
|
|
294d980b52 | ||
|
|
c9d9909d74 | ||
|
|
90eeabae7b | ||
|
|
a32c77c5f1 | ||
|
|
910846f2ce | ||
|
|
35d0438261 | ||
|
|
515af472ce | ||
|
|
f062f7f9fe | ||
|
|
f68378041f | ||
|
|
30ef3057ac | ||
|
|
48f3a934c9 | ||
|
|
38d1fde84f | ||
|
|
dd78670a4b | ||
|
|
b8a8190a43 | ||
|
|
2d0989342f | ||
|
|
15ff9b06a1 | ||
|
|
41b19f69de | ||
|
|
f2bd6f1fce | ||
|
|
ec94218a4b | ||
|
|
c2e5c7cf38 | ||
|
|
bac5734527 | ||
|
|
bef4033d94 | ||
|
|
64216cba67 | ||
|
|
a9d6167a9f | ||
|
|
6c6b40548f | ||
|
|
a916d88e85 | ||
|
|
ab2f0548a3 | ||
|
|
e30ba07285 | ||
|
|
1b336d973d | ||
|
|
7b1866737f | ||
|
|
eedc4ab648 | ||
|
|
5e429c7a94 | ||
|
|
57d23eb043 | ||
|
|
5ebd9be89a | ||
|
|
5a743779e2 | ||
|
|
0495062110 | ||
|
|
d522191f69 | ||
|
|
bbbdc8b7a6 | ||
|
|
c00ed8aa5a | ||
|
|
0beaa94068 | ||
|
|
96e76665d6 | ||
|
|
6423a29600 | ||
|
|
8bddf30dcf | ||
|
|
a6b2db182d | ||
|
|
b9f3f9d859 | ||
|
|
6331447dcd | ||
|
|
4213ee660f | ||
|
|
3bd9287c5d | ||
|
|
b23d3f7c8b | ||
|
|
f8d9e5e027 | ||
|
|
8fa7fb3b1f | ||
|
|
63d92a0872 | ||
|
|
509d83365e | ||
|
|
d6ab73c905 | ||
|
|
5a93ba05d5 | ||
|
|
2d2fdf0b1e | ||
|
|
cf96622261 | ||
|
|
47c8ccb0e5 | ||
|
|
02fdb8778b | ||
|
|
6bed7f0e66 | ||
|
|
7597d335b9 | ||
|
|
f32c5ba244 | ||
|
|
ab53ab21e2 | ||
|
|
acfe5dbb49 | ||
|
|
f9b91fa189 | ||
|
|
2462e68ba1 | ||
|
|
019b2a1681 | ||
|
|
e94a07b677 | ||
|
|
c058c29755 | ||
|
|
9e7c7ac163 | ||
|
|
fcee41f00d | ||
|
|
ff2c1bf8ed | ||
|
|
a2cf26e7ed | ||
|
|
71e6900375 | ||
|
|
3bdc98bd12 | ||
|
|
68d2a6e951 | ||
|
|
acf47ac947 | ||
|
|
fda24e5f5a | ||
|
|
05eca8e4d8 | ||
|
|
b915f3b1a9 | ||
|
|
ab58968f4d | ||
|
|
820d1e7570 | ||
|
|
a120c8cf98 | ||
|
|
5a35ea116f | ||
|
|
e72ad1f030 | ||
|
|
a17362437a | ||
|
|
8643dc43b1 | ||
|
|
e1c869a358 | ||
|
|
2f5b8d9abe | ||
|
|
db324f54eb | ||
|
|
3242efb1a2 | ||
|
|
30f9f1a021 | ||
|
|
3f58d05aa7 | ||
|
|
c611a51575 | ||
|
|
73458dcd28 | ||
|
|
23ebeb1cc0 | ||
|
|
a7ba9f1886 | ||
|
|
d5192e2244 | ||
|
|
7eb595170f | ||
|
|
bd576ca808 | ||
|
|
2f8146b11f | ||
|
|
7f321c89cb | ||
|
|
fa65f606b8 | ||
|
|
7a3285adaf | ||
|
|
8bf78fef10 | ||
|
|
78f97ce4df | ||
|
|
66ccbf70f8 | ||
|
|
cb48600b40 | ||
|
|
33ce235713 | ||
|
|
f1a68ece01 | ||
|
|
dd563360af | ||
|
|
7f6dac4271 | ||
|
|
178fd8e828 | ||
|
|
1641e02a7d | ||
|
|
cfd04e9bb4 | ||
|
|
38effaf740 | ||
|
|
a68eb4a73e | ||
|
|
c02990ef98 | ||
|
|
5aa7d5ffe9 | ||
|
|
4942b0fca5 | ||
|
|
aed1bdff5a | ||
|
|
929c840006 | ||
|
|
57aef1d3c2 | ||
|
|
c1734a094c | ||
|
|
99c46685ac | ||
|
|
066b08040a | ||
|
|
35de2334fb | ||
|
|
5564c966a5 | ||
|
|
0891b5e7b7 | ||
|
|
f3341f201f | ||
|
|
bf0dafabe2 | ||
|
|
60f6587169 | ||
|
|
a9bf32919e | ||
|
|
7633f9f88b | ||
|
|
9b56233938 | ||
|
|
65074264b8 | ||
|
|
4f6fceb87f | ||
|
|
659fdd1d37 | ||
|
|
8bce2ce040 | ||
|
|
55cc83a5d4 | ||
|
|
6a51e93ded | ||
|
|
fcf570e96e | ||
|
|
cbc184e953 | ||
|
|
98f9063352 | ||
|
|
c278f1af00 | ||
|
|
fbb17a0ba5 | ||
|
|
8637a9823e | ||
|
|
d717fe7e8c | ||
|
|
5e8dfe7267 | ||
|
|
aaa4fbcdbd | ||
|
|
150f0cf486 | ||
|
|
711b220a05 | ||
|
|
b615c485f7 | ||
|
|
2f3b6b29ae | ||
|
|
7aecd83c4a | ||
|
|
3b460ab91f | ||
|
|
661c0eb970 | ||
|
|
f4a1f1fdc8 | ||
|
|
b5432c3728 | ||
|
|
6d0625c409 | ||
|
|
2f1ea08b8a | ||
|
|
a092da1943 | ||
|
|
43ef3cc562 | ||
|
|
047774475c | ||
|
|
91b8504df5 | ||
|
|
a4fb2dfcf8 | ||
|
|
ba16ec02e5 | ||
|
|
55b7af2623 | ||
|
|
b428eff10e | ||
|
|
35aee15b6d | ||
|
|
6f53c1bfde | ||
|
|
db0230ed75 | ||
|
|
7fa3c1e12a | ||
|
|
3b5993652f | ||
|
|
be4ad8947f | ||
|
|
2ead15e78e | ||
|
|
aa0740ff94 | ||
|
|
6d3f837820 | ||
|
|
359b82c29c | ||
|
|
b54171bc2c | ||
|
|
936b2131e0 | ||
|
|
aaef6a9e9c | ||
|
|
9327006e61 | ||
|
|
5225c841ae | ||
|
|
8464e319fd | ||
|
|
98b8bd90ea | ||
|
|
ae94993b09 | ||
|
|
72f10aaed1 | ||
|
|
4a74bae8c7 | ||
|
|
5164aec37b | ||
|
|
1581e9b1cd | ||
|
|
536e3ffb11 | ||
|
|
d6103191ba | ||
|
|
49e37a19a5 | ||
|
|
d02431e260 | ||
|
|
f24f6ead92 | ||
|
|
b7bdb7ae50 | ||
|
|
2dcf8ac96f | ||
|
|
5421c94853 | ||
|
|
10f6bc092a | ||
|
|
be9ea4ea8e | ||
|
|
f5c6fa842a | ||
|
|
e0943ce905 | ||
|
|
61e7df77a7 | ||
|
|
a5434360bc | ||
|
|
ba29c66e3b | ||
|
|
b3a72d55ae | ||
|
|
c382ad1cc8 | ||
|
|
363e28f323 | ||
|
|
d695656b8c | ||
|
|
31c6cbc0a2 | ||
|
|
b93284bc2f | ||
|
|
99c855b01b | ||
|
|
c2eaedc959 | ||
|
|
a631cd67ec | ||
|
|
66fccd3c68 | ||
|
|
9a4ebf4daa | ||
|
|
91c14211c6 | ||
|
|
1ea07c458b | ||
|
|
591add6e0c | ||
|
|
b99fff66df | ||
|
|
f54ba05b00 | ||
|
|
6596fb00c7 | ||
|
|
df0b5a80dc | ||
|
|
8cdbb31dbe | ||
|
|
0be4b1222d | ||
|
|
18a6bfd73a | ||
|
|
5e2271b237 | ||
|
|
798999d490 | ||
|
|
0e68c5e8bc | ||
|
|
9694af82f4 | ||
|
|
28a4386975 | ||
|
|
a238f7beba | ||
|
|
75ba16281b | ||
|
|
ad65a278d4 | ||
|
|
a2320b3f8d | ||
|
|
ae12853ad0 | ||
|
|
68adf6dc2f | ||
|
|
f88989bd4b | ||
|
|
77ef3847ce | ||
|
|
b8291837fc | ||
|
|
423a7f951a | ||
|
|
557f5fe364 | ||
|
|
5308970ad8 | ||
|
|
6b368953f4 | ||
|
|
2d0b63c29d | ||
|
|
34e941c8cb | ||
|
|
76cf007fff | ||
|
|
321963be83 | ||
|
|
c733bda6c3 | ||
|
|
e67b50b976 | ||
|
|
9e3c080909 | ||
|
|
cb661aaebd | ||
|
|
573571978c | ||
|
|
dc492a2a0a | ||
|
|
e47747dd0e | ||
|
|
4de944b41e | ||
|
|
5f7a1fa5cd | ||
|
|
b8ad328cde | ||
|
|
3076fd19c1 | ||
|
|
fac437b8c1 | ||
|
|
697c25015e | ||
|
|
d83b404fc3 | ||
|
|
ab7313cbc4 | ||
|
|
1b8d70322b | ||
|
|
844ffab4ed | ||
|
|
f5d0513d1f | ||
|
|
557abd4285 | ||
|
|
f4f1a0fbc6 | ||
|
|
74e10dc012 | ||
|
|
359812b7ed | ||
|
|
c2449e53c4 | ||
|
|
1a91249da2 | ||
|
|
b9162f9576 | ||
|
|
f726e8a7b3 | ||
|
|
2f284cfdc9 | ||
|
|
b74696adba | ||
|
|
c8ccba0192 | ||
|
|
41b0e05f62 | ||
|
|
847d5f1b3b | ||
|
|
0445f9dfc2 | ||
|
|
3001c780bd | ||
|
|
68ad5e2320 | ||
|
|
b5e229b19c | ||
|
|
453d798fcf | ||
|
|
451afdb660 | ||
|
|
d298bda92c | ||
|
|
fd99ba6255 | ||
|
|
6ade0b2b1a | ||
|
|
9902003da9 | ||
|
|
0ff2fcac11 |
@@ -5,3 +5,9 @@ charset = utf-8
|
|||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.rs]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_size = 4
|
||||||
|
insert_final_newline = true
|
||||||
|
|||||||
56
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
name: 问题反馈 / Bug report
|
||||||
|
title: "[BUG] "
|
||||||
|
description: 反馈你遇到的问题 / Report the issue you are experiencing
|
||||||
|
labels: ["bug"]
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## 在提交问题之前,请确认以下事项:
|
||||||
|
1. 请 **确保** 您已经查阅了 [Clash Verge Rev 官方文档](https://clash-verge-rev.github.io/guide.html) 以及 [常见问题](https://clash-verge-rev.github.io/faq/install/)
|
||||||
|
2. 请 **确保** [已有的问题](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue) 中没有人提交过相似issue,否则请在已有的issue下进行讨论
|
||||||
|
3. 请 **务必** 给issue填写一个简洁明了的标题,以便他人快速检索
|
||||||
|
4. 请 **务必** 先下载 [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) 版本测试,确保问题依然存在
|
||||||
|
5. 请 **务必** 按照模板规范详细描述问题,否则issue将会被关闭
|
||||||
|
## Before submitting the issue, please make sure of the following checklist:
|
||||||
|
1. Please make sure you have read the [Clash Verge Rev official documentation](https://clash-verge-rev.github.io/guide.html) and [FAQ](https://clash-verge-rev.github.io/faq/install/)
|
||||||
|
2. Please make sure there is no similar issue in the [existing issues](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue), otherwise please discuss under the existing issue
|
||||||
|
3. Please be sure to fill in a concise and clear title for the issue so that others can quickly search
|
||||||
|
4. Please be sure to download the [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) version for testing to ensure that the problem still exists
|
||||||
|
5. Please describe the problem in detail according to the template specification, otherwise the issue will be closed
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: 问题描述 / Describe the bug
|
||||||
|
description: 详细清晰地描述你遇到的问题 / A clear and concise description of what the bug is
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: 复现步骤 / To Reproduce
|
||||||
|
description: 请提供复现问题的步骤 / Steps to reproduce the behavior
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: 操作系统 / OS
|
||||||
|
options:
|
||||||
|
- Windows
|
||||||
|
- Linux
|
||||||
|
- MacOS
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: 操作系统版本 / OS Version
|
||||||
|
description: 请提供你的操作系统版本,Linux请额外提供桌面环境及窗口系统 / Please provide your OS version, for Linux, please also provide the desktop environment and window system
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: 日志 / Log
|
||||||
|
description: 请提供完整或相关部分的Debug日志 / Please provide the complete or relevant part of the Debug log
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
4
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
contact_links:
|
||||||
|
- name: 讨论交流 / Communication
|
||||||
|
url: https://t.me/clash_verge_rev
|
||||||
|
about: 在 Telegram 群组中与其他用户讨论交流 / Communicate with other users in the Telegram group
|
||||||
35
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
name: 功能请求 / Feature request
|
||||||
|
title: "[Feature] "
|
||||||
|
description: 提出你的功能请求 / Propose your feature request
|
||||||
|
labels: ["enhancement"]
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## 在提交问题之前,请确认以下事项:
|
||||||
|
1. 请 **确保** 您已经查阅了 [Clash Verge Rev 官方文档](https://clash-verge-rev.github.io/guide.html) 确认软件不存在类似的功能
|
||||||
|
2. 请 **确保** [已有的问题](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue) 中没有人提交过相似issue,否则请在已有的issue下进行讨论
|
||||||
|
3. 请 **务必** 给issue填写一个简洁明了的标题,以便他人快速检索
|
||||||
|
4. 请 **务必** 先下载 [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) 版本测试,确保该功能还未实现
|
||||||
|
5. 请 **务必** 按照模板规范详细描述问题,否则issue将会被关闭
|
||||||
|
## Before submitting the issue, please make sure of the following checklist:
|
||||||
|
1. Please make sure you have read the [Clash Verge Rev official documentation](https://clash-verge-rev.github.io/guide.html) to confirm that the software does not have similar functions
|
||||||
|
2. Please make sure there is no similar issue in the [existing issues](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue), otherwise please discuss under the existing issue
|
||||||
|
3. Please be sure to fill in a concise and clear title for the issue so that others can quickly search
|
||||||
|
4. Please be sure to download the [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) version for testing to ensure that the function has not been implemented
|
||||||
|
5. Please describe the problem in detail according to the template specification, otherwise the issue will be closed
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: 功能描述 / Feature description
|
||||||
|
description: 详细清晰地描述你的功能请求 / A clear and concise description of what the feature is
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: 使用场景 / Use case
|
||||||
|
description: 请描述你的功能请求的使用场景 / Please describe the use case of your feature request
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
4
.github/build-for-linux/Dockerfile
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
FROM rust:buster
|
||||||
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
|
RUN chmod a+x /entrypoint.sh
|
||||||
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
14
.github/build-for-linux/action.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
name: "Build for Linux"
|
||||||
|
branding:
|
||||||
|
icon: user-check
|
||||||
|
color: gray-dark
|
||||||
|
inputs:
|
||||||
|
target:
|
||||||
|
required: true
|
||||||
|
description: "Rust Target"
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: "docker"
|
||||||
|
image: "Dockerfile"
|
||||||
|
args:
|
||||||
|
- ${{ inputs.target }}
|
||||||
8
.github/build-for-linux/build.sh
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
pnpm install
|
||||||
|
pnpm check $INPUT_TARGET
|
||||||
|
sed -i "s/#openssl/openssl={version=\"0.10\",features=[\"vendored\"]}/g" src-tauri/Cargo.toml
|
||||||
|
if [ "$INPUT_TARGET" = "x86_64-unknown-linux-gnu" ]; then
|
||||||
|
pnpm build --target $INPUT_TARGET
|
||||||
|
else
|
||||||
|
pnpm build --target $INPUT_TARGET -b deb
|
||||||
|
fi
|
||||||
47
.github/build-for-linux/entrypoint.sh
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
wget https://nodejs.org/dist/v20.10.0/node-v20.10.0-linux-x64.tar.xz
|
||||||
|
tar -Jxvf ./node-v20.10.0-linux-x64.tar.xz
|
||||||
|
export PATH=$(pwd)/node-v20.10.0-linux-x64/bin:$PATH
|
||||||
|
npm install pnpm -g
|
||||||
|
|
||||||
|
rustup target add "$INPUT_TARGET"
|
||||||
|
|
||||||
|
if [ "$INPUT_TARGET" = "x86_64-unknown-linux-gnu" ]; then
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||||
|
elif [ "$INPUT_TARGET" = "aarch64-unknown-linux-gnu" ]; then
|
||||||
|
dpkg --add-architecture arm64
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y libncurses6:arm64 libtinfo6:arm64 linux-libc-dev:arm64 libncursesw6:arm64 libssl3:arm64 libcups2:arm64
|
||||||
|
apt-get install -y --no-install-recommends g++-aarch64-linux-gnu libc6-dev-arm64-cross libwebkit2gtk-4.0-dev:arm64 libgtk-3-dev:arm64 patchelf:arm64 librsvg2-dev:arm64 libayatana-appindicator3-dev:arm64
|
||||||
|
export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc
|
||||||
|
export CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc
|
||||||
|
export CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++
|
||||||
|
export PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig
|
||||||
|
export PKG_CONFIG_ALLOW_CROSS=1
|
||||||
|
elif [ "$INPUT_TARGET" = "armv7-unknown-linux-gnueabihf" ]; then
|
||||||
|
dpkg --add-architecture armhf
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y libncurses6:armhf libtinfo6:armhf linux-libc-dev:armhf libncursesw6:armhf libssl3:armhf libcups2:armhf
|
||||||
|
apt-get install -y --no-install-recommends g++-arm-linux-gnueabihf libc6-dev-armhf-cross libwebkit2gtk-4.0-dev:armhf libgtk-3-dev:armhf patchelf:armhf librsvg2-dev:armhf libayatana-appindicator3-dev:armhf
|
||||||
|
export CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER=arm-linux-gnueabihf-gcc
|
||||||
|
export CC_armv7_unknown_linux_gnueabihf=arm-linux-gnueabihf-gcc
|
||||||
|
export CXX_armv7_unknown_linux_gnueabihf=arm-linux-gnueabihf-g++
|
||||||
|
export PKG_CONFIG_PATH=/usr/lib/arm-linux-gnueabihf/pkgconfig
|
||||||
|
export PKG_CONFIG_ALLOW_CROSS=1
|
||||||
|
elif [ "$INPUT_TARGET" = "riscv64gc-unknown-linux-gnu" ]; then
|
||||||
|
dpkg --add-architecture riscv64
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y libncurses6:riscv64 libtinfo6:riscv64 linux-libc-dev:riscv64 libncursesw6:riscv64 libssl3:riscv64 libcups2:riscv64
|
||||||
|
apt-get install -y --no-install-recommends g++-riscv64-linux-gnu libc6-dev-riscv64-cross libwebkit2gtk-4.0-dev:riscv64 libgtk-3-dev:riscv64 patchelf:riscv64 librsvg2-dev:riscv64 libayatana-appindicator3-dev:riscv64
|
||||||
|
export CARGO_TARGET_RISCV64_UNKNOWN_LINUX_GNU_LINKER=riscv64-linux-gnu-gcc
|
||||||
|
export CC_riscv64_unknown_linux_gnu=riscv64-linux-gnu-gcc
|
||||||
|
export CXX_riscv64_unknown_linux_gnu=riscv64-linux-gnu-g++
|
||||||
|
export PKG_CONFIG_PATH=/usr/lib/riscv64-linux-gnu/pkgconfig
|
||||||
|
export PKG_CONFIG_ALLOW_CROSS=1
|
||||||
|
else
|
||||||
|
echo "Unknown target: $INPUT_TARGET" && exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
bash .github/build-for-linux/build.sh
|
||||||
205
.github/workflows/alpha.yml
vendored
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
name: Alpha Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
tags-ignore: [updater, alpha]
|
||||||
|
permissions: write-all
|
||||||
|
env:
|
||||||
|
CARGO_INCREMENTAL: 0
|
||||||
|
RUST_BACKTRACE: short
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
alpha:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: windows-latest
|
||||||
|
target: x86_64-pc-windows-msvc
|
||||||
|
- os: windows-latest
|
||||||
|
target: aarch64-pc-windows-msvc
|
||||||
|
- os: macos-latest
|
||||||
|
target: aarch64-apple-darwin
|
||||||
|
- os: macos-latest
|
||||||
|
target: x86_64-apple-darwin
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Rust Stable
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Add Rust Target
|
||||||
|
run: rustup target add ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Rust Cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: src-tauri
|
||||||
|
|
||||||
|
- name: Install Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "20"
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v3
|
||||||
|
name: Install pnpm
|
||||||
|
with:
|
||||||
|
version: 9
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Pnpm install and check
|
||||||
|
run: |
|
||||||
|
pnpm i
|
||||||
|
pnpm check ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Tauri build
|
||||||
|
uses: tauri-apps/tauri-action@v0
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
|
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
|
with:
|
||||||
|
tagName: alpha
|
||||||
|
releaseName: "Clash Verge Rev Alpha"
|
||||||
|
releaseBody: "More new features are now supported."
|
||||||
|
releaseDraft: false
|
||||||
|
prerelease: true
|
||||||
|
tauriScript: pnpm
|
||||||
|
args: --target ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Portable Bundle
|
||||||
|
if: matrix.os == 'windows-latest'
|
||||||
|
run: pnpm portable ${{ matrix.target }} --alpha
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
|
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
|
|
||||||
|
alpha-for-linux:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: ubuntu-latest
|
||||||
|
target: x86_64-unknown-linux-gnu
|
||||||
|
- os: ubuntu-latest
|
||||||
|
target: aarch64-unknown-linux-gnu
|
||||||
|
- os: ubuntu-latest
|
||||||
|
target: armv7-unknown-linux-gnueabihf
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Build for Linux
|
||||||
|
uses: ./.github/build-for-linux
|
||||||
|
env:
|
||||||
|
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
|
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
|
with:
|
||||||
|
target: ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Get Version
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install jq
|
||||||
|
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
|
||||||
|
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- run: |
|
||||||
|
cat > release.txt << 'EOF'
|
||||||
|
### 我应该下载哪个版本?
|
||||||
|
|
||||||
|
- Windows x86_64架构: x64-setup.exe (不支持win7)
|
||||||
|
- Windows arm64架构: arm64-setup.exe
|
||||||
|
- MacOS intel芯片: x64.dmg
|
||||||
|
- MacOS apple M芯片: aarch64.dmg (提示文件损坏看下面FAQ)
|
||||||
|
- Linux x64架构: amd64.AppImage/amd64.deb
|
||||||
|
- Linux arm64架构: arm64.deb
|
||||||
|
- Linux armv7架构: armhf.deb
|
||||||
|
- Windows 便携板 x86_64架构: x64_portable.zip (不推荐使用,无法自动更新)
|
||||||
|
- Windows 便携板 arm64架构: arm64_portable.zip (不推荐使用,无法自动更新)
|
||||||
|
|
||||||
|
### FAQ
|
||||||
|
|
||||||
|
- [FAQ](https://clash-verge-rev.github.io/faq/install/)
|
||||||
|
|
||||||
|
Created at ${{ env.BUILDTIME }}.
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: Upload Release
|
||||||
|
if: startsWith(matrix.target, 'x86_64')
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
tag_name: alpha
|
||||||
|
name: "Clash Verge Rev Alpha"
|
||||||
|
body_path: release.txt
|
||||||
|
prerelease: true
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
files: src-tauri/target/${{ matrix.target }}/release/bundle/appimage/*.AppImage*
|
||||||
|
|
||||||
|
- name: Upload Release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
tag_name: alpha
|
||||||
|
name: "Clash Verge Rev Alpha"
|
||||||
|
body_path: release.txt
|
||||||
|
prerelease: true
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
files: src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
|
||||||
|
|
||||||
|
update_tag:
|
||||||
|
name: Update tag
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [alpha, alpha-for-linux]
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set Env
|
||||||
|
run: |
|
||||||
|
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Update Tag
|
||||||
|
uses: richardsimko/update-tag@v1
|
||||||
|
with:
|
||||||
|
tag_name: alpha
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- run: |
|
||||||
|
cat > release.txt << 'EOF'
|
||||||
|
### 我应该下载哪个版本?
|
||||||
|
|
||||||
|
- Windows x86_64架构: x64-setup.exe (不支持win7)
|
||||||
|
- Windows arm64架构: arm64-setup.exe
|
||||||
|
- MacOS intel芯片: x64.dmg
|
||||||
|
- MacOS apple M芯片: aarch64.dmg (提示文件损坏看下面FAQ)
|
||||||
|
- Linux x64架构: amd64.AppImage/amd64.deb
|
||||||
|
- Linux arm64架构: arm64.deb
|
||||||
|
- Linux armv7架构: armhf.deb
|
||||||
|
- Windows 便携板 x86_64架构: x64_portable.zip (不推荐使用,无法自动更新)
|
||||||
|
- Windows 便携板 arm64架构: arm64_portable.zip (不推荐使用,无法自动更新)
|
||||||
|
|
||||||
|
### FAQ
|
||||||
|
|
||||||
|
- [FAQ](https://clash-verge-rev.github.io/faq/install/)
|
||||||
|
|
||||||
|
Created at ${{ env.BUILDTIME }}.
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: Upload Release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
tag_name: alpha
|
||||||
|
name: "Clash Verge Rev Alpha"
|
||||||
|
body_path: release.txt
|
||||||
|
prerelease: true
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
generate_release_notes: true
|
||||||
112
.github/workflows/ci.yml
vendored
@@ -1,112 +0,0 @@
|
|||||||
name: Release CI
|
|
||||||
|
|
||||||
on: [push]
|
|
||||||
|
|
||||||
env:
|
|
||||||
CARGO_INCREMENTAL: 0
|
|
||||||
RUST_BACKTRACE: short
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os: [windows-latest, ubuntu-latest, macos-latest]
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
if: |
|
|
||||||
startsWith(github.repository, 'zzzgydi') &&
|
|
||||||
startsWith(github.ref, 'refs/tags/v')
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Install Rust
|
|
||||||
uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: stable
|
|
||||||
profile: minimal
|
|
||||||
override: true
|
|
||||||
|
|
||||||
- name: Rust Cache
|
|
||||||
uses: Swatinem/rust-cache@ce325b60658c1b38465c06cc965b79baf32c1e72
|
|
||||||
|
|
||||||
- name: Install Node
|
|
||||||
uses: actions/setup-node@v1
|
|
||||||
with:
|
|
||||||
node-version: 14
|
|
||||||
|
|
||||||
- name: Install Dependencies (ubuntu only)
|
|
||||||
if: matrix.os == 'ubuntu-latest'
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf
|
|
||||||
|
|
||||||
- name: Get yarn cache dir path
|
|
||||||
id: yarn-cache-dir-path
|
|
||||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
|
||||||
|
|
||||||
- name: Yarn Cache
|
|
||||||
uses: actions/cache@v2
|
|
||||||
id: yarn-cache
|
|
||||||
with:
|
|
||||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
|
||||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-yarn-
|
|
||||||
|
|
||||||
- name: Yarn install and check
|
|
||||||
run: |
|
|
||||||
yarn install --network-timeout 1000000
|
|
||||||
yarn run check
|
|
||||||
|
|
||||||
- name: Tauri build
|
|
||||||
uses: tauri-apps/tauri-action@0e558392ccadcb49bcc89e7df15a400e8f0c954d
|
|
||||||
# enable cache even though failed
|
|
||||||
# continue-on-error: true
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
|
||||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
|
||||||
with:
|
|
||||||
tagName: v__VERSION__
|
|
||||||
releaseName: "Clash Verge v__VERSION__"
|
|
||||||
releaseBody: "More new features are now supported."
|
|
||||||
releaseDraft: false
|
|
||||||
prerelease: true
|
|
||||||
|
|
||||||
- name: Green zip bundle
|
|
||||||
if: matrix.os == 'windows-latest'
|
|
||||||
run: |
|
|
||||||
yarn run green
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
release-update:
|
|
||||||
needs: release
|
|
||||||
runs-on: macos-latest
|
|
||||||
if: |
|
|
||||||
startsWith(github.repository, 'zzzgydi') &&
|
|
||||||
startsWith(github.ref, 'refs/tags/v')
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Get yarn cache dir path
|
|
||||||
id: yarn-cache-dir-path
|
|
||||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
|
||||||
|
|
||||||
- name: Yarn Cache
|
|
||||||
uses: actions/cache@v2
|
|
||||||
id: yarn-cache
|
|
||||||
with:
|
|
||||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
|
||||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-yarn-
|
|
||||||
|
|
||||||
- name: Yarn install
|
|
||||||
run: yarn install
|
|
||||||
|
|
||||||
- name: Release updater file
|
|
||||||
run: yarn run updater
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
153
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
name: Release Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
permissions: write-all
|
||||||
|
env:
|
||||||
|
CARGO_INCREMENTAL: 0
|
||||||
|
RUST_BACKTRACE: short
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: windows-latest
|
||||||
|
target: x86_64-pc-windows-msvc
|
||||||
|
- os: windows-latest
|
||||||
|
target: aarch64-pc-windows-msvc
|
||||||
|
- os: macos-latest
|
||||||
|
target: aarch64-apple-darwin
|
||||||
|
- os: macos-latest
|
||||||
|
target: x86_64-apple-darwin
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Rust Stable
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Add Rust Target
|
||||||
|
run: rustup target add ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Rust Cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: src-tauri
|
||||||
|
|
||||||
|
- name: Install Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "20"
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v3
|
||||||
|
name: Install pnpm
|
||||||
|
with:
|
||||||
|
version: 9
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Pnpm install and check
|
||||||
|
run: |
|
||||||
|
pnpm i
|
||||||
|
pnpm check ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Tauri build
|
||||||
|
uses: tauri-apps/tauri-action@v0
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
|
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
|
with:
|
||||||
|
tagName: v__VERSION__
|
||||||
|
releaseName: "Clash Verge Rev v__VERSION__"
|
||||||
|
releaseBody: "More new features are now supported."
|
||||||
|
releaseDraft: false
|
||||||
|
prerelease: false
|
||||||
|
tauriScript: pnpm
|
||||||
|
args: --target ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Portable Bundle
|
||||||
|
if: matrix.os == 'windows-latest'
|
||||||
|
run: pnpm portable ${{ matrix.target }}
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
|
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
|
|
||||||
|
release-for-linux:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: ubuntu-latest
|
||||||
|
target: x86_64-unknown-linux-gnu
|
||||||
|
- os: ubuntu-latest
|
||||||
|
target: aarch64-unknown-linux-gnu
|
||||||
|
- os: ubuntu-latest
|
||||||
|
target: armv7-unknown-linux-gnueabihf
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Build for Linux
|
||||||
|
uses: ./.github/build-for-linux
|
||||||
|
env:
|
||||||
|
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
|
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
|
with:
|
||||||
|
target: ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Get Version
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install jq
|
||||||
|
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Upload Release
|
||||||
|
if: startsWith(matrix.target, 'x86_64')
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
tag_name: v${{env.VERSION}}
|
||||||
|
name: "Clash Verge Rev v${{env.VERSION}}"
|
||||||
|
body: "More new features are now supported."
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
files: src-tauri/target/${{ matrix.target }}/release/bundle/appimage/*.AppImage*
|
||||||
|
|
||||||
|
- name: Upload Release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
tag_name: v${{env.VERSION}}
|
||||||
|
name: "Clash Verge Rev v${{env.VERSION}}"
|
||||||
|
body: "More new features are now supported."
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
files: src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
|
||||||
|
|
||||||
|
release-update:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [release, release-for-linux]
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Install Node
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: "20"
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v2
|
||||||
|
name: Install pnpm
|
||||||
|
with:
|
||||||
|
version: 8
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Pnpm install
|
||||||
|
run: pnpm i
|
||||||
|
|
||||||
|
- name: Release updater file
|
||||||
|
run: pnpm updater
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
33
.github/workflows/updater.yml
vendored
@@ -1,32 +1,29 @@
|
|||||||
name: Updater CI
|
name: Updater CI
|
||||||
|
|
||||||
on: workflow_dispatch
|
on: workflow_dispatch
|
||||||
|
permissions: write-all
|
||||||
jobs:
|
jobs:
|
||||||
release-update:
|
release-update:
|
||||||
runs-on: macos-latest
|
runs-on: ubuntu-latest
|
||||||
if: startsWith(github.repository, 'zzzgydi')
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Get yarn cache dir path
|
- name: Install Node
|
||||||
id: yarn-cache-dir-path
|
uses: actions/setup-node@v3
|
||||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
|
||||||
|
|
||||||
- name: Yarn Cache
|
|
||||||
uses: actions/cache@v2
|
|
||||||
id: yarn-cache
|
|
||||||
with:
|
with:
|
||||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
node-version: "20"
|
||||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-yarn-
|
|
||||||
|
|
||||||
- name: Yarn install
|
- uses: pnpm/action-setup@v2
|
||||||
run: yarn install
|
name: Install pnpm
|
||||||
|
with:
|
||||||
|
version: 8
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Pnpm install
|
||||||
|
run: pnpm i
|
||||||
|
|
||||||
- name: Release updater file
|
- name: Release updater file
|
||||||
run: yarn run updater
|
run: pnpm updater
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
4
.gitignore
vendored
@@ -1,6 +1,10 @@
|
|||||||
node_modules
|
node_modules
|
||||||
|
.pnpm-store
|
||||||
.DS_Store
|
.DS_Store
|
||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
update.json
|
update.json
|
||||||
|
scripts/_env.sh
|
||||||
|
.vscode
|
||||||
|
.tool-versions
|
||||||
2
.husky/pre-commit
Normal file → Executable file
@@ -1,4 +1,4 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
. "$(dirname "$0")/_/husky.sh"
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
yarn pretty-quick --staged
|
pnpm pretty-quick --staged
|
||||||
|
|||||||
1
.tool-versions
Normal file
@@ -0,0 +1 @@
|
|||||||
|
nodejs 21.7.1
|
||||||
67
CONTRIBUTING.md
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# CONTRIBUTING
|
||||||
|
|
||||||
|
Thank you for your interest in contributing to Clash Verge Rev! This document provides guidelines and instructions to help you set up your development environment and start contributing.
|
||||||
|
|
||||||
|
## Development Setup
|
||||||
|
|
||||||
|
Before you start contributing to the project, you need to set up your development environment. Here are the steps you need to follow:
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
1. **Install Rust and Node.js**: Our project requires both Rust and Node.js. Please follow the instructions provided [here](https://tauri.app/v1/guides/getting-started/prerequisites) to install them on your system.
|
||||||
|
|
||||||
|
### Setup for Windows Users
|
||||||
|
|
||||||
|
If you're a Windows user, you may need to perform some additional steps:
|
||||||
|
|
||||||
|
- Make sure to add Rust and Node.js to your system's PATH. This is usually done during the installation process, but you can verify and manually add them if necessary.
|
||||||
|
- The gnu `patch` tool should be installed
|
||||||
|
|
||||||
|
### Install Node.js Packages
|
||||||
|
|
||||||
|
After installing Rust and Node.js, install the necessary Node.js packages:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
pnpm i
|
||||||
|
```
|
||||||
|
|
||||||
|
### Download the Clash Binary
|
||||||
|
|
||||||
|
You have two options for downloading the clash binary:
|
||||||
|
|
||||||
|
- Automatically download it via the provided script:
|
||||||
|
```shell
|
||||||
|
pnpm run check
|
||||||
|
# Use '--force' to force update to the latest version
|
||||||
|
# pnpm run check --force
|
||||||
|
```
|
||||||
|
- Manually download it from the [Clash Meta release](https://github.com/MetaCubeX/Clash.Meta/releases). After downloading, rename the binary according to the [Tauri configuration](https://tauri.app/v1/api/config#bundleconfig.externalbin).
|
||||||
|
|
||||||
|
### Run the Development Server
|
||||||
|
|
||||||
|
To run the development server, use the following command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
pnpm dev
|
||||||
|
# If an app instance already exists, use a different command
|
||||||
|
pnpm dev:diff
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build the Project
|
||||||
|
|
||||||
|
If you want to build the project, use:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing Your Changes
|
||||||
|
|
||||||
|
Once you have made your changes:
|
||||||
|
|
||||||
|
1. Fork the repository.
|
||||||
|
2. Create a new branch for your feature or bug fix.
|
||||||
|
3. Commit your changes with clear and concise commit messages.
|
||||||
|
4. Push your branch to your fork and submit a pull request to our repository.
|
||||||
|
|
||||||
|
We appreciate your contributions and look forward to your active participation in our project!
|
||||||
95
README.md
@@ -1,83 +1,80 @@
|
|||||||
<h1 align="center">
|
<h1 align="center">
|
||||||
<img src="./src/assets/image/logo.png" alt="Clash" width="128" />
|
<img src="./src/assets/image/logo.png" alt="Clash" width="128" />
|
||||||
<br>
|
<br>
|
||||||
Clash Verge
|
Continuation of <a href="https://github.com/zzzgydi/clash-verge">Clash Verge</a>
|
||||||
<br>
|
<br>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<h3 align="center">
|
<h3 align="center">
|
||||||
A <a href="https://github.com/Dreamacro/clash">Clash</a> GUI based on <a href="https://github.com/tauri-apps/tauri">tauri</a>.
|
A Clash Meta GUI based on <a href="https://github.com/tauri-apps/tauri">Tauri</a>.
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
## Features
|
## Preview
|
||||||
|
|
||||||
- Full `clash` config supported, Partial `clash premium` config supported.
|

|
||||||
- Profiles management and enhancement (by yaml and Javascript). [Doc](https://github.com/zzzgydi/clash-verge/issues/12)
|
|
||||||
- System proxy setting and guard.
|
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
Download from [release](https://github.com/zzzgydi/clash-verge/releases). Supports Windows x64, Linux x86_64 and macOS 11+
|
请到发布页面下载对应的安装包:[Release page](https://github.com/clash-verge-rev/clash-verge-rev/releases)<br>
|
||||||
|
Go to the [release page](https://github.com/clash-verge-rev/clash-verge-rev/releases) to download the corresponding installation package<br>
|
||||||
|
Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
|
||||||
|
|
||||||
Or you can build it yourself. Supports Windows, Linux and macOS 10.15+
|
### 安装说明和常见问题,请到[文档页](https://clash-verge-rev.github.io/)查看:[Doc](https://clash-verge-rev.github.io/)
|
||||||
|
|
||||||
Notes: If you could not start the app on Windows, please check that you have [Webview2](https://developer.microsoft.com/en-us/microsoft-edge/webview2/#download-section) installed.
|
---
|
||||||
|
|
||||||
|
### TG Group: [@clash_verge_rev](https://t.me/clash_verge_rev)
|
||||||
|
|
||||||
|
## Promotion
|
||||||
|
|
||||||
|
[狗狗加速 —— 技术流机场 Doggygo VPN](https://狗狗加速.com)
|
||||||
|
|
||||||
|
- 高性能海外机场,免费试用,优惠套餐,解锁流媒体,全球首家支持 Hysteria 协议。
|
||||||
|
- 使用 Clash Verge 专属邀请链接注册送 3 天,每天 1G 流量免费试用:https://verge.狗狗加速.com/#/register?code=oaxsAGo6
|
||||||
|
- Clash Verge 专属 8 折优惠码: verge20 (仅有 500 份)
|
||||||
|
- 优惠套餐每月仅需 15.8 元,160G 流量,年付 8 折
|
||||||
|
- 海外团队,无跑路风险,高达 50% 返佣
|
||||||
|
- 集群负载均衡设计,高速专线(兼容老客户端),极低延迟,无视晚高峰,4K 秒开
|
||||||
|
- 全球首家 Hysteria 协议机场,现已上线更快的 `Hysteria2` 协议(Clash Verge 客户端最佳搭配)
|
||||||
|
- 解锁流媒体及 ChatGPT
|
||||||
|
- 官网:https://狗狗加速.com
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Since the clash core has been removed. The project no longer maintains the clash core, but only the Clash Meta core.
|
||||||
|
- Profiles management and enhancement (by yaml and Javascript). [Doc](https://clash-verge-rev.github.io)
|
||||||
|
- Improved UI and supports custom theme color.
|
||||||
|
- Built-in support [Clash.Meta(mihomo)](https://github.com/MetaCubeX/mihomo) core.
|
||||||
|
- System proxy setting and guard.
|
||||||
|
|
||||||
|
### FAQ
|
||||||
|
|
||||||
|
Refer to [Doc FAQ Page](https://clash-verge-rev.github.io/faq/install/)
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
You should install Rust and Nodejs, see [here](https://tauri.studio/docs/getting-started/prerequisites) for more details. Then install Nodejs packages.
|
See [CONTRIBUTING.md](./CONTRIBUTING.md) for more details.
|
||||||
|
|
||||||
|
To run the development server, execute the following commands after all prerequisites for **Tauri** are installed:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
yarn install
|
pnpm i
|
||||||
|
pnpm run check
|
||||||
|
pnpm dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Then download the clash binary... Or you can download it from [clash premium release](https://github.com/Dreamacro/clash/releases/tag/premium) and rename it according to [tauri config](https://tauri.studio/docs/api/config/#tauri.bundle.externalBin).
|
|
||||||
|
|
||||||
```shell
|
|
||||||
yarn run check
|
|
||||||
```
|
|
||||||
|
|
||||||
Then run
|
|
||||||
|
|
||||||
```shell
|
|
||||||
yarn dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Or you can build it
|
|
||||||
|
|
||||||
```shell
|
|
||||||
yarn build
|
|
||||||
```
|
|
||||||
|
|
||||||
## Todos
|
|
||||||
|
|
||||||
> This keng is a little big...
|
|
||||||
|
|
||||||
## Screenshots
|
|
||||||
|
|
||||||
<div align="center">
|
|
||||||
<img src="./docs/demo1.png" alt="demo1" width="32%" />
|
|
||||||
<img src="./docs/demo2.png" alt="demo2" width="32%" />
|
|
||||||
<img src="./docs/demo3.png" alt="demo3" width="32%" />
|
|
||||||
<img src="./docs/demo4.png" alt="demo4" width="32%" />
|
|
||||||
<img src="./docs/demo5.png" alt="demo5" width="32%" />
|
|
||||||
<img src="./docs/demo6.png" alt="demo6" width="32%" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## Disclaimer
|
|
||||||
|
|
||||||
This is a learning project for Rust practice.
|
|
||||||
|
|
||||||
## Contributions
|
## Contributions
|
||||||
|
|
||||||
Issue and PR welcome!
|
Issue and PR welcome!
|
||||||
|
|
||||||
## Acknowledgement
|
## Acknowledgement
|
||||||
|
|
||||||
Clash Verge was based on or inspired by these projects and so on:
|
Clash Verge rev was based on or inspired by these projects and so on:
|
||||||
|
|
||||||
|
- [zzzgydi/clash-verge](https://github.com/zzzgydi/clash-verge): A Clash GUI based on tauri. Supports Windows, macOS and Linux.
|
||||||
- [tauri-apps/tauri](https://github.com/tauri-apps/tauri): Build smaller, faster, and more secure desktop applications with a web frontend.
|
- [tauri-apps/tauri](https://github.com/tauri-apps/tauri): Build smaller, faster, and more secure desktop applications with a web frontend.
|
||||||
- [Dreamacro/clash](https://github.com/Dreamacro/clash): A rule-based tunnel in Go.
|
- [Dreamacro/clash](https://github.com/Dreamacro/clash): A rule-based tunnel in Go.
|
||||||
|
- [MetaCubeX/mihomo](https://github.com/MetaCubeX/mihomo): A rule-based tunnel in Go.
|
||||||
- [Fndroid/clash_for_windows_pkg](https://github.com/Fndroid/clash_for_windows_pkg): A Windows/macOS GUI based on Clash.
|
- [Fndroid/clash_for_windows_pkg](https://github.com/Fndroid/clash_for_windows_pkg): A Windows/macOS GUI based on Clash.
|
||||||
- [vitejs/vite](https://github.com/vitejs/vite): Next generation frontend tooling. It's fast!
|
- [vitejs/vite](https://github.com/vitejs/vite): Next generation frontend tooling. It's fast!
|
||||||
|
|
||||||
|
|||||||
812
UPDATELOG.md
@@ -1,3 +1,815 @@
|
|||||||
|
## v1.6.1
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- 鼠标悬浮显示当前订阅的名称 [#938](https://github.com/clash-verge-rev/clash-verge-rev/pull/938)
|
||||||
|
- 日志过滤支持正则表达式 [#959](https://github.com/clash-verge-rev/clash-verge-rev/pull/959)
|
||||||
|
- 更新 Clash 内核到 1.18.4
|
||||||
|
|
||||||
|
### Bugs Fixes
|
||||||
|
|
||||||
|
- 修复 Linux KDE 环境下系统代理无法开启的问题
|
||||||
|
- 窗口最大化图标调整 [#924](https://github.com/clash-verge-rev/clash-verge-rev/pull/924)
|
||||||
|
- 修改 MacOS 托盘点击行为(左键菜单,右键点击事件)
|
||||||
|
- 修复 MacOS 服务模式安装失败的问题
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.6.0
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Meta(mihomo)内核回退 1.18.1(当前新版内核 hy2 协议有 bug,等修复后更新)
|
||||||
|
- 多处界面细节调整 [#724](https://github.com/clash-verge-rev/clash-verge-rev/pull/724) [#799](https://github.com/clash-verge-rev/clash-verge-rev/pull/799) [#900](https://github.com/clash-verge-rev/clash-verge-rev/pull/900) [#901](https://github.com/clash-verge-rev/clash-verge-rev/pull/901)
|
||||||
|
- Linux 下新增服务模式
|
||||||
|
- 新增订阅卡片右键可以打开机场首页
|
||||||
|
- url-test 支持手动选择、节点组 fixed 节点使用角标展示 [#840](https://github.com/clash-verge-rev/clash-verge-rev/pull/840)
|
||||||
|
- Clash 配置、Merge 配置提供 JSON Schema 语法支持、连接界面调整 [#887](https://github.com/clash-verge-rev/clash-verge-rev/pull/887)
|
||||||
|
- 修改 Merge 配置文件默认内容 [#889](https://github.com/clash-verge-rev/clash-verge-rev/pull/889)
|
||||||
|
- 修改 tun 模式默认 mtu 为 1500,老版本升级,需在 tun 模式设置下“重置为默认值”。
|
||||||
|
- 使用 npm 安装 meta-json-schema [#895](https://github.com/clash-verge-rev/clash-verge-rev/pull/895)
|
||||||
|
- 更新部分翻译 [#904](https://github.com/clash-verge-rev/clash-verge-rev/pull/904)
|
||||||
|
- 支持 ico 格式的任务栏图标
|
||||||
|
|
||||||
|
### Bugs Fixes
|
||||||
|
|
||||||
|
- 修复 Linux KDE 环境下系统代理无法开启的问题
|
||||||
|
- 修复延迟检测动画问题
|
||||||
|
- 窗口最大化图标调整 [#816](https://github.com/clash-verge-rev/clash-verge-rev/pull/816)
|
||||||
|
- 修复 Windows 某些情况下无法安装服务模式 [#822](https://github.com/clash-verge-rev/clash-verge-rev/pull/822)
|
||||||
|
- UI 细节修复 [#821](https://github.com/clash-verge-rev/clash-verge-rev/pull/821)
|
||||||
|
- 修复使用默认编辑器打开配置文件
|
||||||
|
- 修复内核文件在特定目录也可以更新的问题 [#857](https://github.com/clash-verge-rev/clash-verge-rev/pull/857)
|
||||||
|
- 修复服务模式的安装目录问题
|
||||||
|
- 修复删除配置文件的“更新间隔”出现的问题 [#907](https://github.com/clash-verge-rev/clash-verge-rev/issues/907)
|
||||||
|
|
||||||
|
### 已知问题(历史遗留问题,暂未找到有效解决方案)
|
||||||
|
|
||||||
|
- MacOS M 芯片下服务模式无法安装;临时解决方案:在内核 ⚙️ 下,手动授权,再打开 tun 模式。
|
||||||
|
- MacOS 下如果删除过网络配置,会导致无法正常打开系统代理;临时解决方案:使用浏览器代理插件或手动配置系统代理。
|
||||||
|
- Window 拨号连接下无法正确识别并打开系统代理;临时解决方案:使用浏览器代理插件或使用 tun 模式。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.5.11
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Meta(mihomo)内核更新 1.18.2
|
||||||
|
|
||||||
|
### Bugs Fixes
|
||||||
|
|
||||||
|
- 升级图标无法点击的问题
|
||||||
|
- 卸载时检查安装目录是否为空
|
||||||
|
- 代理界面图标重合的问题
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.5.10
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- 优化 Linux 托盘菜单显示
|
||||||
|
- 添加透明代理端口设置
|
||||||
|
- 删除订阅前确认
|
||||||
|
|
||||||
|
### Bugs Fixes
|
||||||
|
|
||||||
|
- 删除 MacOS 程序坞图标
|
||||||
|
- Windows 下 service 日志没有清理
|
||||||
|
- MacOS 无法开启系统代理
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.5.9
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- 缓存代理组图标
|
||||||
|
- 使用`boa_engine` 代替 `rquickjs`
|
||||||
|
- 支持 Linux armv7
|
||||||
|
|
||||||
|
### Bugs Fixes
|
||||||
|
|
||||||
|
- Windows 首次安装无法点击
|
||||||
|
- Windows 触摸屏无法拖动
|
||||||
|
- 规则列表 `REJECT-DROP` 颜色
|
||||||
|
- MacOS Dock 栏不显示图标
|
||||||
|
- MacOS 自定义字体无效
|
||||||
|
- 避免使用空 UA 拉取订阅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.5.8
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- 优化 UI 细节
|
||||||
|
- Linux 绘制窗口圆角
|
||||||
|
- 开放 DevTools
|
||||||
|
|
||||||
|
### Bugs Fixes
|
||||||
|
|
||||||
|
- 修复 MacOS 下开启 Tun 内核崩溃的问题
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.5.7
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- 优化 UI 各种细节
|
||||||
|
- 提供菜单栏图标样式切换选项(单色/彩色/禁用)
|
||||||
|
- 添加自动检查更新开关
|
||||||
|
- MacOS 开启 Tun 模式自动修改 DNS
|
||||||
|
- 调整可拖动区域(尝试修复触摸屏无法拖动的问题)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.5.6
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- 全新专属 Verge rev UI 界面 (by @Amnesiash) 及细节调整
|
||||||
|
- 提供允许无效证书的开关
|
||||||
|
- 删除不必要的快捷键
|
||||||
|
- Provider 更新添加动画
|
||||||
|
- Merge 支持 Provider
|
||||||
|
- 更换订阅框的粘贴按钮,删除默认的"Remote File" Profile 名称
|
||||||
|
- 链接菜单添加节点显示
|
||||||
|
|
||||||
|
### Bugs Fixes
|
||||||
|
|
||||||
|
- Linux 下图片显示错误
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.5.4
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- 支持自定义托盘图标
|
||||||
|
- 支持禁用代理组图标
|
||||||
|
- 代理组显示当前代理
|
||||||
|
- 修改 `打开面板` 快捷键为`打开/关闭面板`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.5.3
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Tun 设置添加重置按钮
|
||||||
|
|
||||||
|
### Bugs Fixes
|
||||||
|
|
||||||
|
- Tun 设置项显示错误的问题
|
||||||
|
- 修改一些默认值
|
||||||
|
- 启动时不更改启动项设置
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.5.2
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- 支持自定义延迟测试超时时间
|
||||||
|
- 优化 Tun 相关设置
|
||||||
|
|
||||||
|
### Bugs Fixes
|
||||||
|
|
||||||
|
- Merge 操作出错
|
||||||
|
- 安装后重启服务
|
||||||
|
- 修复管理员权限启动时开机启动失效的问题
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.5.1
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- 保存窗口最大化状态
|
||||||
|
- Proxy Provider 显示数量
|
||||||
|
- 不再提供 32 位安装包(因为 32 位经常出现各种奇怪问题,比如 tun 模式无法开启;现在系统也几乎没有 32 位了)
|
||||||
|
|
||||||
|
### Bugs Fixes
|
||||||
|
|
||||||
|
- 优化设置项名称
|
||||||
|
- 自定义 GLOBAL 代理组时代理组显示错误的问题
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.5.0
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- 删除 Clash 字段过滤功能
|
||||||
|
- 添加 socks 端口和 http 端口设置
|
||||||
|
- 升级内核到 1.18.1
|
||||||
|
|
||||||
|
### Bugs Fixes
|
||||||
|
|
||||||
|
- 修复 32 位版本无法显示流量信息的问题
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.4.11
|
||||||
|
|
||||||
|
### Break Changes
|
||||||
|
|
||||||
|
- 此版本更改了 Windows 安装包安装模式,需要卸载后手动安装,否则无法安装到正确位置
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- 优化了系统代理开启的代码,解决了稀有场景下代理开启卡顿的问题
|
||||||
|
- 添加 MacOS 下的 debug 日志,以便日后调试稀有场景下 MacOS 下无法开启系统代理的问题
|
||||||
|
- MacOS 关闭 GUI 时同步杀除后台 GUI [#306](https://github.com/clash-verge-rev/clash-verge-rev/issues/306)
|
||||||
|
|
||||||
|
### Bugs Fixes
|
||||||
|
|
||||||
|
- 解决自动更新时文件占用问题
|
||||||
|
- 解决稀有场景下系统代理开启失败的问题
|
||||||
|
- 删除冗余内核代码
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.4.10
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- 设置中添加退出按钮
|
||||||
|
- 支持自定义软件启动页
|
||||||
|
- 在 Proxy Provider 页面展示订阅信息
|
||||||
|
- 优化 Provider 支持
|
||||||
|
|
||||||
|
### Bugs Fixes
|
||||||
|
|
||||||
|
- 更改端口时立即重设系统代理
|
||||||
|
- 网站测试超时错误
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.4.9
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- 支持启动时运行脚本
|
||||||
|
- 支持代理组显示图标
|
||||||
|
- 新增测试页面
|
||||||
|
|
||||||
|
### Bugs Fixes
|
||||||
|
|
||||||
|
- 连接页面时间排序错误
|
||||||
|
- 连接页面表格宽度优化
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.4.8
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- 连接页面总流量显示
|
||||||
|
|
||||||
|
### Bugs Fixes
|
||||||
|
|
||||||
|
- 连接页面数据排序错误
|
||||||
|
- 新建订阅时设置更新间隔无效
|
||||||
|
- Windows 拨号网络无法设置系统代理
|
||||||
|
- Windows 开启/关闭系统代理延迟(使用注册表即可)
|
||||||
|
- 删除无效的背景模糊选项
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.4.7
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Windows 便携版禁用应用内更新
|
||||||
|
- 支持代理组 Hidden 选项
|
||||||
|
- 支持 URL Scheme(MacOS & Linux)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.4.6
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- 更新 Clash Meta(mihomo) 内核到 v1.18.0
|
||||||
|
- 支持 URL Scheme(暂时仅支持 Windows)
|
||||||
|
- 添加窗口置顶按钮
|
||||||
|
- UI 优化调整
|
||||||
|
|
||||||
|
### Bugs Fixes
|
||||||
|
|
||||||
|
- 修复一些编译错误
|
||||||
|
- 获取订阅名称错误
|
||||||
|
- 订阅信息解析错误
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.4.5
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- 更新 MacOS 托盘图标样式(@gxx2778 贡献)
|
||||||
|
|
||||||
|
### Bugs Fixes
|
||||||
|
|
||||||
|
- Windows 下更新时无法覆盖`clash-verge-service.exe`的问题(需要卸载重装一次服务,下次更新生效)
|
||||||
|
- 窗口最大化按钮变化问题
|
||||||
|
- 窗口尺寸保存错误问题
|
||||||
|
- 复制环境变量类型无法切换问题
|
||||||
|
- 某些情况下闪退的问题
|
||||||
|
- 某些订阅无法导入的问题
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.4.4
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- 支持 Windows aarch64(arm64) 版本
|
||||||
|
- 支持一键更新 GeoData
|
||||||
|
- 支持一键更新 Alpha 内核
|
||||||
|
- MacOS 支持在系统代理时显示不同的托盘图标
|
||||||
|
- Linux 支持在系统代理时显示不同的托盘图标
|
||||||
|
- 优化复制环境变量逻辑
|
||||||
|
|
||||||
|
### Bugs Fixes
|
||||||
|
|
||||||
|
- 修改 PID 文件的路径
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
- 优化创建窗口的速度
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.4.3
|
||||||
|
|
||||||
|
### Break Changes
|
||||||
|
|
||||||
|
- 更改配置文件路径到标准目录(可以保证卸载时没有残留)
|
||||||
|
- 更改 appid 为 `io.github.clash-verge-rev.clash-verge-rev`
|
||||||
|
- 建议卸载旧版本后再安装新版本,该版本安装后不会使用旧版配置文件,你可以手动将旧版配置文件迁移到新版配置文件目录下
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- 移除页面切换动画
|
||||||
|
- 更改 Tun 模式托盘图标颜色
|
||||||
|
- Portable 版本默认使用当前目录作为配置文件目录
|
||||||
|
- 禁用 Clash 字段过滤时隐藏 Clash 字段选项
|
||||||
|
- 优化拖拽时光标样式
|
||||||
|
|
||||||
|
### Bugs Fixes
|
||||||
|
|
||||||
|
- 修复 windows 下更新时没有关闭内核导致的更新失败的问题
|
||||||
|
- 修复打开文件报错的问题
|
||||||
|
- 修复 url 导入时无法获取中文配置名称的问题
|
||||||
|
- 修复 alpha 内核无法显示内存信息的问题
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.4.2
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- update clash meta core to mihomo 1.17.0
|
||||||
|
- support both clash meta stable release and prerelease-alpha release
|
||||||
|
- fixed the problem of not being able to set the system proxy when there is a dial-up link on windows system [#833](https://github.com/zzzgydi/clash-verge/issues/833)
|
||||||
|
- support new clash field
|
||||||
|
- support random mixed port
|
||||||
|
- add windows x86 and linux armv7 support
|
||||||
|
- support disable tray click event
|
||||||
|
- add download progress for updater
|
||||||
|
- support drag to reorder the profile
|
||||||
|
- embed emoji fonts
|
||||||
|
- update depends
|
||||||
|
- improve UI style
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.4.1
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- update clash meta core to newest 虚空终端(2023.11.23)
|
||||||
|
- delete clash core UI
|
||||||
|
- improve UI
|
||||||
|
- change Logo to original
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.4.0
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- update clash meta core to newest 虚空终端
|
||||||
|
- delete clash core, no longer maintain
|
||||||
|
- merge Clash nyanpasu changes
|
||||||
|
- remove delay display different color
|
||||||
|
- use Meta Country.mmdb
|
||||||
|
- update dependencies
|
||||||
|
- small changes here and there
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.3.8
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- update clash meta core
|
||||||
|
- add default valid keys
|
||||||
|
- adjust the delay display interval and color
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- fix connections page undefined exception
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.3.7
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- update clash and clash meta core
|
||||||
|
- profiles page add paste button
|
||||||
|
- subscriptions url textfield use multi lines
|
||||||
|
- set min window size
|
||||||
|
- add check for updates buttons
|
||||||
|
- add open dashboard to the hotkey list
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- fix profiles page undefined exception
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.3.6
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- add russian translation
|
||||||
|
- support to show connection detail
|
||||||
|
- support clash meta memory usage display
|
||||||
|
- support proxy provider update ui
|
||||||
|
- update geo data file from meta repo
|
||||||
|
- adjust setting page
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- center the window when it is out of screen
|
||||||
|
- use `sudo` when `pkexec` not found (Linux)
|
||||||
|
- reconnect websocket when window focus
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
- The current version of the Linux installation package is built by Ubuntu 20.04 (Github Action).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.3.5
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- update clash core
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- fix blurry system tray icon (Windows)
|
||||||
|
- fix v1.3.4 wintun.dll not found (Windows)
|
||||||
|
- fix v1.3.4 clash core not found (macOS, Linux)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.3.4
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- update clash and clash meta core
|
||||||
|
- optimize traffic graph high CPU usage when window hidden
|
||||||
|
- use polkit to elevate permission (Linux)
|
||||||
|
- support app log level setting
|
||||||
|
- support copy environment variable
|
||||||
|
- overwrite resource file according to file modified
|
||||||
|
- save window size and position
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- remove fallback group select status
|
||||||
|
- enable context menu on editable element (Windows)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.3.3
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- update clash and clash meta core
|
||||||
|
- show tray icon variants in different system proxy status (Windows)
|
||||||
|
- close all connections when mode changed
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- encode controller secret into uri
|
||||||
|
- error boundary for each page
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.3.2
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- update clash and clash meta core
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- fix import url issue
|
||||||
|
- fix profile undefined issue
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.3.1
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- update clash and clash meta core
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- fix open url issue
|
||||||
|
- fix appimage path panic
|
||||||
|
- fix grant root permission in macOS
|
||||||
|
- fix linux system proxy default bypass
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.3.0
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- update clash and clash meta
|
||||||
|
- support opening dir on tray
|
||||||
|
- support updating all profiles with one click
|
||||||
|
- support granting root permission to clash core(Linux, macOS)
|
||||||
|
- support enable/disable clash fields filter, feel free to experience the latest features of Clash Meta
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- deb add openssl depend(Linux)
|
||||||
|
- fix the AppImage auto launch path(Linux)
|
||||||
|
- fix get the default network service(macOS)
|
||||||
|
- remove the esc key listener in macOS, cmd+w instead(macOS)
|
||||||
|
- fix infinite retry when websocket error
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.2.3
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- update clash
|
||||||
|
- adjust macOS window style
|
||||||
|
- profile supports UTF8 with BOM
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- fix selected proxy
|
||||||
|
- fix error log
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.2.2
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- update clash meta
|
||||||
|
- recover clash core after panic
|
||||||
|
- use system window decorations(Linux)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- flush system proxy settings(Windows)
|
||||||
|
- fix parse log panic
|
||||||
|
- fix ui bug
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.2.1
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- update clash version
|
||||||
|
- proxy groups support multi columns
|
||||||
|
- optimize ui
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- fix ui websocket connection
|
||||||
|
- adjust delay check concurrency
|
||||||
|
- avoid setting login item repeatedly(macOS)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.2.0
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- update clash meta version
|
||||||
|
- support to change external-controller
|
||||||
|
- support to change default latency test URL
|
||||||
|
- close all connections when proxy changed or profile changed
|
||||||
|
- check the config by using the core
|
||||||
|
- increase the robustness of the program
|
||||||
|
- optimize windows service mode (need to reinstall)
|
||||||
|
- optimize ui
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- invalid hotkey cause panic
|
||||||
|
- invalid theme setting cause panic
|
||||||
|
- fix some other glitches
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.1.2
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- the system tray follows i18n
|
||||||
|
- change the proxy group ui of global mode
|
||||||
|
- support to update profile with the system proxy/clash proxy
|
||||||
|
- check the remote profile more strictly
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- use app version as default user agent
|
||||||
|
- the clash not exit in service mode
|
||||||
|
- reset the system proxy when quit the app
|
||||||
|
- fix some other glitches
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.1.1
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- optimize clash config feedback
|
||||||
|
- hide macOS dock icon
|
||||||
|
- use clash meta compatible version (Linux)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- fix some other glitches
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.1.0
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- add rule page
|
||||||
|
- supports proxy providers delay check
|
||||||
|
- add proxy delay check loading status
|
||||||
|
- supports hotkey/shortcut management
|
||||||
|
- supports displaying connections data in table layout(refer to yacd)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- supports yaml merge key in clash config
|
||||||
|
- detect the network interface and set the system proxy(macOS)
|
||||||
|
- fix some other glitches
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.0.6
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- update clash and clash.meta
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- only script profile display console
|
||||||
|
- automatic configuration update on demand at launch
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.0.5
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- reimplement profile enhanced mode with quick-js
|
||||||
|
- optimize the runtime config generation process
|
||||||
|
- support web ui management
|
||||||
|
- support clash field management
|
||||||
|
- support viewing the runtime config
|
||||||
|
- adjust some pages style
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- fix silent start
|
||||||
|
- fix incorrectly reset system proxy on exit
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.0.4
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- update clash core and clash meta version
|
||||||
|
- support switch clash mode on system tray
|
||||||
|
- theme mode support follows system
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- config load error on first use
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.0.3
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- save some states such as URL test, filter, etc
|
||||||
|
- update clash core and clash-meta core
|
||||||
|
- new icon for macOS
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.0.2
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- supports for switching clash core
|
||||||
|
- supports release UI processes
|
||||||
|
- supports script mode setting
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- fix service mode bug (Windows)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.0.1
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- adjust default theme settings
|
||||||
|
- reduce gpu usage of traffic graph when hidden
|
||||||
|
- supports more remote profile response header setting
|
||||||
|
- check remote profile data format when imported
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- service mode install and start issue (Windows)
|
||||||
|
- fix launch panic (Some Windows)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.0.0
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- update clash core
|
||||||
|
- optimize traffic graph animation
|
||||||
|
- supports interval update profiles
|
||||||
|
- supports service mode (Windows)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- reset system proxy when exit from dock (macOS)
|
||||||
|
- adjust clash dns config process strategy
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v0.0.29
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- sort proxy node
|
||||||
|
- custom proxy test url
|
||||||
|
- logs page filter
|
||||||
|
- connections page filter
|
||||||
|
- default user agent for subscription
|
||||||
|
- system tray add tun mode toggle
|
||||||
|
- enable to change the config dir (Windows only)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v0.0.28
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- enable to use clash config fields (UI)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- remove the character
|
||||||
|
- fix some icon color
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v0.0.27
|
## v0.0.27
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
BIN
docs/demo1.png
|
Before Width: | Height: | Size: 21 KiB |
BIN
docs/demo2.png
|
Before Width: | Height: | Size: 30 KiB |
BIN
docs/demo3.png
|
Before Width: | Height: | Size: 28 KiB |
BIN
docs/demo4.png
|
Before Width: | Height: | Size: 24 KiB |
BIN
docs/demo5.png
|
Before Width: | Height: | Size: 27 KiB |
BIN
docs/demo6.png
|
Before Width: | Height: | Size: 28 KiB |
BIN
docs/preview.png
Normal file
|
After Width: | Height: | Size: 576 KiB |
97
package.json
@@ -1,59 +1,78 @@
|
|||||||
{
|
{
|
||||||
"name": "clash-verge",
|
"name": "clash-verge",
|
||||||
"version": "0.0.27",
|
"version": "1.6.1",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0-only",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "tauri dev",
|
"dev": "tauri dev",
|
||||||
|
"dev:diff": "tauri dev -f verge-dev",
|
||||||
"build": "tauri build",
|
"build": "tauri build",
|
||||||
"tauri": "tauri",
|
"tauri": "tauri",
|
||||||
"web:dev": "vite",
|
"web:dev": "vite",
|
||||||
"web:build": "tsc && vite build",
|
"web:build": "tsc && vite build",
|
||||||
"web:serve": "vite preview",
|
"web:serve": "vite preview",
|
||||||
"check": "node scripts/check.mjs",
|
"check": "node scripts/check.mjs",
|
||||||
"green": "node scripts/green.mjs",
|
|
||||||
"publish": "node scripts/publish.mjs",
|
|
||||||
"updater": "node scripts/updater.mjs",
|
"updater": "node scripts/updater.mjs",
|
||||||
|
"portable": "node scripts/portable.mjs",
|
||||||
"prepare": "husky install"
|
"prepare": "husky install"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.8.2",
|
"@dnd-kit/core": "^6.1.0",
|
||||||
"@emotion/styled": "^11.8.1",
|
"@dnd-kit/sortable": "^8.0.0",
|
||||||
"@mui/icons-material": "^5.5.1",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@mui/material": "^5.5.3",
|
"@emotion/react": "^11.11.4",
|
||||||
"@tauri-apps/api": "^1.0.0-rc.3",
|
"@emotion/styled": "^11.11.5",
|
||||||
"ahooks": "^3.2.0",
|
"@juggle/resize-observer": "^3.4.0",
|
||||||
"axios": "^0.26.0",
|
"@mui/icons-material": "^5.15.15",
|
||||||
"dayjs": "^1.11.0",
|
"@mui/lab": "5.0.0-alpha.149",
|
||||||
"i18next": "^21.6.14",
|
"@mui/material": "^5.15.15",
|
||||||
"monaco-editor": "^0.33.0",
|
"@mui/x-data-grid": "^6.19.11",
|
||||||
"react": "^17.0.2",
|
"@tauri-apps/api": "^1.5.4",
|
||||||
"react-dom": "^17.0.2",
|
"@types/json-schema": "^7.0.15",
|
||||||
"react-i18next": "^11.15.6",
|
"ahooks": "^3.7.11",
|
||||||
"react-router-dom": "^6.2.2",
|
"axios": "^1.6.8",
|
||||||
"react-virtuoso": "~2.7.2",
|
"dayjs": "1.11.5",
|
||||||
"recoil": "^0.6.1",
|
"i18next": "^23.11.2",
|
||||||
"snarkdown": "^2.0.0",
|
"lodash-es": "^4.17.21",
|
||||||
"swr": "^1.2.2"
|
"meta-json-schema": "1.18.4-beta2",
|
||||||
|
"monaco-editor": "^0.47.0",
|
||||||
|
"monaco-yaml": "^5.1.1",
|
||||||
|
"nanoid": "^5.0.7",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-error-boundary": "^3.1.4",
|
||||||
|
"react-hook-form": "^7.51.3",
|
||||||
|
"react-i18next": "^13.5.0",
|
||||||
|
"react-markdown": "^9.0.1",
|
||||||
|
"react-router-dom": "^6.23.0",
|
||||||
|
"react-transition-group": "^4.4.5",
|
||||||
|
"react-virtuoso": "^4.7.10",
|
||||||
|
"recoil": "^0.7.7",
|
||||||
|
"swr": "^1.3.0",
|
||||||
|
"tar": "^6.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@actions/github": "^5.0.0",
|
"@actions/github": "^5.1.1",
|
||||||
"@tauri-apps/cli": "^1.0.0-rc.8",
|
"@tauri-apps/cli": "^1.5.12",
|
||||||
"@types/fs-extra": "^9.0.13",
|
"@types/fs-extra": "^9.0.13",
|
||||||
"@types/js-cookie": "^3.0.1",
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/lodash": "^4.14.180",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/react": "^17.0.0",
|
"@types/react": "^18.3.1",
|
||||||
"@types/react-dom": "^17.0.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@vitejs/plugin-react": "^1.2.0",
|
"@types/react-transition-group": "^4.4.10",
|
||||||
"adm-zip": "^0.5.9",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"fs-extra": "^10.0.0",
|
"adm-zip": "^0.5.12",
|
||||||
"husky": "^7.0.0",
|
"cross-env": "^7.0.3",
|
||||||
"node-fetch": "^3.2.0",
|
"fs-extra": "^11.2.0",
|
||||||
"pretty-quick": "^3.1.3",
|
"https-proxy-agent": "^5.0.1",
|
||||||
"sass": "^1.49.7",
|
"husky": "^7.0.4",
|
||||||
"typescript": "^4.5.5",
|
"node-fetch": "^3.3.2",
|
||||||
"vite": "^2.8.6",
|
"prettier": "^2.8.8",
|
||||||
"vite-plugin-monaco-editor": "^1.0.10",
|
"pretty-quick": "^3.3.1",
|
||||||
"vite-plugin-svgr": "^1.1.0"
|
"sass": "^1.75.0",
|
||||||
|
"typescript": "^5.4.5",
|
||||||
|
"vite": "^5.2.10",
|
||||||
|
"vite-plugin-monaco-editor": "^1.1.0",
|
||||||
|
"vite-plugin-svgr": "^4.2.0"
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
|
|||||||
5602
pnpm-lock.yaml
generated
Normal file
@@ -1,159 +1,317 @@
|
|||||||
import fs from "fs-extra";
|
import fs from "fs-extra";
|
||||||
import zlib from "zlib";
|
import zlib from "zlib";
|
||||||
|
import tar from "tar";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import AdmZip from "adm-zip";
|
import AdmZip from "adm-zip";
|
||||||
import fetch from "node-fetch";
|
import fetch from "node-fetch";
|
||||||
|
import proxyAgent from "https-proxy-agent";
|
||||||
import { execSync } from "child_process";
|
import { execSync } from "child_process";
|
||||||
|
|
||||||
const cwd = process.cwd();
|
const cwd = process.cwd();
|
||||||
|
const TEMP_DIR = path.join(cwd, "node_modules/.verge");
|
||||||
const FORCE = process.argv.includes("--force");
|
const FORCE = process.argv.includes("--force");
|
||||||
|
|
||||||
/**
|
const PLATFORM_MAP = {
|
||||||
* get the correct clash release infomation
|
"x86_64-pc-windows-msvc": "win32",
|
||||||
*/
|
"i686-pc-windows-msvc": "win32",
|
||||||
function resolveClash() {
|
"aarch64-pc-windows-msvc": "win32",
|
||||||
const { platform, arch } = process;
|
"x86_64-apple-darwin": "darwin",
|
||||||
|
"aarch64-apple-darwin": "darwin",
|
||||||
|
"x86_64-unknown-linux-gnu": "linux",
|
||||||
|
"i686-unknown-linux-gnu": "linux",
|
||||||
|
"aarch64-unknown-linux-gnu": "linux",
|
||||||
|
"armv7-unknown-linux-gnueabihf": "linux",
|
||||||
|
"riscv64gc-unknown-linux-gnu": "linux",
|
||||||
|
"loongarch64-unknown-linux-gnu": "linux",
|
||||||
|
};
|
||||||
|
const ARCH_MAP = {
|
||||||
|
"x86_64-pc-windows-msvc": "x64",
|
||||||
|
"i686-pc-windows-msvc": "ia32",
|
||||||
|
"aarch64-pc-windows-msvc": "arm64",
|
||||||
|
"x86_64-apple-darwin": "x64",
|
||||||
|
"aarch64-apple-darwin": "arm64",
|
||||||
|
"x86_64-unknown-linux-gnu": "x64",
|
||||||
|
"i686-unknown-linux-gnu": "ia32",
|
||||||
|
"aarch64-unknown-linux-gnu": "arm64",
|
||||||
|
"armv7-unknown-linux-gnueabihf": "arm",
|
||||||
|
"riscv64gc-unknown-linux-gnu": "riscv64",
|
||||||
|
"loongarch64-unknown-linux-gnu": "loong64",
|
||||||
|
};
|
||||||
|
|
||||||
const CLASH_URL_PREFIX =
|
const arg1 = process.argv.slice(2)[0];
|
||||||
"https://github.com/Dreamacro/clash/releases/download/premium/";
|
const arg2 = process.argv.slice(2)[1];
|
||||||
const CLASH_LATEST_DATE = "2022.03.21";
|
const target = arg1 === "--force" ? arg2 : arg1;
|
||||||
|
const { platform, arch } = target
|
||||||
|
? { platform: PLATFORM_MAP[target], arch: ARCH_MAP[target] }
|
||||||
|
: process;
|
||||||
|
|
||||||
// todo
|
const SIDECAR_HOST = target
|
||||||
const map = {
|
? target
|
||||||
"win32-x64": "clash-windows-amd64",
|
: execSync("rustc -vV")
|
||||||
"darwin-x64": "clash-darwin-amd64",
|
.toString()
|
||||||
"darwin-arm64": "clash-darwin-arm64",
|
.match(/(?<=host: ).+(?=\s*)/g)[0];
|
||||||
"linux-x64": "clash-linux-amd64",
|
|
||||||
};
|
|
||||||
|
|
||||||
const name = map[`${platform}-${arch}`];
|
/* ======= clash meta alpha======= */
|
||||||
|
const META_ALPHA_VERSION_URL =
|
||||||
|
"https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha/version.txt";
|
||||||
|
const META_ALPHA_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha`;
|
||||||
|
let META_ALPHA_VERSION;
|
||||||
|
|
||||||
if (!name) {
|
const META_ALPHA_MAP = {
|
||||||
throw new Error(`unsupport platform "${platform}-${arch}"`);
|
"win32-x64": "mihomo-windows-amd64-compatible",
|
||||||
|
"win32-ia32": "mihomo-windows-386",
|
||||||
|
"win32-arm64": "mihomo-windows-arm64",
|
||||||
|
"darwin-x64": "mihomo-darwin-amd64",
|
||||||
|
"darwin-arm64": "mihomo-darwin-arm64",
|
||||||
|
"linux-x64": "mihomo-linux-amd64-compatible",
|
||||||
|
"linux-ia32": "mihomo-linux-386",
|
||||||
|
"linux-arm64": "mihomo-linux-arm64",
|
||||||
|
"linux-arm": "mihomo-linux-armv7",
|
||||||
|
"linux-riscv64": "mihomo-linux-riscv64",
|
||||||
|
"linux-loong64": "mihomo-linux-loong64",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch the latest alpha release version from the version.txt file
|
||||||
|
async function getLatestAlphaVersion() {
|
||||||
|
const options = {};
|
||||||
|
|
||||||
|
const httpProxy =
|
||||||
|
process.env.HTTP_PROXY ||
|
||||||
|
process.env.http_proxy ||
|
||||||
|
process.env.HTTPS_PROXY ||
|
||||||
|
process.env.https_proxy;
|
||||||
|
|
||||||
|
if (httpProxy) {
|
||||||
|
options.agent = proxyAgent(httpProxy);
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
const response = await fetch(META_ALPHA_VERSION_URL, {
|
||||||
|
...options,
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
let v = await response.text();
|
||||||
|
META_ALPHA_VERSION = v.trim(); // Trim to remove extra whitespaces
|
||||||
|
console.log(`Latest alpha version: ${META_ALPHA_VERSION}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching latest alpha version:", error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const isWin = platform === "win32";
|
/* ======= clash meta stable ======= */
|
||||||
const zip = isWin ? "zip" : "gz";
|
const META_VERSION_URL =
|
||||||
const url = `${CLASH_URL_PREFIX}${name}-${CLASH_LATEST_DATE}.${zip}`;
|
"https://github.com/MetaCubeX/mihomo/releases/latest/download/version.txt";
|
||||||
const exefile = `${name}${isWin ? ".exe" : ""}`;
|
const META_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/download`;
|
||||||
const zipfile = `${name}.${zip}`;
|
let META_VERSION;
|
||||||
|
|
||||||
return { url, zip, exefile, zipfile };
|
const META_MAP = {
|
||||||
|
"win32-x64": "mihomo-windows-amd64-compatible",
|
||||||
|
"win32-ia32": "mihomo-windows-386",
|
||||||
|
"win32-arm64": "mihomo-windows-arm64",
|
||||||
|
"darwin-x64": "mihomo-darwin-amd64",
|
||||||
|
"darwin-arm64": "mihomo-darwin-arm64",
|
||||||
|
"linux-x64": "mihomo-linux-amd64-compatible",
|
||||||
|
"linux-ia32": "mihomo-linux-386",
|
||||||
|
"linux-arm64": "mihomo-linux-arm64",
|
||||||
|
"linux-arm": "mihomo-linux-armv7",
|
||||||
|
"linux-riscv64": "mihomo-linux-riscv64",
|
||||||
|
"linux-loong64": "mihomo-linux-loong64",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch the latest release version from the version.txt file
|
||||||
|
async function getLatestReleaseVersion() {
|
||||||
|
const options = {};
|
||||||
|
|
||||||
|
const httpProxy =
|
||||||
|
process.env.HTTP_PROXY ||
|
||||||
|
process.env.http_proxy ||
|
||||||
|
process.env.HTTPS_PROXY ||
|
||||||
|
process.env.https_proxy;
|
||||||
|
|
||||||
|
if (httpProxy) {
|
||||||
|
options.agent = proxyAgent(httpProxy);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await fetch(META_VERSION_URL, {
|
||||||
|
...options,
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
let v = await response.text();
|
||||||
|
META_VERSION = v.trim(); // Trim to remove extra whitespaces
|
||||||
|
console.log(`Latest release version: ${META_VERSION}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching latest release version:", error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* check available
|
||||||
|
*/
|
||||||
|
if (!META_MAP[`${platform}-${arch}`]) {
|
||||||
|
throw new Error(
|
||||||
|
`clash meta alpha unsupported platform "${platform}-${arch}"`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!META_ALPHA_MAP[`${platform}-${arch}`]) {
|
||||||
|
throw new Error(
|
||||||
|
`clash meta alpha unsupported platform "${platform}-${arch}"`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get the sidecar bin
|
* core info
|
||||||
*/
|
*/
|
||||||
async function resolveSidecar() {
|
function clashMetaAlpha() {
|
||||||
const sidecarDir = path.join(cwd, "src-tauri", "sidecar");
|
const name = META_ALPHA_MAP[`${platform}-${arch}`];
|
||||||
|
const isWin = platform === "win32";
|
||||||
|
const urlExt = isWin ? "zip" : "gz";
|
||||||
|
const downloadURL = `${META_ALPHA_URL_PREFIX}/${name}-${META_ALPHA_VERSION}.${urlExt}`;
|
||||||
|
const exeFile = `${name}${isWin ? ".exe" : ""}`;
|
||||||
|
const zipFile = `${name}-${META_ALPHA_VERSION}.${urlExt}`;
|
||||||
|
|
||||||
const host = execSync("rustc -vV | grep host").toString().slice(6).trim();
|
return {
|
||||||
const ext = process.platform === "win32" ? ".exe" : "";
|
name: "clash-meta-alpha",
|
||||||
const sidecarFile = `clash-${host}${ext}`;
|
targetFile: `clash-meta-alpha-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
|
||||||
const sidecarPath = path.join(sidecarDir, sidecarFile);
|
exeFile,
|
||||||
|
zipFile,
|
||||||
|
downloadURL,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function clashMeta() {
|
||||||
|
const name = META_MAP[`${platform}-${arch}`];
|
||||||
|
const isWin = platform === "win32";
|
||||||
|
const urlExt = isWin ? "zip" : "gz";
|
||||||
|
const downloadURL = `${META_URL_PREFIX}/${META_VERSION}/${name}-${META_VERSION}.${urlExt}`;
|
||||||
|
const exeFile = `${name}${isWin ? ".exe" : ""}`;
|
||||||
|
const zipFile = `${name}-${META_VERSION}.${urlExt}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: "clash-meta",
|
||||||
|
targetFile: `clash-meta-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
|
||||||
|
exeFile,
|
||||||
|
zipFile,
|
||||||
|
downloadURL,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* download sidecar and rename
|
||||||
|
*/
|
||||||
|
async function resolveSidecar(binInfo) {
|
||||||
|
const { name, targetFile, zipFile, exeFile, downloadURL } = binInfo;
|
||||||
|
|
||||||
|
const sidecarDir = path.join(cwd, "src-tauri", "sidecar");
|
||||||
|
const sidecarPath = path.join(sidecarDir, targetFile);
|
||||||
|
|
||||||
await fs.mkdirp(sidecarDir);
|
await fs.mkdirp(sidecarDir);
|
||||||
if (!FORCE && (await fs.pathExists(sidecarPath))) return;
|
if (!FORCE && (await fs.pathExists(sidecarPath))) return;
|
||||||
|
|
||||||
// download sidecar
|
const tempDir = path.join(TEMP_DIR, name);
|
||||||
const binInfo = resolveClash();
|
const tempZip = path.join(tempDir, zipFile);
|
||||||
const tempDir = path.join(cwd, "pre-dev-temp");
|
const tempExe = path.join(tempDir, exeFile);
|
||||||
const tempZip = path.join(tempDir, binInfo.zipfile);
|
|
||||||
const tempExe = path.join(tempDir, binInfo.exefile);
|
|
||||||
|
|
||||||
await fs.mkdirp(tempDir);
|
await fs.mkdirp(tempDir);
|
||||||
if (!(await fs.pathExists(tempZip))) await downloadFile(binInfo.url, tempZip);
|
try {
|
||||||
|
if (!(await fs.pathExists(tempZip))) {
|
||||||
|
await downloadFile(downloadURL, tempZip);
|
||||||
|
}
|
||||||
|
|
||||||
if (binInfo.zip === "zip") {
|
if (zipFile.endsWith(".zip")) {
|
||||||
const zip = new AdmZip(tempZip);
|
const zip = new AdmZip(tempZip);
|
||||||
zip.getEntries().forEach((entry) => {
|
zip.getEntries().forEach((entry) => {
|
||||||
console.log("[INFO]: entry name", entry.entryName);
|
console.log(`[DEBUG]: "${name}" entry name`, entry.entryName);
|
||||||
});
|
});
|
||||||
zip.extractAllTo(tempDir, true);
|
zip.extractAllTo(tempDir, true);
|
||||||
// save as sidecar
|
await fs.rename(tempExe, sidecarPath);
|
||||||
await fs.rename(tempExe, sidecarPath);
|
console.log(`[INFO]: "${name}" unzip finished`);
|
||||||
console.log(`[INFO]: unzip finished`);
|
} else if (zipFile.endsWith(".tgz")) {
|
||||||
} else {
|
// tgz
|
||||||
// gz
|
await fs.mkdirp(tempDir);
|
||||||
const readStream = fs.createReadStream(tempZip);
|
await tar.extract({
|
||||||
const writeStream = fs.createWriteStream(sidecarPath);
|
cwd: tempDir,
|
||||||
readStream
|
file: tempZip,
|
||||||
.pipe(zlib.createGunzip())
|
//strip: 1, // 可能需要根据实际的 .tgz 文件结构调整
|
||||||
.pipe(writeStream)
|
});
|
||||||
.on("finish", () => {
|
const files = await fs.readdir(tempDir);
|
||||||
console.log(`[INFO]: gunzip finished`);
|
console.log(`[DEBUG]: "${name}" files in tempDir:`, files);
|
||||||
|
const extractedFile = files.find((file) => file.startsWith("虚空终端-"));
|
||||||
|
if (extractedFile) {
|
||||||
|
const extractedFilePath = path.join(tempDir, extractedFile);
|
||||||
|
await fs.rename(extractedFilePath, sidecarPath);
|
||||||
|
console.log(`[INFO]: "${name}" file renamed to "${sidecarPath}"`);
|
||||||
execSync(`chmod 755 ${sidecarPath}`);
|
execSync(`chmod 755 ${sidecarPath}`);
|
||||||
console.log(`[INFO]: chmod binary finished`);
|
console.log(`[INFO]: "${name}" chmod binary finished`);
|
||||||
})
|
} else {
|
||||||
.on("error", (error) => console.error(error));
|
throw new Error(`Expected file not found in ${tempDir}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// gz
|
||||||
|
const readStream = fs.createReadStream(tempZip);
|
||||||
|
const writeStream = fs.createWriteStream(sidecarPath);
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const onError = (error) => {
|
||||||
|
console.error(`[ERROR]: "${name}" gz failed:`, error.message);
|
||||||
|
reject(error);
|
||||||
|
};
|
||||||
|
readStream
|
||||||
|
.pipe(zlib.createGunzip().on("error", onError))
|
||||||
|
.pipe(writeStream)
|
||||||
|
.on("finish", () => {
|
||||||
|
console.log(`[INFO]: "${name}" gunzip finished`);
|
||||||
|
execSync(`chmod 755 ${sidecarPath}`);
|
||||||
|
console.log(`[INFO]: "${name}" chmod binary finished`);
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.on("error", onError);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// 需要删除文件
|
||||||
|
await fs.remove(sidecarPath);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
// delete temp dir
|
||||||
|
await fs.remove(tempDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete temp dir
|
|
||||||
await fs.remove(tempDir);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* only Windows
|
* download the file to the resources dir
|
||||||
* get the wintun.dll (not required)
|
|
||||||
*/
|
*/
|
||||||
async function resolveWintun() {
|
async function resolveResource(binInfo) {
|
||||||
const { platform } = process;
|
const { file, downloadURL } = binInfo;
|
||||||
|
|
||||||
if (platform !== "win32") return;
|
const resDir = path.join(cwd, "src-tauri/resources");
|
||||||
|
const targetPath = path.join(resDir, file);
|
||||||
const url = "https://www.wintun.net/builds/wintun-0.14.1.zip";
|
|
||||||
|
|
||||||
const tempDir = path.join(cwd, "pre-dev-temp-1");
|
|
||||||
const tempZip = path.join(tempDir, "wintun.zip");
|
|
||||||
|
|
||||||
const wintunPath = path.join(tempDir, "wintun/bin/amd64/wintun.dll");
|
|
||||||
const targetPath = path.join(cwd, "src-tauri/resources", "wintun.dll");
|
|
||||||
|
|
||||||
if (!FORCE && (await fs.pathExists(targetPath))) return;
|
if (!FORCE && (await fs.pathExists(targetPath))) return;
|
||||||
|
|
||||||
await fs.mkdirp(tempDir);
|
|
||||||
|
|
||||||
if (!(await fs.pathExists(tempZip))) {
|
|
||||||
await downloadFile(url, tempZip);
|
|
||||||
}
|
|
||||||
|
|
||||||
// unzip
|
|
||||||
const zip = new AdmZip(tempZip);
|
|
||||||
zip.extractAllTo(tempDir, true);
|
|
||||||
|
|
||||||
if (!(await fs.pathExists(wintunPath))) {
|
|
||||||
throw new Error(`path not found "${wintunPath}"`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// move wintun.dll to resources
|
|
||||||
await fs.rename(wintunPath, targetPath);
|
|
||||||
// delete temp dir
|
|
||||||
await fs.remove(tempDir);
|
|
||||||
|
|
||||||
console.log(`[INFO]: resolve wintun.dll finished`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get the Country.mmdb (not required)
|
|
||||||
*/
|
|
||||||
async function resolveMmdb() {
|
|
||||||
const url =
|
|
||||||
"https://github.com/Dreamacro/maxmind-geoip/releases/latest/download/Country.mmdb";
|
|
||||||
|
|
||||||
const resDir = path.join(cwd, "src-tauri", "resources");
|
|
||||||
const resPath = path.join(resDir, "Country.mmdb");
|
|
||||||
if (!FORCE && (await fs.pathExists(resPath))) return;
|
|
||||||
await fs.mkdirp(resDir);
|
await fs.mkdirp(resDir);
|
||||||
await downloadFile(url, resPath);
|
await downloadFile(downloadURL, targetPath);
|
||||||
|
|
||||||
|
console.log(`[INFO]: ${file} finished`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* download file and save to `path`
|
* download file and save to `path`
|
||||||
*/
|
*/
|
||||||
async function downloadFile(url, path) {
|
async function downloadFile(url, path) {
|
||||||
console.log(`[INFO]: downloading from "${url}"`);
|
const options = {};
|
||||||
|
|
||||||
|
const httpProxy =
|
||||||
|
process.env.HTTP_PROXY ||
|
||||||
|
process.env.http_proxy ||
|
||||||
|
process.env.HTTPS_PROXY ||
|
||||||
|
process.env.https_proxy;
|
||||||
|
|
||||||
|
if (httpProxy) {
|
||||||
|
options.agent = proxyAgent(httpProxy);
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
|
...options,
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: { "Content-Type": "application/octet-stream" },
|
headers: { "Content-Type": "application/octet-stream" },
|
||||||
});
|
});
|
||||||
@@ -163,7 +321,171 @@ async function downloadFile(url, path) {
|
|||||||
console.log(`[INFO]: download finished "${url}"`);
|
console.log(`[INFO]: download finished "${url}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// main
|
// SimpleSC.dll
|
||||||
resolveSidecar().catch(console.error);
|
const resolvePlugin = async () => {
|
||||||
resolveWintun().catch(console.error);
|
const url =
|
||||||
resolveMmdb().catch(console.error);
|
"https://nsis.sourceforge.io/mediawiki/images/e/ef/NSIS_Simple_Service_Plugin_Unicode_1.30.zip";
|
||||||
|
|
||||||
|
const tempDir = path.join(TEMP_DIR, "SimpleSC");
|
||||||
|
const tempZip = path.join(
|
||||||
|
tempDir,
|
||||||
|
"NSIS_Simple_Service_Plugin_Unicode_1.30.zip"
|
||||||
|
);
|
||||||
|
const tempDll = path.join(tempDir, "SimpleSC.dll");
|
||||||
|
const pluginDir = path.join(process.env.APPDATA, "Local/NSIS");
|
||||||
|
const pluginPath = path.join(pluginDir, "SimpleSC.dll");
|
||||||
|
await fs.mkdirp(pluginDir);
|
||||||
|
await fs.mkdirp(tempDir);
|
||||||
|
if (!FORCE && (await fs.pathExists(pluginPath))) return;
|
||||||
|
try {
|
||||||
|
if (!(await fs.pathExists(tempZip))) {
|
||||||
|
await downloadFile(url, tempZip);
|
||||||
|
}
|
||||||
|
const zip = new AdmZip(tempZip);
|
||||||
|
zip.getEntries().forEach((entry) => {
|
||||||
|
console.log(`[DEBUG]: "SimpleSC" entry name`, entry.entryName);
|
||||||
|
});
|
||||||
|
zip.extractAllTo(tempDir, true);
|
||||||
|
await fs.copyFile(tempDll, pluginPath);
|
||||||
|
console.log(`[INFO]: "SimpleSC" unzip finished`);
|
||||||
|
} finally {
|
||||||
|
await fs.remove(tempDir);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// service chmod
|
||||||
|
const resolveServicePermission = async () => {
|
||||||
|
const serviceExecutables = [
|
||||||
|
"clash-verge-service",
|
||||||
|
"install-service",
|
||||||
|
"uninstall-service",
|
||||||
|
];
|
||||||
|
const resDir = path.join(cwd, "src-tauri/resources");
|
||||||
|
for (let f of serviceExecutables) {
|
||||||
|
const targetPath = path.join(resDir, f);
|
||||||
|
if (await fs.pathExists(targetPath)) {
|
||||||
|
execSync(`chmod 755 ${targetPath}`);
|
||||||
|
console.log(`[INFO]: "${targetPath}" chmod finished`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* main
|
||||||
|
*/
|
||||||
|
|
||||||
|
const SERVICE_URL = `https://github.com/clash-verge-rev/clash-verge-service/releases/download/${SIDECAR_HOST}`;
|
||||||
|
|
||||||
|
const resolveService = () => {
|
||||||
|
let ext = platform === "win32" ? ".exe" : "";
|
||||||
|
resolveResource({
|
||||||
|
file: "clash-verge-service" + ext,
|
||||||
|
downloadURL: `${SERVICE_URL}/clash-verge-service${ext}`,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveInstall = () => {
|
||||||
|
let ext = platform === "win32" ? ".exe" : "";
|
||||||
|
resolveResource({
|
||||||
|
file: "install-service" + ext,
|
||||||
|
downloadURL: `${SERVICE_URL}/install-service${ext}`,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveUninstall = () => {
|
||||||
|
let ext = platform === "win32" ? ".exe" : "";
|
||||||
|
resolveResource({
|
||||||
|
file: "uninstall-service" + ext,
|
||||||
|
downloadURL: `${SERVICE_URL}/uninstall-service${ext}`,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveSetDnsScript = () =>
|
||||||
|
resolveResource({
|
||||||
|
file: "set_dns.sh",
|
||||||
|
downloadURL: `https://github.com/clash-verge-rev/set-dns-script/releases/download/script/set_dns.sh`,
|
||||||
|
});
|
||||||
|
const resolveUnSetDnsScript = () =>
|
||||||
|
resolveResource({
|
||||||
|
file: "unset_dns.sh",
|
||||||
|
downloadURL: `https://github.com/clash-verge-rev/set-dns-script/releases/download/script/unset_dns.sh`,
|
||||||
|
});
|
||||||
|
const resolveMmdb = () =>
|
||||||
|
resolveResource({
|
||||||
|
file: "Country.mmdb",
|
||||||
|
downloadURL: `https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/country.mmdb`,
|
||||||
|
});
|
||||||
|
const resolveGeosite = () =>
|
||||||
|
resolveResource({
|
||||||
|
file: "geosite.dat",
|
||||||
|
downloadURL: `https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat`,
|
||||||
|
});
|
||||||
|
const resolveGeoIP = () =>
|
||||||
|
resolveResource({
|
||||||
|
file: "geoip.dat",
|
||||||
|
downloadURL: `https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat`,
|
||||||
|
});
|
||||||
|
const resolveEnableLoopback = () =>
|
||||||
|
resolveResource({
|
||||||
|
file: "enableLoopback.exe",
|
||||||
|
downloadURL: `https://github.com/Kuingsmile/uwp-tool/releases/download/latest/enableLoopback.exe`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const tasks = [
|
||||||
|
// { name: "clash", func: resolveClash, retry: 5 },
|
||||||
|
{
|
||||||
|
name: "clash-meta-alpha",
|
||||||
|
func: () =>
|
||||||
|
getLatestAlphaVersion().then(() => resolveSidecar(clashMetaAlpha())),
|
||||||
|
retry: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "clash-meta",
|
||||||
|
func: () =>
|
||||||
|
getLatestReleaseVersion().then(() => resolveSidecar(clashMeta())),
|
||||||
|
retry: 5,
|
||||||
|
},
|
||||||
|
{ name: "plugin", func: resolvePlugin, retry: 5, winOnly: true },
|
||||||
|
{ name: "service", func: resolveService, retry: 5 },
|
||||||
|
{ name: "install", func: resolveInstall, retry: 5 },
|
||||||
|
{ name: "uninstall", func: resolveUninstall, retry: 5 },
|
||||||
|
{ name: "set_dns_script", func: resolveSetDnsScript, retry: 5 },
|
||||||
|
{ name: "unset_dns_script", func: resolveUnSetDnsScript, retry: 5 },
|
||||||
|
{ name: "mmdb", func: resolveMmdb, retry: 5 },
|
||||||
|
{ name: "geosite", func: resolveGeosite, retry: 5 },
|
||||||
|
{ name: "geoip", func: resolveGeoIP, retry: 5 },
|
||||||
|
{
|
||||||
|
name: "enableLoopback",
|
||||||
|
func: resolveEnableLoopback,
|
||||||
|
retry: 5,
|
||||||
|
winOnly: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "service_chmod",
|
||||||
|
func: resolveServicePermission,
|
||||||
|
retry: 1,
|
||||||
|
unixOnly: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
async function runTask() {
|
||||||
|
const task = tasks.shift();
|
||||||
|
if (!task) return;
|
||||||
|
if (task.winOnly && platform !== "win32") return runTask();
|
||||||
|
if (task.linuxOnly && platform !== "linux") return runTask();
|
||||||
|
if (task.unixOnly && platform === "win32") return runTask();
|
||||||
|
|
||||||
|
for (let i = 0; i < task.retry; i++) {
|
||||||
|
try {
|
||||||
|
await task.func();
|
||||||
|
break;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[ERROR]: task::${task.name} try ${i} ==`, err.message);
|
||||||
|
if (i === task.retry - 1) throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return runTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
runTask();
|
||||||
|
runTask();
|
||||||
|
|||||||
@@ -4,31 +4,47 @@ import AdmZip from "adm-zip";
|
|||||||
import { createRequire } from "module";
|
import { createRequire } from "module";
|
||||||
import { getOctokit, context } from "@actions/github";
|
import { getOctokit, context } from "@actions/github";
|
||||||
|
|
||||||
|
const target = process.argv.slice(2)[0];
|
||||||
|
const alpha = process.argv.slice(2)[1];
|
||||||
|
|
||||||
|
const ARCH_MAP = {
|
||||||
|
"x86_64-pc-windows-msvc": "x64",
|
||||||
|
"aarch64-pc-windows-msvc": "arm64",
|
||||||
|
};
|
||||||
|
|
||||||
/// Script for ci
|
/// Script for ci
|
||||||
/// 打包绿色版/便携版 (only Windows)
|
/// 打包绿色版/便携版 (only Windows)
|
||||||
async function resolveGreen() {
|
async function resolvePortable() {
|
||||||
if (process.platform !== "win32") return;
|
if (process.platform !== "win32") return;
|
||||||
|
|
||||||
const releaseDir = "./src-tauri/target/release";
|
const releaseDir = target
|
||||||
|
? `./src-tauri/target/${target}/release`
|
||||||
|
: `./src-tauri/target/release`;
|
||||||
|
const configDir = path.join(releaseDir, ".config");
|
||||||
|
|
||||||
if (!(await fs.pathExists(releaseDir))) {
|
if (!(await fs.pathExists(releaseDir))) {
|
||||||
throw new Error("could not found the release dir");
|
throw new Error("could not found the release dir");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await fs.mkdir(configDir);
|
||||||
|
await fs.createFile(path.join(configDir, "PORTABLE"));
|
||||||
|
|
||||||
const zip = new AdmZip();
|
const zip = new AdmZip();
|
||||||
|
|
||||||
zip.addLocalFile(path.join(releaseDir, "Clash Verge.exe"));
|
zip.addLocalFile(path.join(releaseDir, "Clash Verge.exe"));
|
||||||
zip.addLocalFile(path.join(releaseDir, "clash.exe"));
|
zip.addLocalFile(path.join(releaseDir, "clash-meta.exe"));
|
||||||
|
zip.addLocalFile(path.join(releaseDir, "clash-meta-alpha.exe"));
|
||||||
zip.addLocalFolder(path.join(releaseDir, "resources"), "resources");
|
zip.addLocalFolder(path.join(releaseDir, "resources"), "resources");
|
||||||
|
zip.addLocalFolder(configDir, ".config");
|
||||||
|
|
||||||
const require = createRequire(import.meta.url);
|
const require = createRequire(import.meta.url);
|
||||||
const packageJson = require("../package.json");
|
const packageJson = require("../package.json");
|
||||||
const { version } = packageJson;
|
const { version } = packageJson;
|
||||||
|
|
||||||
const zipFile = `Clash.Verge_${version}_x64_green.zip`;
|
const zipFile = `Clash.Verge_${version}_${ARCH_MAP[target]}_portable.zip`;
|
||||||
zip.writeZip(zipFile);
|
zip.writeZip(zipFile);
|
||||||
|
|
||||||
console.log("[INFO]: create green zip successfully");
|
console.log("[INFO]: create portable zip successfully");
|
||||||
|
|
||||||
// push release assets
|
// push release assets
|
||||||
if (process.env.GITHUB_TOKEN === undefined) {
|
if (process.env.GITHUB_TOKEN === undefined) {
|
||||||
@@ -37,12 +53,25 @@ async function resolveGreen() {
|
|||||||
|
|
||||||
const options = { owner: context.repo.owner, repo: context.repo.repo };
|
const options = { owner: context.repo.owner, repo: context.repo.repo };
|
||||||
const github = getOctokit(process.env.GITHUB_TOKEN);
|
const github = getOctokit(process.env.GITHUB_TOKEN);
|
||||||
|
const tag = alpha ? "alpha" : process.env.TAG_NAME || `v${version}`;
|
||||||
|
console.log("[INFO]: upload to ", tag);
|
||||||
|
|
||||||
const { data: release } = await github.rest.repos.getReleaseByTag({
|
const { data: release } = await github.rest.repos.getReleaseByTag({
|
||||||
...options,
|
...options,
|
||||||
tag: `v${version}`,
|
tag,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let assets = release.assets.filter((x) => {
|
||||||
|
return x.name === zipFile;
|
||||||
|
});
|
||||||
|
if (assets.length > 0) {
|
||||||
|
let id = assets[0].id;
|
||||||
|
await github.rest.repos.deleteReleaseAsset({
|
||||||
|
...options,
|
||||||
|
asset_id: id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
console.log(release.name);
|
console.log(release.name);
|
||||||
|
|
||||||
await github.rest.repos.uploadReleaseAsset({
|
await github.rest.repos.uploadReleaseAsset({
|
||||||
@@ -53,4 +82,4 @@ async function resolveGreen() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
resolveGreen().catch(console.error);
|
resolvePortable().catch(console.error);
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import fs from "fs-extra";
|
|
||||||
import { createRequire } from "module";
|
|
||||||
import { execSync } from "child_process";
|
|
||||||
import { resolveUpdateLog } from "./updatelog.mjs";
|
|
||||||
|
|
||||||
const require = createRequire(import.meta.url);
|
|
||||||
|
|
||||||
// publish
|
|
||||||
async function resolvePublish() {
|
|
||||||
const flag = process.argv[2] ?? "patch";
|
|
||||||
const packageJson = require("../package.json");
|
|
||||||
const tauriJson = require("../src-tauri/tauri.conf.json");
|
|
||||||
|
|
||||||
let [a, b, c] = packageJson.version.split(".").map(Number);
|
|
||||||
|
|
||||||
if (flag === "major") {
|
|
||||||
a += 1;
|
|
||||||
b = 0;
|
|
||||||
c = 0;
|
|
||||||
} else if (flag === "minor") {
|
|
||||||
b += 1;
|
|
||||||
c = 0;
|
|
||||||
} else if (flag === "patch") {
|
|
||||||
c += 1;
|
|
||||||
} else throw new Error(`invalid flag "${flag}"`);
|
|
||||||
|
|
||||||
const nextVersion = `${a}.${b}.${c}`;
|
|
||||||
packageJson.version = nextVersion;
|
|
||||||
tauriJson.package.version = nextVersion;
|
|
||||||
|
|
||||||
// 发布更新前先写更新日志
|
|
||||||
const nextTag = `v${nextVersion}`;
|
|
||||||
await resolveUpdateLog(nextTag);
|
|
||||||
|
|
||||||
await fs.writeFile(
|
|
||||||
"./package.json",
|
|
||||||
JSON.stringify(packageJson, undefined, 2)
|
|
||||||
);
|
|
||||||
await fs.writeFile(
|
|
||||||
"./src-tauri/tauri.conf.json",
|
|
||||||
JSON.stringify(tauriJson, undefined, 2)
|
|
||||||
);
|
|
||||||
|
|
||||||
execSync("git add ./package.json");
|
|
||||||
execSync("git add ./src-tauri/tauri.conf.json");
|
|
||||||
execSync(`git commit -m "v${nextVersion}"`);
|
|
||||||
execSync(`git tag -a v${nextVersion} -m "v${nextVersion}"`);
|
|
||||||
execSync(`git push`);
|
|
||||||
execSync(`git push origin v${nextVersion}`);
|
|
||||||
console.log(`Publish Successfully...`);
|
|
||||||
}
|
|
||||||
|
|
||||||
resolvePublish();
|
|
||||||
@@ -43,9 +43,12 @@ async function resolveUpdater() {
|
|||||||
darwin: { signature: "", url: "" }, // compatible with older formats
|
darwin: { signature: "", url: "" }, // compatible with older formats
|
||||||
"darwin-aarch64": { signature: "", url: "" },
|
"darwin-aarch64": { signature: "", url: "" },
|
||||||
"darwin-intel": { signature: "", url: "" },
|
"darwin-intel": { signature: "", url: "" },
|
||||||
|
"darwin-x86_64": { signature: "", url: "" },
|
||||||
"linux-x86_64": { signature: "", url: "" },
|
"linux-x86_64": { signature: "", url: "" },
|
||||||
|
"linux-aarch64": { signature: "", url: "" },
|
||||||
|
"linux-armv7": { signature: "", url: "" },
|
||||||
"windows-x86_64": { signature: "", url: "" },
|
"windows-x86_64": { signature: "", url: "" },
|
||||||
"windows-i686": { signature: "", url: "" }, // no supported
|
"windows-aarch64": { signature: "", url: "" },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -53,49 +56,67 @@ async function resolveUpdater() {
|
|||||||
const { name, browser_download_url } = asset;
|
const { name, browser_download_url } = asset;
|
||||||
|
|
||||||
// win64 url
|
// win64 url
|
||||||
if (name.endsWith(".msi.zip")) {
|
if (name.endsWith("x64-setup.nsis.zip")) {
|
||||||
updateData.platforms.win64.url = browser_download_url;
|
updateData.platforms.win64.url = browser_download_url;
|
||||||
updateData.platforms["windows-x86_64"].url = browser_download_url;
|
updateData.platforms["windows-x86_64"].url = browser_download_url;
|
||||||
}
|
}
|
||||||
// win64 signature
|
// win64 signature
|
||||||
if (name.endsWith(".msi.zip.sig")) {
|
if (name.endsWith("x64-setup.nsis.zip.sig")) {
|
||||||
const sig = await getSignature(browser_download_url);
|
const sig = await getSignature(browser_download_url);
|
||||||
updateData.platforms.win64.signature = sig;
|
updateData.platforms.win64.signature = sig;
|
||||||
updateData.platforms["windows-x86_64"].signature = sig;
|
updateData.platforms["windows-x86_64"].signature = sig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// win arm url
|
||||||
|
if (name.endsWith("arm64-setup.nsis.zip")) {
|
||||||
|
updateData.platforms["windows-aarch64"].url = browser_download_url;
|
||||||
|
}
|
||||||
|
// win arm signature
|
||||||
|
if (name.endsWith("arm64-setup.nsis.zip.sig")) {
|
||||||
|
const sig = await getSignature(browser_download_url);
|
||||||
|
updateData.platforms["windows-aarch64"].signature = sig;
|
||||||
|
}
|
||||||
|
|
||||||
// darwin url (intel)
|
// darwin url (intel)
|
||||||
if (name.endsWith(".app.tar.gz") && !name.includes("aarch")) {
|
if (name.endsWith(".app.tar.gz") && !name.includes("aarch")) {
|
||||||
updateData.platforms.darwin.url = browser_download_url;
|
updateData.platforms.darwin.url = browser_download_url;
|
||||||
updateData.platforms["darwin-intel"].url = browser_download_url;
|
updateData.platforms["darwin-intel"].url = browser_download_url;
|
||||||
|
updateData.platforms["darwin-x86_64"].url = browser_download_url;
|
||||||
}
|
}
|
||||||
// darwin signature (intel)
|
// darwin signature (intel)
|
||||||
if (name.endsWith(".app.tar.gz.sig") && !name.includes("aarch")) {
|
if (name.endsWith(".app.tar.gz.sig") && !name.includes("aarch")) {
|
||||||
const sig = await getSignature(browser_download_url);
|
const sig = await getSignature(browser_download_url);
|
||||||
updateData.platforms.darwin.signature = sig;
|
updateData.platforms.darwin.signature = sig;
|
||||||
updateData.platforms["darwin-intel"].signature = sig;
|
updateData.platforms["darwin-intel"].signature = sig;
|
||||||
|
updateData.platforms["darwin-x86_64"].signature = sig;
|
||||||
}
|
}
|
||||||
|
|
||||||
// darwin url (aarch)
|
// darwin url (aarch)
|
||||||
if (name.endsWith("aarch.app.tar.gz")) {
|
if (name.endsWith("aarch64.app.tar.gz")) {
|
||||||
updateData.platforms["darwin-aarch64"].url = browser_download_url;
|
updateData.platforms["darwin-aarch64"].url = browser_download_url;
|
||||||
}
|
}
|
||||||
// darwin signature (aarch)
|
// darwin signature (aarch)
|
||||||
if (name.endsWith("aarch.app.tar.gz.sig")) {
|
if (name.endsWith("aarch64.app.tar.gz.sig")) {
|
||||||
const sig = await getSignature(browser_download_url);
|
const sig = await getSignature(browser_download_url);
|
||||||
updateData.platforms["darwin-aarch64"].signature = sig;
|
updateData.platforms["darwin-aarch64"].signature = sig;
|
||||||
}
|
}
|
||||||
|
|
||||||
// linux url
|
// linux x64 url
|
||||||
if (name.endsWith(".AppImage.tar.gz")) {
|
if (name.endsWith("amd64.AppImage.tar.gz")) {
|
||||||
updateData.platforms.linux.url = browser_download_url;
|
updateData.platforms.linux.url = browser_download_url;
|
||||||
updateData.platforms["linux-x86_64"].url = browser_download_url;
|
updateData.platforms["linux-x86_64"].url = browser_download_url;
|
||||||
|
// 暂时使用x64版本的url和sig,使得可以检查更新,但aarch64版本还不支持构建appimage
|
||||||
|
updateData.platforms["linux-aarch64"].url = browser_download_url;
|
||||||
|
updateData.platforms["linux-armv7"].url = browser_download_url;
|
||||||
}
|
}
|
||||||
// linux signature
|
// linux x64 signature
|
||||||
if (name.endsWith(".AppImage.tar.gz.sig")) {
|
if (name.endsWith("amd64.AppImage.tar.gz.sig")) {
|
||||||
const sig = await getSignature(browser_download_url);
|
const sig = await getSignature(browser_download_url);
|
||||||
updateData.platforms.linux.signature = sig;
|
updateData.platforms.linux.signature = sig;
|
||||||
updateData.platforms["linux-x86_64"].signature = sig;
|
updateData.platforms["linux-x86_64"].signature = sig;
|
||||||
|
// 暂时使用x64版本的url和sig,使得可以检查更新,但aarch64版本还不支持构建appimage
|
||||||
|
updateData.platforms["linux-aarch64"].signature = sig;
|
||||||
|
updateData.platforms["linux-armv7"].signature = sig;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -117,10 +138,8 @@ async function resolveUpdater() {
|
|||||||
|
|
||||||
Object.entries(updateDataNew.platforms).forEach(([key, value]) => {
|
Object.entries(updateDataNew.platforms).forEach(([key, value]) => {
|
||||||
if (value.url) {
|
if (value.url) {
|
||||||
updateDataNew.platforms[key].url = value.url.replace(
|
updateDataNew.platforms[key].url =
|
||||||
"https://github.com/",
|
"https://mirror.ghproxy.com/" + value.url;
|
||||||
"https://hub.fastgit.xyz/"
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
console.log(`[Error]: updateDataNew.platforms.${key} is null`);
|
console.log(`[Error]: updateDataNew.platforms.${key} is null`);
|
||||||
}
|
}
|
||||||
|
|||||||
5660
src-tauri/Cargo.lock
generated
@@ -1,48 +1,57 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "clash-verge"
|
name = "clash-verge"
|
||||||
version = "0.1.0"
|
version = "1.6.1"
|
||||||
description = "clash verge"
|
description = "clash verge"
|
||||||
authors = ["zzzgydi"]
|
authors = ["zzzgydi", "wonfen", "MystiPanda"]
|
||||||
license = "GPL-3.0"
|
license = "GPL-3.0-only"
|
||||||
repository = "https://github.com/zzzgydi/clash-verge.git"
|
repository = "https://github.com/clash-verge-rev/clash-verge-rev.git"
|
||||||
default-run = "clash-verge"
|
default-run = "clash-verge"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "1.0.0-rc.5", features = [] }
|
tauri-build = { version = "1", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0"
|
|
||||||
dirs = "4.0.0"
|
|
||||||
open = "2.1.1"
|
|
||||||
dunce = "1.0.2"
|
|
||||||
nanoid = "0.4.0"
|
|
||||||
chrono = "0.4.19"
|
|
||||||
serde_json = "1.0"
|
|
||||||
serde_yaml = "0.8"
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
|
||||||
tauri = { version = "1.0.0-rc.6", features = ["process-all", "shell-all", "system-tray", "updater", "window-all"] }
|
|
||||||
window-shadows = { git = "https://github.com/tauri-apps/window-shadows" }
|
|
||||||
window-vibrancy = { git = "https://github.com/tauri-apps/window-vibrancy" }
|
|
||||||
|
|
||||||
reqwest = { version = "0.11", features = ["json"] }
|
|
||||||
tokio = { version = "1", features = ["full"] }
|
|
||||||
log = "0.4.14"
|
|
||||||
log4rs = "1.0.0"
|
|
||||||
warp = "0.3"
|
warp = "0.3"
|
||||||
which = "4.2.2"
|
anyhow = "1.0"
|
||||||
auto-launch = "0.2"
|
dirs = "5.0"
|
||||||
|
open = "5.0"
|
||||||
|
log = "0.4"
|
||||||
|
dunce = "1.0"
|
||||||
|
log4rs = "1"
|
||||||
|
nanoid = "0.4"
|
||||||
|
chrono = "0.4"
|
||||||
|
sysinfo = "0.30"
|
||||||
|
boa_engine = "0.18"
|
||||||
|
serde_json = "1.0"
|
||||||
|
serde_yaml = "0.9"
|
||||||
|
once_cell = "1.18"
|
||||||
port_scanner = "0.1.5"
|
port_scanner = "0.1.5"
|
||||||
|
delay_timer = "0.11.5"
|
||||||
|
parking_lot = "0.12"
|
||||||
|
percent-encoding = "2.3.1"
|
||||||
|
window-shadows = { version = "0.2" }
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
reqwest = { version = "0.12", features = ["json", "rustls-tls"] }
|
||||||
|
sysproxy = { git="https://github.com/zzzgydi/sysproxy-rs", branch = "main" }
|
||||||
|
auto-launch = { git="https://github.com/zzzgydi/auto-launch", branch = "main" }
|
||||||
|
tauri = { version = "1.6", features = [ "fs-read-file", "fs-exists", "path-all", "protocol-asset", "dialog-open", "notification-all", "icon-png", "icon-ico", "clipboard-all", "global-shortcut-all", "process-all", "shell-all", "system-tray", "updater", "window-all", "devtools"] }
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
winreg = { version = "0.10", features = ["transactions"] }
|
runas = "=1.2.0"
|
||||||
|
deelevate = "0.2.0"
|
||||||
|
winreg = "0.52.0"
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
|
users = "0.11.0"
|
||||||
|
#openssl
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = [ "custom-protocol" ]
|
default = ["custom-protocol"]
|
||||||
custom-protocol = [ "tauri/custom-protocol" ]
|
custom-protocol = ["tauri/custom-protocol"]
|
||||||
verge-dev = []
|
verge-dev = []
|
||||||
debug-yml = []
|
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
panic = "abort"
|
panic = "abort"
|
||||||
|
|||||||
17
src-tauri/Info.plist
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>Clash Verge</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>clash</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
fn main() {
|
fn main() {
|
||||||
tauri_build::build()
|
tauri_build::build()
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 33 KiB |
BIN
src-tauri/icons/icon-new.icns
Normal file
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 33 KiB |
BIN
src-tauri/icons/mac-tray-icon-sys.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
src-tauri/icons/mac-tray-icon-tun.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src-tauri/icons/mac-tray-icon.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
src-tauri/icons/tray-icon-sys.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src-tauri/icons/tray-icon-tun.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src-tauri/icons/tray-icon.ico
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
src-tauri/icons/tray-icon.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
@@ -1,6 +1,6 @@
|
|||||||
max_width = 100
|
max_width = 100
|
||||||
hard_tabs = false
|
hard_tabs = false
|
||||||
tab_spaces = 2
|
tab_spaces = 4
|
||||||
newline_style = "Auto"
|
newline_style = "Auto"
|
||||||
use_small_heuristics = "Default"
|
use_small_heuristics = "Default"
|
||||||
reorder_imports = true
|
reorder_imports = true
|
||||||
|
|||||||
@@ -1,370 +1,371 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
core::{ClashInfo, PrfItem, PrfOption, Profiles, VergeConfig},
|
config::*,
|
||||||
states::{ClashState, ProfilesState, VergeState},
|
core::*,
|
||||||
utils::{dirs, sysopt::SysProxyConfig},
|
feat,
|
||||||
|
utils::{dirs, help, resolve},
|
||||||
};
|
};
|
||||||
use crate::{ret_err, wrap_err};
|
use crate::{ret_err, wrap_err};
|
||||||
use anyhow::Result;
|
use anyhow::{Context, Result};
|
||||||
use serde_yaml::Mapping;
|
use serde_yaml::Mapping;
|
||||||
use std::process::Command;
|
use std::collections::{HashMap, VecDeque};
|
||||||
use tauri::{api, Manager, State};
|
use sysproxy::Sysproxy;
|
||||||
|
use tauri::{api, Manager};
|
||||||
|
type CmdResult<T = ()> = Result<T, String>;
|
||||||
|
|
||||||
/// get all profiles from `profiles.yaml`
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_profiles<'a>(profiles_state: State<'_, ProfilesState>) -> Result<Profiles, String> {
|
pub fn get_profiles() -> CmdResult<IProfiles> {
|
||||||
let profiles = profiles_state.0.lock().unwrap();
|
Ok(Config::profiles().data().clone())
|
||||||
Ok(profiles.clone())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// synchronize data irregularly
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn sync_profiles(profiles_state: State<'_, ProfilesState>) -> Result<(), String> {
|
pub async fn enhance_profiles() -> CmdResult {
|
||||||
let mut profiles = profiles_state.0.lock().unwrap();
|
wrap_err!(CoreManager::global().update_config().await)?;
|
||||||
wrap_err!(profiles.sync_file())
|
handle::Handle::refresh_clash();
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// import the profile from url
|
|
||||||
/// and save to `profiles.yaml`
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn import_profile(
|
pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult {
|
||||||
url: String,
|
let item = wrap_err!(PrfItem::from_url(&url, None, None, option).await)?;
|
||||||
option: Option<PrfOption>,
|
wrap_err!(Config::profiles().data().append_item(item))
|
||||||
profiles_state: State<'_, ProfilesState>,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
let item = wrap_err!(PrfItem::from_url(&url, None, None, option).await)?;
|
|
||||||
|
|
||||||
let mut profiles = profiles_state.0.lock().unwrap();
|
|
||||||
wrap_err!(profiles.append_item(item))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// new a profile
|
|
||||||
/// append a temp profile item file to the `profiles` dir
|
|
||||||
/// view the temp profile file by using vscode or other editor
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn create_profile(
|
pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
|
||||||
item: PrfItem, // partial
|
wrap_err!(Config::profiles().data().reorder(active_id, over_id))
|
||||||
file_data: Option<String>,
|
|
||||||
profiles_state: State<'_, ProfilesState>,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
let item = wrap_err!(PrfItem::from(item, file_data).await)?;
|
|
||||||
let mut profiles = profiles_state.0.lock().unwrap();
|
|
||||||
|
|
||||||
wrap_err!(profiles.append_item(item))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update the profile
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn update_profile(
|
pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResult {
|
||||||
index: String,
|
let item = wrap_err!(PrfItem::from(item, file_data).await)?;
|
||||||
option: Option<PrfOption>,
|
wrap_err!(Config::profiles().data().append_item(item))
|
||||||
clash_state: State<'_, ClashState>,
|
}
|
||||||
profiles_state: State<'_, ProfilesState>,
|
|
||||||
) -> Result<(), String> {
|
#[tauri::command]
|
||||||
let (url, opt) = {
|
pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult {
|
||||||
// must release the lock here
|
wrap_err!(feat::update_profile(index, option).await)
|
||||||
let profiles = profiles_state.0.lock().unwrap();
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn delete_profile(index: String) -> CmdResult {
|
||||||
|
let should_update = wrap_err!({ Config::profiles().data().delete_item(index) })?;
|
||||||
|
if should_update {
|
||||||
|
wrap_err!(CoreManager::global().update_config().await)?;
|
||||||
|
handle::Handle::refresh_clash();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 修改profiles的
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult {
|
||||||
|
wrap_err!({ Config::profiles().draft().patch_config(profiles) })?;
|
||||||
|
|
||||||
|
match CoreManager::global().update_config().await {
|
||||||
|
Ok(_) => {
|
||||||
|
handle::Handle::refresh_clash();
|
||||||
|
let _ = handle::Handle::update_systray_part();
|
||||||
|
Config::profiles().apply();
|
||||||
|
wrap_err!(Config::profiles().data().save_file())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
Config::profiles().discard();
|
||||||
|
log::error!(target: "app", "{err}");
|
||||||
|
Err(format!("{err}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 修改某个profile item的
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
|
||||||
|
wrap_err!(Config::profiles().data().patch_item(index, profile))?;
|
||||||
|
wrap_err!(timer::Timer::global().refresh())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn view_profile(app_handle: tauri::AppHandle, index: String) -> CmdResult {
|
||||||
|
let file = {
|
||||||
|
wrap_err!(Config::profiles().latest().get_item(&index))?
|
||||||
|
.file
|
||||||
|
.clone()
|
||||||
|
.ok_or("the file field is null")
|
||||||
|
}?;
|
||||||
|
|
||||||
|
let path = wrap_err!(dirs::app_profiles_dir())?.join(file);
|
||||||
|
if !path.exists() {
|
||||||
|
ret_err!("the file not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
wrap_err!(help::open_file(app_handle, path))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn read_profile_file(index: String) -> CmdResult<String> {
|
||||||
|
let profiles = Config::profiles();
|
||||||
|
let profiles = profiles.latest();
|
||||||
let item = wrap_err!(profiles.get_item(&index))?;
|
let item = wrap_err!(profiles.get_item(&index))?;
|
||||||
|
let data = wrap_err!(item.read_file())?;
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
|
||||||
// check the profile type
|
#[tauri::command]
|
||||||
if let Some(typ) = item.itype.as_ref() {
|
pub fn save_profile_file(index: String, file_data: Option<String>) -> CmdResult {
|
||||||
if *typ != "remote" {
|
if file_data.is_none() {
|
||||||
ret_err!(format!("could not update the `{typ}` profile"));
|
return Ok(());
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if item.url.is_none() {
|
let profiles = Config::profiles();
|
||||||
ret_err!("failed to get the item url");
|
let profiles = profiles.latest();
|
||||||
}
|
let item = wrap_err!(profiles.get_item(&index))?;
|
||||||
|
wrap_err!(item.save_file(file_data.unwrap()))
|
||||||
(item.url.clone().unwrap(), item.option.clone())
|
|
||||||
};
|
|
||||||
|
|
||||||
let fetch_opt = PrfOption::merge(opt, option);
|
|
||||||
let item = wrap_err!(PrfItem::from_url(&url, None, None, fetch_opt).await)?;
|
|
||||||
|
|
||||||
let mut profiles = profiles_state.0.lock().unwrap();
|
|
||||||
wrap_err!(profiles.update_item(index.clone(), item))?;
|
|
||||||
|
|
||||||
// reactivate the profile
|
|
||||||
if Some(index) == profiles.get_current() {
|
|
||||||
let clash = clash_state.0.lock().unwrap();
|
|
||||||
wrap_err!(clash.activate_enhanced(&profiles, false, false))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// change the current profile
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn select_profile(
|
pub fn get_clash_info() -> CmdResult<ClashInfo> {
|
||||||
index: String,
|
Ok(Config::clash().latest().get_client_info())
|
||||||
clash_state: State<'_, ClashState>,
|
|
||||||
profiles_state: State<'_, ProfilesState>,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
let mut profiles = profiles_state.0.lock().unwrap();
|
|
||||||
wrap_err!(profiles.put_current(index))?;
|
|
||||||
|
|
||||||
let clash = clash_state.0.lock().unwrap();
|
|
||||||
wrap_err!(clash.activate_enhanced(&profiles, false, false))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// change the profile chain
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn change_profile_chain(
|
pub fn get_runtime_config() -> CmdResult<Option<Mapping>> {
|
||||||
chain: Option<Vec<String>>,
|
Ok(Config::runtime().latest().config.clone())
|
||||||
app_handle: tauri::AppHandle,
|
|
||||||
clash_state: State<'_, ClashState>,
|
|
||||||
profiles_state: State<'_, ProfilesState>,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
let mut clash = clash_state.0.lock().unwrap();
|
|
||||||
let mut profiles = profiles_state.0.lock().unwrap();
|
|
||||||
|
|
||||||
profiles.put_chain(chain);
|
|
||||||
clash.set_window(app_handle.get_window("main"));
|
|
||||||
|
|
||||||
wrap_err!(clash.activate_enhanced(&profiles, false, false))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// manually exec enhanced profile
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn enhance_profiles(
|
pub fn get_runtime_yaml() -> CmdResult<String> {
|
||||||
app_handle: tauri::AppHandle,
|
let runtime = Config::runtime();
|
||||||
clash_state: State<'_, ClashState>,
|
let runtime = runtime.latest();
|
||||||
profiles_state: State<'_, ProfilesState>,
|
let config = runtime.config.as_ref();
|
||||||
) -> Result<(), String> {
|
wrap_err!(config
|
||||||
let mut clash = clash_state.0.lock().unwrap();
|
.ok_or(anyhow::anyhow!("failed to parse config to yaml file"))
|
||||||
let profiles = profiles_state.0.lock().unwrap();
|
.and_then(
|
||||||
|
|config| serde_yaml::to_string(config).context("failed to convert config to yaml")
|
||||||
clash.set_window(app_handle.get_window("main"));
|
))
|
||||||
|
|
||||||
wrap_err!(clash.activate_enhanced(&profiles, false, false))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// delete profile item
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn delete_profile(
|
pub fn get_runtime_exists() -> CmdResult<Vec<String>> {
|
||||||
index: String,
|
Ok(Config::runtime().latest().exists_keys.clone())
|
||||||
clash_state: State<'_, ClashState>,
|
|
||||||
profiles_state: State<'_, ProfilesState>,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
let mut profiles = profiles_state.0.lock().unwrap();
|
|
||||||
|
|
||||||
if wrap_err!(profiles.delete_item(index))? {
|
|
||||||
let clash = clash_state.0.lock().unwrap();
|
|
||||||
wrap_err!(clash.activate_enhanced(&profiles, false, false))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// patch the profile config
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn patch_profile(
|
pub fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String)>>> {
|
||||||
index: String,
|
Ok(Config::runtime().latest().chain_logs.clone())
|
||||||
profile: PrfItem,
|
|
||||||
profiles_state: State<'_, ProfilesState>,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
let mut profiles = profiles_state.0.lock().unwrap();
|
|
||||||
wrap_err!(profiles.patch_item(index, profile))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// run vscode command to edit the profile
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn view_profile(index: String, profiles_state: State<'_, ProfilesState>) -> Result<(), String> {
|
pub async fn patch_clash_config(payload: Mapping) -> CmdResult {
|
||||||
let profiles = profiles_state.0.lock().unwrap();
|
wrap_err!(feat::patch_clash(payload).await)
|
||||||
let item = wrap_err!(profiles.get_item(&index))?;
|
|
||||||
|
|
||||||
let file = item.file.clone();
|
|
||||||
if file.is_none() {
|
|
||||||
ret_err!("the file is null");
|
|
||||||
}
|
|
||||||
|
|
||||||
let path = dirs::app_profiles_dir().join(file.unwrap());
|
|
||||||
if !path.exists() {
|
|
||||||
ret_err!("the file not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
// use vscode first
|
|
||||||
if let Ok(code) = which::which("code") {
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{
|
|
||||||
use std::os::windows::process::CommandExt;
|
|
||||||
|
|
||||||
if let Err(err) = Command::new(code)
|
|
||||||
.creation_flags(0x08000000)
|
|
||||||
.arg(path)
|
|
||||||
.spawn()
|
|
||||||
{
|
|
||||||
log::error!("failed to open file by VScode for {err}");
|
|
||||||
return Err("failed to open file by VScode".into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
if let Err(err) = Command::new(code).arg(path).spawn() {
|
|
||||||
log::error!("failed to open file by VScode for {err}");
|
|
||||||
return Err("failed to open file by VScode".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
wrap_err!(open::that(path))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// read the profile item file data
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn read_profile_file(
|
pub fn get_verge_config() -> CmdResult<IVerge> {
|
||||||
index: String,
|
Ok(Config::verge().data().clone())
|
||||||
profiles_state: State<'_, ProfilesState>,
|
|
||||||
) -> Result<String, String> {
|
|
||||||
let profiles = profiles_state.0.lock().unwrap();
|
|
||||||
let item = wrap_err!(profiles.get_item(&index))?;
|
|
||||||
let data = wrap_err!(item.read_file())?;
|
|
||||||
|
|
||||||
Ok(data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// save the profile item file data
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn save_profile_file(
|
pub async fn patch_verge_config(payload: IVerge) -> CmdResult {
|
||||||
index: String,
|
wrap_err!(feat::patch_verge(payload).await)
|
||||||
file_data: Option<String>,
|
}
|
||||||
profiles_state: State<'_, ProfilesState>,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
if file_data.is_none() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let profiles = profiles_state.0.lock().unwrap();
|
#[tauri::command]
|
||||||
let item = wrap_err!(profiles.get_item(&index))?;
|
pub async fn change_clash_core(clash_core: Option<String>) -> CmdResult {
|
||||||
wrap_err!(item.save_file(file_data.unwrap()))
|
wrap_err!(CoreManager::global().change_core(clash_core).await)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// restart the sidecar
|
/// restart the sidecar
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn restart_sidecar(
|
pub async fn restart_sidecar() -> CmdResult {
|
||||||
clash_state: State<'_, ClashState>,
|
wrap_err!(CoreManager::global().run_core().await)
|
||||||
profiles_state: State<'_, ProfilesState>,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
let mut clash = clash_state.0.lock().unwrap();
|
|
||||||
let mut profiles = profiles_state.0.lock().unwrap();
|
|
||||||
|
|
||||||
wrap_err!(clash.restart_sidecar(&mut profiles))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// get the clash core info from the state
|
|
||||||
/// the caller can also get the infomation by clash's api
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_clash_info(clash_state: State<'_, ClashState>) -> Result<ClashInfo, String> {
|
pub fn grant_permission(_core: String) -> CmdResult {
|
||||||
let clash = clash_state.0.lock().unwrap();
|
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||||
Ok(clash.info.clone())
|
return wrap_err!(manager::grant_permission(_core));
|
||||||
}
|
|
||||||
|
|
||||||
/// update the clash core config
|
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
|
||||||
/// after putting the change to the clash core
|
return Err("Unsupported target".into());
|
||||||
/// then we should save the latest config
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn patch_clash_config(
|
|
||||||
payload: Mapping,
|
|
||||||
clash_state: State<'_, ClashState>,
|
|
||||||
verge_state: State<'_, VergeState>,
|
|
||||||
profiles_state: State<'_, ProfilesState>,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
let mut clash = clash_state.0.lock().unwrap();
|
|
||||||
let mut verge = verge_state.0.lock().unwrap();
|
|
||||||
let mut profiles = profiles_state.0.lock().unwrap();
|
|
||||||
wrap_err!(clash.patch_config(payload, &mut verge, &mut profiles))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// get the system proxy
|
/// get the system proxy
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_sys_proxy() -> Result<SysProxyConfig, String> {
|
pub fn get_sys_proxy() -> CmdResult<Mapping> {
|
||||||
wrap_err!(SysProxyConfig::get_sys())
|
let current = wrap_err!(Sysproxy::get_system_proxy())?;
|
||||||
|
|
||||||
|
let mut map = Mapping::new();
|
||||||
|
map.insert("enable".into(), current.enable.into());
|
||||||
|
map.insert(
|
||||||
|
"server".into(),
|
||||||
|
format!("{}:{}", current.host, current.port).into(),
|
||||||
|
);
|
||||||
|
map.insert("bypass".into(), current.bypass.into());
|
||||||
|
|
||||||
|
Ok(map)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// get the current proxy config
|
|
||||||
/// which may not the same as system proxy
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_cur_proxy(verge_state: State<'_, VergeState>) -> Result<Option<SysProxyConfig>, String> {
|
pub fn get_clash_logs() -> CmdResult<VecDeque<String>> {
|
||||||
let verge = verge_state.0.lock().unwrap();
|
Ok(logger::Logger::global().get_log())
|
||||||
Ok(verge.cur_sysproxy.clone())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// get the verge config
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_verge_config(verge_state: State<'_, VergeState>) -> Result<VergeConfig, String> {
|
pub fn open_app_dir() -> CmdResult<()> {
|
||||||
let verge = verge_state.0.lock().unwrap();
|
let app_dir = wrap_err!(dirs::app_home_dir())?;
|
||||||
let mut config = verge.config.clone();
|
wrap_err!(open::that(app_dir))
|
||||||
|
|
||||||
if config.system_proxy_bypass.is_none() && verge.cur_sysproxy.is_some() {
|
|
||||||
config.system_proxy_bypass = Some(verge.cur_sysproxy.clone().unwrap().bypass)
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(config)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// patch the verge config
|
|
||||||
/// this command only save the config and not responsible for other things
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn patch_verge_config(
|
pub fn open_core_dir() -> CmdResult<()> {
|
||||||
payload: VergeConfig,
|
let core_dir = wrap_err!(tauri::utils::platform::current_exe())?;
|
||||||
app_handle: tauri::AppHandle,
|
let core_dir = core_dir.parent().ok_or("failed to get core dir")?;
|
||||||
clash_state: State<'_, ClashState>,
|
wrap_err!(open::that(core_dir))
|
||||||
verge_state: State<'_, VergeState>,
|
}
|
||||||
profiles_state: State<'_, ProfilesState>,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
let tun_mode = payload.enable_tun_mode.clone();
|
|
||||||
let system_proxy = payload.enable_system_proxy.clone();
|
|
||||||
|
|
||||||
let mut verge = verge_state.0.lock().unwrap();
|
#[tauri::command]
|
||||||
wrap_err!(verge.patch_config(payload))?;
|
pub fn open_logs_dir() -> CmdResult<()> {
|
||||||
|
let log_dir = wrap_err!(dirs::app_logs_dir())?;
|
||||||
|
wrap_err!(open::that(log_dir))
|
||||||
|
}
|
||||||
|
|
||||||
// change system tray
|
#[tauri::command]
|
||||||
if system_proxy.is_some() {
|
pub fn open_web_url(url: String) -> CmdResult<()> {
|
||||||
app_handle
|
wrap_err!(open::that(url))
|
||||||
.tray_handle()
|
}
|
||||||
.get_item("system_proxy")
|
|
||||||
.set_selected(system_proxy.unwrap())
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// change tun mode
|
#[cfg(windows)]
|
||||||
if tun_mode.is_some() {
|
pub mod uwp {
|
||||||
#[cfg(target_os = "windows")]
|
use super::*;
|
||||||
if *tun_mode.as_ref().unwrap() {
|
use crate::core::win_uwp;
|
||||||
let wintun_dll = dirs::app_home_dir().join("wintun.dll");
|
|
||||||
if !wintun_dll.exists() {
|
#[tauri::command]
|
||||||
log::error!("failed to enable TUN for missing `wintun.dll`");
|
pub async fn invoke_uwp_tool() -> CmdResult {
|
||||||
return Err("failed to enable TUN for missing `wintun.dll`".into());
|
wrap_err!(win_uwp::invoke_uwptools().await)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn clash_api_get_proxy_delay(
|
||||||
|
name: String,
|
||||||
|
url: Option<String>,
|
||||||
|
timeout: i32,
|
||||||
|
) -> CmdResult<clash_api::DelayRes> {
|
||||||
|
match clash_api::get_proxy_delay(name, url, timeout).await {
|
||||||
|
Ok(res) => Ok(res),
|
||||||
|
Err(err) => Err(err.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_portable_flag() -> CmdResult<bool> {
|
||||||
|
Ok(*dirs::PORTABLE_FLAG.get().unwrap_or(&false))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn test_delay(url: String) -> CmdResult<u32> {
|
||||||
|
Ok(feat::test_delay(url).await.unwrap_or(10000u32))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_app_dir() -> CmdResult<String> {
|
||||||
|
let app_home_dir = wrap_err!(dirs::app_home_dir())?
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string();
|
||||||
|
Ok(app_home_dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String> {
|
||||||
|
let icon_cache_dir = wrap_err!(dirs::app_home_dir())?.join("icons").join("cache");
|
||||||
|
let icon_path = icon_cache_dir.join(name);
|
||||||
|
if !icon_cache_dir.exists() {
|
||||||
|
let _ = std::fs::create_dir_all(&icon_cache_dir);
|
||||||
|
}
|
||||||
|
if !icon_path.exists() {
|
||||||
|
let response = wrap_err!(reqwest::get(url).await)?;
|
||||||
|
|
||||||
|
let mut file = wrap_err!(std::fs::File::create(&icon_path))?;
|
||||||
|
|
||||||
|
let content = wrap_err!(response.bytes().await)?;
|
||||||
|
wrap_err!(std::io::copy(&mut content.as_ref(), &mut file))?;
|
||||||
|
}
|
||||||
|
Ok(icon_path.to_string_lossy().to_string())
|
||||||
|
}
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn copy_icon_file(path: String, name: String) -> CmdResult<String> {
|
||||||
|
let file_path = std::path::Path::new(&path);
|
||||||
|
let icon_dir = wrap_err!(dirs::app_home_dir())?.join("icons");
|
||||||
|
if !icon_dir.exists() {
|
||||||
|
let _ = std::fs::create_dir_all(&icon_dir);
|
||||||
|
}
|
||||||
|
let ext = match file_path.extension() {
|
||||||
|
Some(e) => e.to_string_lossy().to_string(),
|
||||||
|
None => "ico".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let png_dest_path = icon_dir.join(format!("{name}.png"));
|
||||||
|
let ico_dest_path = icon_dir.join(format!("{name}.ico"));
|
||||||
|
let dest_path = icon_dir.join(format!("{name}.{ext}"));
|
||||||
|
if file_path.exists() {
|
||||||
|
std::fs::remove_file(png_dest_path).unwrap_or_default();
|
||||||
|
std::fs::remove_file(ico_dest_path).unwrap_or_default();
|
||||||
|
match std::fs::copy(file_path, &dest_path) {
|
||||||
|
Ok(_) => Ok(dest_path.to_string_lossy().to_string()),
|
||||||
|
Err(err) => Err(err.to_string()),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err("file not found".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn open_devtools(app_handle: tauri::AppHandle) {
|
||||||
|
if let Some(window) = app_handle.get_window("main") {
|
||||||
|
if !window.is_devtools_open() {
|
||||||
|
window.open_devtools();
|
||||||
|
} else {
|
||||||
|
window.close_devtools();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn exit_app(app_handle: tauri::AppHandle) {
|
||||||
|
let _ = resolve::save_window_size_position(&app_handle, true);
|
||||||
|
resolve::resolve_reset();
|
||||||
|
api::process::kill_children();
|
||||||
|
app_handle.exit(0);
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod service {
|
||||||
|
use super::*;
|
||||||
|
use crate::core::service;
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn check_service() -> CmdResult<service::JsonResponse> {
|
||||||
|
wrap_err!(service::check_service().await)
|
||||||
}
|
}
|
||||||
|
|
||||||
let clash = clash_state.0.lock().unwrap();
|
#[tauri::command]
|
||||||
let profiles = profiles_state.0.lock().unwrap();
|
pub async fn install_service() -> CmdResult {
|
||||||
|
wrap_err!(service::install_service().await)
|
||||||
|
}
|
||||||
|
|
||||||
wrap_err!(clash.activate_enhanced(&profiles, false, false))?;
|
#[tauri::command]
|
||||||
}
|
pub async fn uninstall_service() -> CmdResult {
|
||||||
|
wrap_err!(service::uninstall_service().await)
|
||||||
Ok(())
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// kill all sidecars when update app
|
#[cfg(not(windows))]
|
||||||
#[tauri::command]
|
pub mod uwp {
|
||||||
pub fn kill_sidecars() {
|
use super::*;
|
||||||
api::process::kill_children();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// open app config dir
|
#[tauri::command]
|
||||||
#[tauri::command]
|
pub async fn invoke_uwp_tool() -> CmdResult {
|
||||||
pub fn open_app_dir() -> Result<(), String> {
|
Ok(())
|
||||||
let app_dir = dirs::app_home_dir();
|
}
|
||||||
wrap_err!(open::that(app_dir))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// open logs dir
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn open_logs_dir() -> Result<(), String> {
|
|
||||||
let log_dir = dirs::app_logs_dir();
|
|
||||||
wrap_err!(open::that(log_dir))
|
|
||||||
}
|
}
|
||||||
|
|||||||
360
src-tauri/src/config/clash.rs
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
use crate::utils::{dirs, help};
|
||||||
|
use anyhow::Result;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_yaml::{Mapping, Value};
|
||||||
|
use std::{
|
||||||
|
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||||
|
str::FromStr,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone)]
|
||||||
|
pub struct IClashTemp(pub Mapping);
|
||||||
|
|
||||||
|
impl IClashTemp {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let template = Self::template();
|
||||||
|
match dirs::clash_path().and_then(|path| help::read_merge_mapping(&path)) {
|
||||||
|
Ok(mut map) => {
|
||||||
|
template.0.keys().for_each(|key| {
|
||||||
|
if !map.contains_key(key) {
|
||||||
|
map.insert(key.clone(), template.0.get(key).unwrap().clone());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Self(Self::guard(map))
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log::error!(target: "app", "{err}");
|
||||||
|
template
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn template() -> Self {
|
||||||
|
let mut map = Mapping::new();
|
||||||
|
let mut tun = Mapping::new();
|
||||||
|
tun.insert("stack".into(), "gvisor".into());
|
||||||
|
tun.insert("device".into(), "Meta".into());
|
||||||
|
tun.insert("auto-route".into(), true.into());
|
||||||
|
tun.insert("strict-route".into(), false.into());
|
||||||
|
tun.insert("auto-detect-interface".into(), true.into());
|
||||||
|
tun.insert("dns-hijack".into(), vec!["any:53"].into());
|
||||||
|
tun.insert("mtu".into(), 1500.into());
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
map.insert("redir-port".into(), 7895.into());
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
map.insert("tproxy-port".into(), 7896.into());
|
||||||
|
map.insert("mixed-port".into(), 7897.into());
|
||||||
|
map.insert("socks-port".into(), 7898.into());
|
||||||
|
map.insert("port".into(), 7899.into());
|
||||||
|
map.insert("log-level".into(), "info".into());
|
||||||
|
map.insert("allow-lan".into(), false.into());
|
||||||
|
map.insert("mode".into(), "rule".into());
|
||||||
|
map.insert("external-controller".into(), "127.0.0.1:9097".into());
|
||||||
|
map.insert("secret".into(), "".into());
|
||||||
|
map.insert("tun".into(), tun.into());
|
||||||
|
|
||||||
|
Self(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn guard(mut config: Mapping) -> Mapping {
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
let redir_port = Self::guard_redir_port(&config);
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
let tproxy_port = Self::guard_tproxy_port(&config);
|
||||||
|
let mixed_port = Self::guard_mixed_port(&config);
|
||||||
|
let socks_port = Self::guard_socks_port(&config);
|
||||||
|
let port = Self::guard_port(&config);
|
||||||
|
let ctrl = Self::guard_server_ctrl(&config);
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
config.insert("redir-port".into(), redir_port.into());
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
config.insert("tproxy-port".into(), tproxy_port.into());
|
||||||
|
config.insert("mixed-port".into(), mixed_port.into());
|
||||||
|
config.insert("socks-port".into(), socks_port.into());
|
||||||
|
config.insert("port".into(), port.into());
|
||||||
|
config.insert("external-controller".into(), ctrl.into());
|
||||||
|
config
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn patch_config(&mut self, patch: Mapping) {
|
||||||
|
for (key, value) in patch.into_iter() {
|
||||||
|
self.0.insert(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_config(&self) -> Result<()> {
|
||||||
|
help::save_yaml(
|
||||||
|
&dirs::clash_path()?,
|
||||||
|
&self.0,
|
||||||
|
Some("# Generated by Clash Verge"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_mixed_port(&self) -> u16 {
|
||||||
|
Self::guard_mixed_port(&self.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
pub fn get_socks_port(&self) -> u16 {
|
||||||
|
Self::guard_socks_port(&self.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
pub fn get_port(&self) -> u16 {
|
||||||
|
Self::guard_port(&self.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_client_info(&self) -> ClashInfo {
|
||||||
|
let config = &self.0;
|
||||||
|
|
||||||
|
ClashInfo {
|
||||||
|
mixed_port: Self::guard_mixed_port(config),
|
||||||
|
socks_port: Self::guard_socks_port(config),
|
||||||
|
port: Self::guard_port(config),
|
||||||
|
server: Self::guard_client_ctrl(config),
|
||||||
|
secret: config.get("secret").and_then(|value| match value {
|
||||||
|
Value::String(val_str) => Some(val_str.clone()),
|
||||||
|
Value::Bool(val_bool) => Some(val_bool.to_string()),
|
||||||
|
Value::Number(val_num) => Some(val_num.to_string()),
|
||||||
|
_ => None,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
pub fn guard_redir_port(config: &Mapping) -> u16 {
|
||||||
|
let mut port = config
|
||||||
|
.get("redir-port")
|
||||||
|
.and_then(|value| match value {
|
||||||
|
Value::String(val_str) => val_str.parse().ok(),
|
||||||
|
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.unwrap_or(7895);
|
||||||
|
if port == 0 {
|
||||||
|
port = 7895;
|
||||||
|
}
|
||||||
|
port
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub fn guard_tproxy_port(config: &Mapping) -> u16 {
|
||||||
|
let mut port = config
|
||||||
|
.get("tproxy-port")
|
||||||
|
.and_then(|value| match value {
|
||||||
|
Value::String(val_str) => val_str.parse().ok(),
|
||||||
|
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.unwrap_or(7896);
|
||||||
|
if port == 0 {
|
||||||
|
port = 7896;
|
||||||
|
}
|
||||||
|
port
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn guard_mixed_port(config: &Mapping) -> u16 {
|
||||||
|
let mut port = config
|
||||||
|
.get("mixed-port")
|
||||||
|
.and_then(|value| match value {
|
||||||
|
Value::String(val_str) => val_str.parse().ok(),
|
||||||
|
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.unwrap_or(7897);
|
||||||
|
if port == 0 {
|
||||||
|
port = 7897;
|
||||||
|
}
|
||||||
|
port
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn guard_socks_port(config: &Mapping) -> u16 {
|
||||||
|
let mut port = config
|
||||||
|
.get("socks-port")
|
||||||
|
.and_then(|value| match value {
|
||||||
|
Value::String(val_str) => val_str.parse().ok(),
|
||||||
|
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.unwrap_or(7898);
|
||||||
|
if port == 0 {
|
||||||
|
port = 7898;
|
||||||
|
}
|
||||||
|
port
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn guard_port(config: &Mapping) -> u16 {
|
||||||
|
let mut port = config
|
||||||
|
.get("port")
|
||||||
|
.and_then(|value| match value {
|
||||||
|
Value::String(val_str) => val_str.parse().ok(),
|
||||||
|
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.unwrap_or(7899);
|
||||||
|
if port == 0 {
|
||||||
|
port = 7899;
|
||||||
|
}
|
||||||
|
port
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn guard_server_ctrl(config: &Mapping) -> String {
|
||||||
|
config
|
||||||
|
.get("external-controller")
|
||||||
|
.and_then(|value| match value.as_str() {
|
||||||
|
Some(val_str) => {
|
||||||
|
let val_str = val_str.trim();
|
||||||
|
|
||||||
|
let val = match val_str.starts_with(':') {
|
||||||
|
true => format!("127.0.0.1{val_str}"),
|
||||||
|
false => val_str.to_owned(),
|
||||||
|
};
|
||||||
|
|
||||||
|
SocketAddr::from_str(val.as_str())
|
||||||
|
.ok()
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
})
|
||||||
|
.unwrap_or("127.0.0.1:9097".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn guard_client_ctrl(config: &Mapping) -> String {
|
||||||
|
let value = Self::guard_server_ctrl(config);
|
||||||
|
match SocketAddr::from_str(value.as_str()) {
|
||||||
|
Ok(mut socket) => {
|
||||||
|
if socket.ip().is_unspecified() {
|
||||||
|
socket.set_ip(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
|
||||||
|
}
|
||||||
|
socket.to_string()
|
||||||
|
}
|
||||||
|
Err(_) => "127.0.0.1:9097".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
pub struct ClashInfo {
|
||||||
|
/// clash core port
|
||||||
|
pub mixed_port: u16,
|
||||||
|
pub socks_port: u16,
|
||||||
|
pub port: u16,
|
||||||
|
/// same as `external-controller`
|
||||||
|
pub server: String,
|
||||||
|
/// clash secret
|
||||||
|
pub secret: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_clash_info() {
|
||||||
|
fn get_case<T: Into<Value>, D: Into<Value>>(mp: T, ec: D) -> ClashInfo {
|
||||||
|
let mut map = Mapping::new();
|
||||||
|
map.insert("mixed-port".into(), mp.into());
|
||||||
|
map.insert("external-controller".into(), ec.into());
|
||||||
|
|
||||||
|
IClashTemp(IClashTemp::guard(map)).get_client_info()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_result<S: Into<String>>(port: u16, server: S) -> ClashInfo {
|
||||||
|
ClashInfo {
|
||||||
|
mixed_port: port,
|
||||||
|
socks_port: 7898,
|
||||||
|
port: 7899,
|
||||||
|
server: server.into(),
|
||||||
|
secret: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
IClashTemp(IClashTemp::guard(Mapping::new())).get_client_info(),
|
||||||
|
get_result(7897, "127.0.0.1:9097")
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(get_case("", ""), get_result(7897, "127.0.0.1:9097"));
|
||||||
|
|
||||||
|
assert_eq!(get_case(65537, ""), get_result(1, "127.0.0.1:9097"));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
get_case(8888, "127.0.0.1:8888"),
|
||||||
|
get_result(8888, "127.0.0.1:8888")
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
get_case(8888, " :98888 "),
|
||||||
|
get_result(8888, "127.0.0.1:9097")
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
get_case(8888, "0.0.0.0:8080 "),
|
||||||
|
get_result(8888, "127.0.0.1:8080")
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
get_case(8888, "0.0.0.0:8080"),
|
||||||
|
get_result(8888, "127.0.0.1:8080")
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
get_case(8888, "[::]:8080"),
|
||||||
|
get_result(8888, "127.0.0.1:8080")
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
get_case(8888, "192.168.1.1:8080"),
|
||||||
|
get_result(8888, "192.168.1.1:8080")
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
get_case(8888, "192.168.1.1:80800"),
|
||||||
|
get_result(8888, "127.0.0.1:9097")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct IClash {
|
||||||
|
pub mixed_port: Option<u16>,
|
||||||
|
pub allow_lan: Option<bool>,
|
||||||
|
pub log_level: Option<String>,
|
||||||
|
pub ipv6: Option<bool>,
|
||||||
|
pub mode: Option<String>,
|
||||||
|
pub external_controller: Option<String>,
|
||||||
|
pub secret: Option<String>,
|
||||||
|
pub dns: Option<IClashDNS>,
|
||||||
|
pub tun: Option<IClashTUN>,
|
||||||
|
pub interface_name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct IClashTUN {
|
||||||
|
pub enable: Option<bool>,
|
||||||
|
pub stack: Option<String>,
|
||||||
|
pub auto_route: Option<bool>,
|
||||||
|
pub auto_detect_interface: Option<bool>,
|
||||||
|
pub dns_hijack: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct IClashDNS {
|
||||||
|
pub enable: Option<bool>,
|
||||||
|
pub listen: Option<String>,
|
||||||
|
pub default_nameserver: Option<Vec<String>>,
|
||||||
|
pub enhanced_mode: Option<String>,
|
||||||
|
pub fake_ip_range: Option<String>,
|
||||||
|
pub use_hosts: Option<bool>,
|
||||||
|
pub fake_ip_filter: Option<Vec<String>>,
|
||||||
|
pub nameserver: Option<Vec<String>>,
|
||||||
|
pub fallback: Option<Vec<String>>,
|
||||||
|
pub fallback_filter: Option<IClashFallbackFilter>,
|
||||||
|
pub nameserver_policy: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct IClashFallbackFilter {
|
||||||
|
pub geoip: Option<bool>,
|
||||||
|
pub geoip_code: Option<String>,
|
||||||
|
pub ipcidr: Option<Vec<String>>,
|
||||||
|
pub domain: Option<Vec<String>>,
|
||||||
|
}
|
||||||
103
src-tauri/src/config/config.rs
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
use super::{Draft, IClashTemp, IProfiles, IRuntime, IVerge};
|
||||||
|
use crate::{
|
||||||
|
enhance,
|
||||||
|
utils::{dirs, help},
|
||||||
|
};
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
|
use std::{env::temp_dir, path::PathBuf};
|
||||||
|
|
||||||
|
pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
|
||||||
|
pub const CHECK_CONFIG: &str = "clash-verge-check.yaml";
|
||||||
|
|
||||||
|
pub struct Config {
|
||||||
|
clash_config: Draft<IClashTemp>,
|
||||||
|
verge_config: Draft<IVerge>,
|
||||||
|
profiles_config: Draft<IProfiles>,
|
||||||
|
runtime_config: Draft<IRuntime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn global() -> &'static Config {
|
||||||
|
static CONFIG: OnceCell<Config> = OnceCell::new();
|
||||||
|
|
||||||
|
CONFIG.get_or_init(|| Config {
|
||||||
|
clash_config: Draft::from(IClashTemp::new()),
|
||||||
|
verge_config: Draft::from(IVerge::new()),
|
||||||
|
profiles_config: Draft::from(IProfiles::new()),
|
||||||
|
runtime_config: Draft::from(IRuntime::new()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clash() -> Draft<IClashTemp> {
|
||||||
|
Self::global().clash_config.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verge() -> Draft<IVerge> {
|
||||||
|
Self::global().verge_config.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn profiles() -> Draft<IProfiles> {
|
||||||
|
Self::global().profiles_config.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn runtime() -> Draft<IRuntime> {
|
||||||
|
Self::global().runtime_config.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 初始化订阅
|
||||||
|
pub fn init_config() -> Result<()> {
|
||||||
|
crate::log_err!(Self::generate());
|
||||||
|
if let Err(err) = Self::generate_file(ConfigType::Run) {
|
||||||
|
log::error!(target: "app", "{err}");
|
||||||
|
|
||||||
|
let runtime_path = dirs::app_home_dir()?.join(RUNTIME_CONFIG);
|
||||||
|
// 如果不存在就将默认的clash文件拿过来
|
||||||
|
if !runtime_path.exists() {
|
||||||
|
help::save_yaml(
|
||||||
|
&runtime_path,
|
||||||
|
&Config::clash().latest().0,
|
||||||
|
Some("# Clash Verge Runtime"),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 将订阅丢到对应的文件中
|
||||||
|
pub fn generate_file(typ: ConfigType) -> Result<PathBuf> {
|
||||||
|
let path = match typ {
|
||||||
|
ConfigType::Run => dirs::app_home_dir()?.join(RUNTIME_CONFIG),
|
||||||
|
ConfigType::Check => temp_dir().join(CHECK_CONFIG),
|
||||||
|
};
|
||||||
|
|
||||||
|
let runtime = Config::runtime();
|
||||||
|
let runtime = runtime.latest();
|
||||||
|
let config = runtime
|
||||||
|
.config
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(anyhow!("failed to get runtime config"))?;
|
||||||
|
|
||||||
|
help::save_yaml(&path, &config, Some("# Generated by Clash Verge"))?;
|
||||||
|
Ok(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 生成订阅存好
|
||||||
|
pub fn generate() -> Result<()> {
|
||||||
|
let (config, exists_keys, logs) = enhance::enhance();
|
||||||
|
|
||||||
|
*Config::runtime().draft() = IRuntime {
|
||||||
|
config: Some(config),
|
||||||
|
exists_keys,
|
||||||
|
chain_logs: logs,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ConfigType {
|
||||||
|
Run,
|
||||||
|
Check,
|
||||||
|
}
|
||||||
127
src-tauri/src/config/draft.rs
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
use super::{IClashTemp, IProfiles, IRuntime, IVerge};
|
||||||
|
use parking_lot::{MappedMutexGuard, Mutex, MutexGuard};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Draft<T: Clone + ToOwned> {
|
||||||
|
inner: Arc<Mutex<(T, Option<T>)>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! draft_define {
|
||||||
|
($id: ident) => {
|
||||||
|
impl Draft<$id> {
|
||||||
|
#[allow(unused)]
|
||||||
|
pub fn data(&self) -> MappedMutexGuard<$id> {
|
||||||
|
MutexGuard::map(self.inner.lock(), |guard| &mut guard.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn latest(&self) -> MappedMutexGuard<$id> {
|
||||||
|
MutexGuard::map(self.inner.lock(), |inner| {
|
||||||
|
if inner.1.is_none() {
|
||||||
|
&mut inner.0
|
||||||
|
} else {
|
||||||
|
inner.1.as_mut().unwrap()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draft(&self) -> MappedMutexGuard<$id> {
|
||||||
|
MutexGuard::map(self.inner.lock(), |inner| {
|
||||||
|
if inner.1.is_none() {
|
||||||
|
inner.1 = Some(inner.0.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
inner.1.as_mut().unwrap()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply(&self) -> Option<$id> {
|
||||||
|
let mut inner = self.inner.lock();
|
||||||
|
|
||||||
|
match inner.1.take() {
|
||||||
|
Some(draft) => {
|
||||||
|
let old_value = inner.0.to_owned();
|
||||||
|
inner.0 = draft.to_owned();
|
||||||
|
Some(old_value)
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn discard(&self) -> Option<$id> {
|
||||||
|
let mut inner = self.inner.lock();
|
||||||
|
inner.1.take()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<$id> for Draft<$id> {
|
||||||
|
fn from(data: $id) -> Self {
|
||||||
|
Draft {
|
||||||
|
inner: Arc::new(Mutex::new((data, None))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// draft_define!(IClash);
|
||||||
|
draft_define!(IClashTemp);
|
||||||
|
draft_define!(IProfiles);
|
||||||
|
draft_define!(IRuntime);
|
||||||
|
draft_define!(IVerge);
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_draft() {
|
||||||
|
let verge = IVerge {
|
||||||
|
enable_auto_launch: Some(true),
|
||||||
|
enable_tun_mode: Some(false),
|
||||||
|
..IVerge::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let draft = Draft::from(verge);
|
||||||
|
|
||||||
|
assert_eq!(draft.data().enable_auto_launch, Some(true));
|
||||||
|
assert_eq!(draft.data().enable_tun_mode, Some(false));
|
||||||
|
|
||||||
|
assert_eq!(draft.draft().enable_auto_launch, Some(true));
|
||||||
|
assert_eq!(draft.draft().enable_tun_mode, Some(false));
|
||||||
|
|
||||||
|
let mut d = draft.draft();
|
||||||
|
d.enable_auto_launch = Some(false);
|
||||||
|
d.enable_tun_mode = Some(true);
|
||||||
|
drop(d);
|
||||||
|
|
||||||
|
assert_eq!(draft.data().enable_auto_launch, Some(true));
|
||||||
|
assert_eq!(draft.data().enable_tun_mode, Some(false));
|
||||||
|
|
||||||
|
assert_eq!(draft.draft().enable_auto_launch, Some(false));
|
||||||
|
assert_eq!(draft.draft().enable_tun_mode, Some(true));
|
||||||
|
|
||||||
|
assert_eq!(draft.latest().enable_auto_launch, Some(false));
|
||||||
|
assert_eq!(draft.latest().enable_tun_mode, Some(true));
|
||||||
|
|
||||||
|
assert!(draft.apply().is_some());
|
||||||
|
assert!(draft.apply().is_none());
|
||||||
|
|
||||||
|
assert_eq!(draft.data().enable_auto_launch, Some(false));
|
||||||
|
assert_eq!(draft.data().enable_tun_mode, Some(true));
|
||||||
|
|
||||||
|
assert_eq!(draft.draft().enable_auto_launch, Some(false));
|
||||||
|
assert_eq!(draft.draft().enable_tun_mode, Some(true));
|
||||||
|
|
||||||
|
let mut d = draft.draft();
|
||||||
|
d.enable_auto_launch = Some(true);
|
||||||
|
drop(d);
|
||||||
|
|
||||||
|
assert_eq!(draft.data().enable_auto_launch, Some(false));
|
||||||
|
|
||||||
|
assert_eq!(draft.draft().enable_auto_launch, Some(true));
|
||||||
|
|
||||||
|
assert!(draft.discard().is_some());
|
||||||
|
|
||||||
|
assert_eq!(draft.data().enable_auto_launch, Some(false));
|
||||||
|
|
||||||
|
assert!(draft.discard().is_none());
|
||||||
|
|
||||||
|
assert_eq!(draft.draft().enable_auto_launch, Some(false));
|
||||||
|
}
|
||||||
15
src-tauri/src/config/mod.rs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
mod clash;
|
||||||
|
mod config;
|
||||||
|
mod draft;
|
||||||
|
mod prfitem;
|
||||||
|
mod profiles;
|
||||||
|
mod runtime;
|
||||||
|
mod verge;
|
||||||
|
|
||||||
|
pub use self::clash::*;
|
||||||
|
pub use self::config::*;
|
||||||
|
pub use self::draft::*;
|
||||||
|
pub use self::prfitem::*;
|
||||||
|
pub use self::profiles::*;
|
||||||
|
pub use self::runtime::*;
|
||||||
|
pub use self::verge::*;
|
||||||
404
src-tauri/src/config/prfitem.rs
Normal file
@@ -0,0 +1,404 @@
|
|||||||
|
use crate::utils::{dirs, help, resolve::VERSION, tmpl};
|
||||||
|
use anyhow::{bail, Context, Result};
|
||||||
|
use reqwest::StatusCode;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_yaml::Mapping;
|
||||||
|
use std::fs;
|
||||||
|
use sysproxy::Sysproxy;
|
||||||
|
|
||||||
|
use super::Config;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
||||||
|
pub struct PrfItem {
|
||||||
|
pub uid: Option<String>,
|
||||||
|
|
||||||
|
/// profile item type
|
||||||
|
/// enum value: remote | local | script | merge
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub itype: Option<String>,
|
||||||
|
|
||||||
|
/// profile name
|
||||||
|
pub name: Option<String>,
|
||||||
|
|
||||||
|
/// profile file
|
||||||
|
pub file: Option<String>,
|
||||||
|
|
||||||
|
/// profile description
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub desc: Option<String>,
|
||||||
|
|
||||||
|
/// source url
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub url: Option<String>,
|
||||||
|
|
||||||
|
/// selected information
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub selected: Option<Vec<PrfSelected>>,
|
||||||
|
|
||||||
|
/// subscription user info
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub extra: Option<PrfExtra>,
|
||||||
|
|
||||||
|
/// updated time
|
||||||
|
pub updated: Option<usize>,
|
||||||
|
|
||||||
|
/// some options of the item
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub option: Option<PrfOption>,
|
||||||
|
|
||||||
|
/// profile web page url
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub home: Option<String>,
|
||||||
|
|
||||||
|
/// the file data
|
||||||
|
#[serde(skip)]
|
||||||
|
pub file_data: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct PrfSelected {
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub now: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Copy, Deserialize, Serialize)]
|
||||||
|
pub struct PrfExtra {
|
||||||
|
pub upload: u64,
|
||||||
|
pub download: u64,
|
||||||
|
pub total: u64,
|
||||||
|
pub expire: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
pub struct PrfOption {
|
||||||
|
/// for `remote` profile's http request
|
||||||
|
/// see issue #13
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub user_agent: Option<String>,
|
||||||
|
|
||||||
|
/// for `remote` profile
|
||||||
|
/// use system proxy
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub with_proxy: Option<bool>,
|
||||||
|
|
||||||
|
/// for `remote` profile
|
||||||
|
/// use self proxy
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub self_proxy: Option<bool>,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub update_interval: Option<u64>,
|
||||||
|
|
||||||
|
/// for `remote` profile
|
||||||
|
/// disable certificate validation
|
||||||
|
/// default is `false`
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub danger_accept_invalid_certs: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PrfOption {
|
||||||
|
pub fn merge(one: Option<Self>, other: Option<Self>) -> Option<Self> {
|
||||||
|
match (one, other) {
|
||||||
|
(Some(mut a), Some(b)) => {
|
||||||
|
a.user_agent = b.user_agent.or(a.user_agent);
|
||||||
|
a.with_proxy = b.with_proxy.or(a.with_proxy);
|
||||||
|
a.self_proxy = b.self_proxy.or(a.self_proxy);
|
||||||
|
a.danger_accept_invalid_certs = b.danger_accept_invalid_certs.or(a.danger_accept_invalid_certs);
|
||||||
|
a.update_interval = b.update_interval.or(a.update_interval);
|
||||||
|
Some(a)
|
||||||
|
}
|
||||||
|
t => t.0.or(t.1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PrfItem {
|
||||||
|
/// From partial item
|
||||||
|
/// must contain `itype`
|
||||||
|
pub async fn from(item: PrfItem, file_data: Option<String>) -> Result<PrfItem> {
|
||||||
|
if item.itype.is_none() {
|
||||||
|
bail!("type should not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
match item.itype.unwrap().as_str() {
|
||||||
|
"remote" => {
|
||||||
|
if item.url.is_none() {
|
||||||
|
bail!("url should not be null");
|
||||||
|
}
|
||||||
|
let url = item.url.as_ref().unwrap().as_str();
|
||||||
|
let name = item.name;
|
||||||
|
let desc = item.desc;
|
||||||
|
PrfItem::from_url(url, name, desc, item.option).await
|
||||||
|
}
|
||||||
|
"local" => {
|
||||||
|
let name = item.name.unwrap_or("Local File".into());
|
||||||
|
let desc = item.desc.unwrap_or("".into());
|
||||||
|
PrfItem::from_local(name, desc, file_data)
|
||||||
|
}
|
||||||
|
"merge" => {
|
||||||
|
let name = item.name.unwrap_or("Merge".into());
|
||||||
|
let desc = item.desc.unwrap_or("".into());
|
||||||
|
PrfItem::from_merge(name, desc)
|
||||||
|
}
|
||||||
|
"script" => {
|
||||||
|
let name = item.name.unwrap_or("Script".into());
|
||||||
|
let desc = item.desc.unwrap_or("".into());
|
||||||
|
PrfItem::from_script(name, desc)
|
||||||
|
}
|
||||||
|
typ => bail!("invalid profile item type \"{typ}\""),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ## Local type
|
||||||
|
/// create a new item from name/desc
|
||||||
|
pub fn from_local(name: String, desc: String, file_data: Option<String>) -> Result<PrfItem> {
|
||||||
|
let uid = help::get_uid("l");
|
||||||
|
let file = format!("{uid}.yaml");
|
||||||
|
|
||||||
|
Ok(PrfItem {
|
||||||
|
uid: Some(uid),
|
||||||
|
itype: Some("local".into()),
|
||||||
|
name: Some(name),
|
||||||
|
desc: Some(desc),
|
||||||
|
file: Some(file),
|
||||||
|
url: None,
|
||||||
|
selected: None,
|
||||||
|
extra: None,
|
||||||
|
option: None,
|
||||||
|
home: None,
|
||||||
|
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||||
|
file_data: Some(file_data.unwrap_or(tmpl::ITEM_LOCAL.into())),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ## Remote type
|
||||||
|
/// create a new item from url
|
||||||
|
pub async fn from_url(
|
||||||
|
url: &str,
|
||||||
|
name: Option<String>,
|
||||||
|
desc: Option<String>,
|
||||||
|
option: Option<PrfOption>,
|
||||||
|
) -> Result<PrfItem> {
|
||||||
|
let opt_ref = option.as_ref();
|
||||||
|
let with_proxy = opt_ref.map_or(false, |o| o.with_proxy.unwrap_or(false));
|
||||||
|
let self_proxy = opt_ref.map_or(false, |o| o.self_proxy.unwrap_or(false));
|
||||||
|
let accept_invalid_certs = opt_ref.map_or(false, |o| o.danger_accept_invalid_certs.unwrap_or(false));
|
||||||
|
let user_agent = opt_ref.and_then(|o| o.user_agent.clone());
|
||||||
|
let update_interval = opt_ref.and_then(|o| o.update_interval);
|
||||||
|
|
||||||
|
let mut builder = reqwest::ClientBuilder::new().use_rustls_tls().no_proxy();
|
||||||
|
|
||||||
|
// 使用软件自己的代理
|
||||||
|
if self_proxy {
|
||||||
|
let port = Config::verge()
|
||||||
|
.latest()
|
||||||
|
.verge_mixed_port
|
||||||
|
.unwrap_or(Config::clash().data().get_mixed_port());
|
||||||
|
|
||||||
|
let proxy_scheme = format!("http://127.0.0.1:{port}");
|
||||||
|
|
||||||
|
if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) {
|
||||||
|
builder = builder.proxy(proxy);
|
||||||
|
}
|
||||||
|
if let Ok(proxy) = reqwest::Proxy::https(&proxy_scheme) {
|
||||||
|
builder = builder.proxy(proxy);
|
||||||
|
}
|
||||||
|
if let Ok(proxy) = reqwest::Proxy::all(&proxy_scheme) {
|
||||||
|
builder = builder.proxy(proxy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 使用系统代理
|
||||||
|
else if with_proxy {
|
||||||
|
if let Ok(p @ Sysproxy { enable: true, .. }) = Sysproxy::get_system_proxy() {
|
||||||
|
let proxy_scheme = format!("http://{}:{}", p.host, p.port);
|
||||||
|
|
||||||
|
if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) {
|
||||||
|
builder = builder.proxy(proxy);
|
||||||
|
}
|
||||||
|
if let Ok(proxy) = reqwest::Proxy::https(&proxy_scheme) {
|
||||||
|
builder = builder.proxy(proxy);
|
||||||
|
}
|
||||||
|
if let Ok(proxy) = reqwest::Proxy::all(&proxy_scheme) {
|
||||||
|
builder = builder.proxy(proxy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let version = match VERSION.get() {
|
||||||
|
Some(v) => format!("clash-verge/v{}", v),
|
||||||
|
None => "clash-verge/unknown".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
builder = builder.danger_accept_invalid_certs(accept_invalid_certs);
|
||||||
|
builder = builder.user_agent(user_agent.unwrap_or(version));
|
||||||
|
|
||||||
|
let resp = builder.build()?.get(url).send().await?;
|
||||||
|
|
||||||
|
let status_code = resp.status();
|
||||||
|
if !StatusCode::is_success(&status_code) {
|
||||||
|
bail!("failed to fetch remote profile with status {status_code}")
|
||||||
|
}
|
||||||
|
|
||||||
|
let header = resp.headers();
|
||||||
|
|
||||||
|
// parse the Subscription UserInfo
|
||||||
|
let extra = match header.get("Subscription-Userinfo") {
|
||||||
|
Some(value) => {
|
||||||
|
let sub_info = value.to_str().unwrap_or("");
|
||||||
|
Some(PrfExtra {
|
||||||
|
upload: help::parse_str(sub_info, "upload").unwrap_or(0),
|
||||||
|
download: help::parse_str(sub_info, "download").unwrap_or(0),
|
||||||
|
total: help::parse_str(sub_info, "total").unwrap_or(0),
|
||||||
|
expire: help::parse_str(sub_info, "expire").unwrap_or(0),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// parse the Content-Disposition
|
||||||
|
let filename = match header.get("Content-Disposition") {
|
||||||
|
Some(value) => {
|
||||||
|
let filename = format!("{value:?}");
|
||||||
|
let filename = filename.trim_matches('"');
|
||||||
|
match help::parse_str::<String>(filename, "filename*") {
|
||||||
|
Some(filename) => {
|
||||||
|
let iter = percent_encoding::percent_decode(filename.as_bytes());
|
||||||
|
let filename = iter.decode_utf8().unwrap_or_default();
|
||||||
|
filename.split("''").last().map(|s| s.to_string())
|
||||||
|
}
|
||||||
|
None => match help::parse_str::<String>(filename, "filename") {
|
||||||
|
Some(filename) => {
|
||||||
|
let filename = filename.trim_matches('"');
|
||||||
|
Some(filename.to_string())
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => Some(
|
||||||
|
crate::utils::help::get_last_part_and_decode(url).unwrap_or("Remote File".into()),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
let option = match update_interval {
|
||||||
|
Some(val) => Some(PrfOption {
|
||||||
|
update_interval: Some(val),
|
||||||
|
..PrfOption::default()
|
||||||
|
}),
|
||||||
|
None => match header.get("profile-update-interval") {
|
||||||
|
Some(value) => match value.to_str().unwrap_or("").parse::<u64>() {
|
||||||
|
Ok(val) => Some(PrfOption {
|
||||||
|
update_interval: Some(val * 60), // hour -> min
|
||||||
|
..PrfOption::default()
|
||||||
|
}),
|
||||||
|
Err(_) => None,
|
||||||
|
},
|
||||||
|
None => None,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let home = match header.get("profile-web-page-url") {
|
||||||
|
Some(value) => {
|
||||||
|
let str_value = value.to_str().unwrap_or("");
|
||||||
|
Some(str_value.to_string())
|
||||||
|
},
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let uid = help::get_uid("r");
|
||||||
|
let file = format!("{uid}.yaml");
|
||||||
|
let name = name.unwrap_or(filename.unwrap_or("Remote File".into()));
|
||||||
|
let data = resp.text_with_charset("utf-8").await?;
|
||||||
|
|
||||||
|
// process the charset "UTF-8 with BOM"
|
||||||
|
let data = data.trim_start_matches('\u{feff}');
|
||||||
|
|
||||||
|
// check the data whether the valid yaml format
|
||||||
|
let yaml = serde_yaml::from_str::<Mapping>(data)
|
||||||
|
.context("the remote profile data is invalid yaml")?;
|
||||||
|
|
||||||
|
if !yaml.contains_key("proxies") && !yaml.contains_key("proxy-providers") {
|
||||||
|
bail!("profile does not contain `proxies` or `proxy-providers`");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(PrfItem {
|
||||||
|
uid: Some(uid),
|
||||||
|
itype: Some("remote".into()),
|
||||||
|
name: Some(name),
|
||||||
|
desc,
|
||||||
|
file: Some(file),
|
||||||
|
url: Some(url.into()),
|
||||||
|
selected: None,
|
||||||
|
extra,
|
||||||
|
option,
|
||||||
|
home,
|
||||||
|
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||||
|
file_data: Some(data.into()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ## Merge type (enhance)
|
||||||
|
/// create the enhanced item by using `merge` rule
|
||||||
|
pub fn from_merge(name: String, desc: String) -> Result<PrfItem> {
|
||||||
|
let uid = help::get_uid("m");
|
||||||
|
let file = format!("{uid}.yaml");
|
||||||
|
|
||||||
|
Ok(PrfItem {
|
||||||
|
uid: Some(uid),
|
||||||
|
itype: Some("merge".into()),
|
||||||
|
name: Some(name),
|
||||||
|
desc: Some(desc),
|
||||||
|
file: Some(file),
|
||||||
|
url: None,
|
||||||
|
selected: None,
|
||||||
|
extra: None,
|
||||||
|
option: None,
|
||||||
|
home: None,
|
||||||
|
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||||
|
file_data: Some(tmpl::ITEM_MERGE.into()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ## Script type (enhance)
|
||||||
|
/// create the enhanced item by using javascript quick.js
|
||||||
|
pub fn from_script(name: String, desc: String) -> Result<PrfItem> {
|
||||||
|
let uid = help::get_uid("s");
|
||||||
|
let file = format!("{uid}.js"); // js ext
|
||||||
|
|
||||||
|
Ok(PrfItem {
|
||||||
|
uid: Some(uid),
|
||||||
|
itype: Some("script".into()),
|
||||||
|
name: Some(name),
|
||||||
|
desc: Some(desc),
|
||||||
|
file: Some(file),
|
||||||
|
url: None,
|
||||||
|
home: None,
|
||||||
|
selected: None,
|
||||||
|
extra: None,
|
||||||
|
option: None,
|
||||||
|
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||||
|
file_data: Some(tmpl::ITEM_SCRIPT.into()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// get the file data
|
||||||
|
pub fn read_file(&self) -> Result<String> {
|
||||||
|
if self.file.is_none() {
|
||||||
|
bail!("could not find the file");
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = self.file.clone().unwrap();
|
||||||
|
let path = dirs::app_profiles_dir()?.join(file);
|
||||||
|
fs::read_to_string(path).context("failed to read the file")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// save the file data
|
||||||
|
pub fn save_file(&self, data: String) -> Result<()> {
|
||||||
|
if self.file.is_none() {
|
||||||
|
bail!("could not find the file");
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = self.file.clone().unwrap();
|
||||||
|
let path = dirs::app_profiles_dir()?.join(file);
|
||||||
|
fs::write(path, data.as_bytes()).context("failed to save the file")
|
||||||
|
}
|
||||||
|
}
|
||||||
298
src-tauri/src/config/profiles.rs
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
use super::prfitem::PrfItem;
|
||||||
|
use crate::utils::{dirs, help};
|
||||||
|
use anyhow::{bail, Context, Result};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_yaml::Mapping;
|
||||||
|
use std::{fs, io::Write};
|
||||||
|
|
||||||
|
/// Define the `profiles.yaml` schema
|
||||||
|
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct IProfiles {
|
||||||
|
/// same as PrfConfig.current
|
||||||
|
pub current: Option<String>,
|
||||||
|
|
||||||
|
/// same as PrfConfig.chain
|
||||||
|
pub chain: Option<Vec<String>>,
|
||||||
|
|
||||||
|
/// profile list
|
||||||
|
pub items: Option<Vec<PrfItem>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! patch {
|
||||||
|
($lv: expr, $rv: expr, $key: tt) => {
|
||||||
|
if ($rv.$key).is_some() {
|
||||||
|
$lv.$key = $rv.$key;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IProfiles {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
match dirs::profiles_path().and_then(|path| help::read_yaml::<Self>(&path)) {
|
||||||
|
Ok(mut profiles) => {
|
||||||
|
if profiles.items.is_none() {
|
||||||
|
profiles.items = Some(vec![]);
|
||||||
|
}
|
||||||
|
// compatible with the old old old version
|
||||||
|
if let Some(items) = profiles.items.as_mut() {
|
||||||
|
for item in items.iter_mut() {
|
||||||
|
if item.uid.is_none() {
|
||||||
|
item.uid = Some(help::get_uid("d"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
profiles
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log::error!(target: "app", "{err}");
|
||||||
|
Self::template()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn template() -> Self {
|
||||||
|
Self {
|
||||||
|
items: Some(vec![]),
|
||||||
|
..Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_file(&self) -> Result<()> {
|
||||||
|
help::save_yaml(
|
||||||
|
&dirs::profiles_path()?,
|
||||||
|
self,
|
||||||
|
Some("# Profiles Config for Clash Verge"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 只修改current,valid和chain
|
||||||
|
pub fn patch_config(&mut self, patch: IProfiles) -> Result<()> {
|
||||||
|
if self.items.is_none() {
|
||||||
|
self.items = Some(vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(current) = patch.current {
|
||||||
|
let items = self.items.as_ref().unwrap();
|
||||||
|
let some_uid = Some(current);
|
||||||
|
|
||||||
|
if items.iter().any(|e| e.uid == some_uid) {
|
||||||
|
self.current = some_uid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(chain) = patch.chain {
|
||||||
|
self.chain = Some(chain);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_current(&self) -> Option<String> {
|
||||||
|
self.current.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// get items ref
|
||||||
|
pub fn get_items(&self) -> Option<&Vec<PrfItem>> {
|
||||||
|
self.items.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// find the item by the uid
|
||||||
|
pub fn get_item(&self, uid: &String) -> Result<&PrfItem> {
|
||||||
|
if let Some(items) = self.items.as_ref() {
|
||||||
|
let some_uid = Some(uid.clone());
|
||||||
|
|
||||||
|
for each in items.iter() {
|
||||||
|
if each.uid == some_uid {
|
||||||
|
return Ok(each);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bail!("failed to get the profile item \"uid:{uid}\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// append new item
|
||||||
|
/// if the file_data is some
|
||||||
|
/// then should save the data to file
|
||||||
|
pub fn append_item(&mut self, mut item: PrfItem) -> Result<()> {
|
||||||
|
if item.uid.is_none() {
|
||||||
|
bail!("the uid should not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
// save the file data
|
||||||
|
// move the field value after save
|
||||||
|
if let Some(file_data) = item.file_data.take() {
|
||||||
|
if item.file.is_none() {
|
||||||
|
bail!("the file should not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = item.file.clone().unwrap();
|
||||||
|
let path = dirs::app_profiles_dir()?.join(&file);
|
||||||
|
|
||||||
|
fs::File::create(path)
|
||||||
|
.with_context(|| format!("failed to create file \"{}\"", file))?
|
||||||
|
.write(file_data.as_bytes())
|
||||||
|
.with_context(|| format!("failed to write to file \"{}\"", file))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.items.is_none() {
|
||||||
|
self.items = Some(vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(items) = self.items.as_mut() {
|
||||||
|
items.push(item)
|
||||||
|
}
|
||||||
|
self.save_file()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// reorder items
|
||||||
|
pub fn reorder(&mut self, active_id: String, over_id: String) -> Result<()> {
|
||||||
|
let mut items = self.items.take().unwrap_or_default();
|
||||||
|
let mut old_index = None;
|
||||||
|
let mut new_index = None;
|
||||||
|
|
||||||
|
for (i, _) in items.iter().enumerate() {
|
||||||
|
if items[i].uid == Some(active_id.clone()) {
|
||||||
|
old_index = Some(i);
|
||||||
|
}
|
||||||
|
if items[i].uid == Some(over_id.clone()) {
|
||||||
|
new_index = Some(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if old_index.is_none() || new_index.is_none() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let item = items.remove(old_index.unwrap());
|
||||||
|
items.insert(new_index.unwrap(), item);
|
||||||
|
self.items = Some(items);
|
||||||
|
self.save_file()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// update the item value
|
||||||
|
pub fn patch_item(&mut self, uid: String, item: PrfItem) -> Result<()> {
|
||||||
|
let mut items = self.items.take().unwrap_or_default();
|
||||||
|
|
||||||
|
for each in items.iter_mut() {
|
||||||
|
if each.uid == Some(uid.clone()) {
|
||||||
|
patch!(each, item, itype);
|
||||||
|
patch!(each, item, name);
|
||||||
|
patch!(each, item, desc);
|
||||||
|
patch!(each, item, file);
|
||||||
|
patch!(each, item, url);
|
||||||
|
patch!(each, item, selected);
|
||||||
|
patch!(each, item, extra);
|
||||||
|
patch!(each, item, updated);
|
||||||
|
patch!(each, item, option);
|
||||||
|
|
||||||
|
self.items = Some(items);
|
||||||
|
return self.save_file();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.items = Some(items);
|
||||||
|
bail!("failed to find the profile item \"uid:{uid}\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// be used to update the remote item
|
||||||
|
/// only patch `updated` `extra` `file_data`
|
||||||
|
pub fn update_item(&mut self, uid: String, mut item: PrfItem) -> Result<()> {
|
||||||
|
if self.items.is_none() {
|
||||||
|
self.items = Some(vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// find the item
|
||||||
|
let _ = self.get_item(&uid)?;
|
||||||
|
|
||||||
|
if let Some(items) = self.items.as_mut() {
|
||||||
|
let some_uid = Some(uid.clone());
|
||||||
|
|
||||||
|
for each in items.iter_mut() {
|
||||||
|
if each.uid == some_uid {
|
||||||
|
each.extra = item.extra;
|
||||||
|
each.updated = item.updated;
|
||||||
|
each.home = item.home;
|
||||||
|
// save the file data
|
||||||
|
// move the field value after save
|
||||||
|
if let Some(file_data) = item.file_data.take() {
|
||||||
|
let file = each.file.take();
|
||||||
|
let file =
|
||||||
|
file.unwrap_or(item.file.take().unwrap_or(format!("{}.yaml", &uid)));
|
||||||
|
|
||||||
|
// the file must exists
|
||||||
|
each.file = Some(file.clone());
|
||||||
|
|
||||||
|
let path = dirs::app_profiles_dir()?.join(&file);
|
||||||
|
|
||||||
|
fs::File::create(path)
|
||||||
|
.with_context(|| format!("failed to create file \"{}\"", file))?
|
||||||
|
.write(file_data.as_bytes())
|
||||||
|
.with_context(|| format!("failed to write to file \"{}\"", file))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.save_file()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// delete item
|
||||||
|
/// if delete the current then return true
|
||||||
|
pub fn delete_item(&mut self, uid: String) -> Result<bool> {
|
||||||
|
let current = self.current.as_ref().unwrap_or(&uid);
|
||||||
|
let current = current.clone();
|
||||||
|
|
||||||
|
let mut items = self.items.take().unwrap_or_default();
|
||||||
|
let mut index = None;
|
||||||
|
|
||||||
|
// get the index
|
||||||
|
for (i, _) in items.iter().enumerate() {
|
||||||
|
if items[i].uid == Some(uid.clone()) {
|
||||||
|
index = Some(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(index) = index {
|
||||||
|
if let Some(file) = items.remove(index).file {
|
||||||
|
let _ = dirs::app_profiles_dir().map(|path| {
|
||||||
|
let path = path.join(file);
|
||||||
|
if path.exists() {
|
||||||
|
let _ = fs::remove_file(path);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete the original uid
|
||||||
|
if current == uid {
|
||||||
|
self.current = match !items.is_empty() {
|
||||||
|
true => items[0].uid.clone(),
|
||||||
|
false => None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
self.items = Some(items);
|
||||||
|
self.save_file()?;
|
||||||
|
Ok(current == uid)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取current指向的订阅内容
|
||||||
|
pub fn current_mapping(&self) -> Result<Mapping> {
|
||||||
|
match (self.current.as_ref(), self.items.as_ref()) {
|
||||||
|
(Some(current), Some(items)) => {
|
||||||
|
if let Some(item) = items.iter().find(|e| e.uid.as_ref() == Some(current)) {
|
||||||
|
let file_path = match item.file.as_ref() {
|
||||||
|
Some(file) => dirs::app_profiles_dir()?.join(file),
|
||||||
|
None => bail!("failed to get the file field"),
|
||||||
|
};
|
||||||
|
return help::read_merge_mapping(&file_path);
|
||||||
|
}
|
||||||
|
bail!("failed to find the current profile \"uid:{current}\"");
|
||||||
|
}
|
||||||
|
_ => Ok(Mapping::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
45
src-tauri/src/config/runtime.rs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
use crate::enhance::field::use_keys;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_yaml::{Mapping, Value};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct IRuntime {
|
||||||
|
pub config: Option<Mapping>,
|
||||||
|
// 记录在订阅中(包括merge和script生成的)出现过的keys
|
||||||
|
// 这些keys不一定都生效
|
||||||
|
pub exists_keys: Vec<String>,
|
||||||
|
pub chain_logs: HashMap<String, Vec<(String, String)>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IRuntime {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 这里只更改 allow-lan | ipv6 | log-level | tun
|
||||||
|
pub fn patch_config(&mut self, patch: Mapping) {
|
||||||
|
if let Some(config) = self.config.as_mut() {
|
||||||
|
["allow-lan", "ipv6", "log-level"]
|
||||||
|
.into_iter()
|
||||||
|
.for_each(|key| {
|
||||||
|
if let Some(value) = patch.get(key).to_owned() {
|
||||||
|
config.insert(key.into(), value.clone());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let tun = config.get("tun");
|
||||||
|
let mut tun = tun.map_or(Mapping::new(), |val| {
|
||||||
|
val.as_mapping().cloned().unwrap_or(Mapping::new())
|
||||||
|
});
|
||||||
|
let patch_tun = patch.get("tun");
|
||||||
|
let patch_tun = patch_tun.map_or(Mapping::new(), |val| {
|
||||||
|
val.as_mapping().cloned().unwrap_or(Mapping::new())
|
||||||
|
});
|
||||||
|
use_keys(&patch_tun).into_iter().for_each(|key| {
|
||||||
|
if let Some(value) = patch_tun.get(&key).to_owned() {
|
||||||
|
tun.insert(key.into(), value.clone());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
config.insert("tun".into(), Value::from(tun));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
310
src-tauri/src/config/verge.rs
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
use crate::utils::{dirs, help};
|
||||||
|
use anyhow::Result;
|
||||||
|
use log::LevelFilter;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// ### `verge.yaml` schema
|
||||||
|
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct IVerge {
|
||||||
|
/// app listening port for app singleton
|
||||||
|
pub app_singleton_port: Option<u16>,
|
||||||
|
|
||||||
|
/// app log level
|
||||||
|
/// silent | error | warn | info | debug | trace
|
||||||
|
pub app_log_level: Option<String>,
|
||||||
|
|
||||||
|
// i18n
|
||||||
|
pub language: Option<String>,
|
||||||
|
|
||||||
|
/// `light` or `dark` or `system`
|
||||||
|
pub theme_mode: Option<String>,
|
||||||
|
|
||||||
|
/// tray click event
|
||||||
|
pub tray_event: Option<String>,
|
||||||
|
|
||||||
|
/// copy env type
|
||||||
|
pub env_type: Option<String>,
|
||||||
|
|
||||||
|
/// start page
|
||||||
|
pub start_page: Option<String>,
|
||||||
|
/// startup script path
|
||||||
|
pub startup_script: Option<String>,
|
||||||
|
|
||||||
|
/// enable traffic graph default is true
|
||||||
|
pub traffic_graph: Option<bool>,
|
||||||
|
|
||||||
|
/// show memory info (only for Clash Meta)
|
||||||
|
pub enable_memory_usage: Option<bool>,
|
||||||
|
|
||||||
|
/// enable group icon
|
||||||
|
pub enable_group_icon: Option<bool>,
|
||||||
|
|
||||||
|
/// common tray icon
|
||||||
|
pub common_tray_icon: Option<bool>,
|
||||||
|
|
||||||
|
/// menu icon
|
||||||
|
pub menu_icon: Option<String>,
|
||||||
|
|
||||||
|
/// sysproxy tray icon
|
||||||
|
pub sysproxy_tray_icon: Option<bool>,
|
||||||
|
|
||||||
|
/// tun tray icon
|
||||||
|
pub tun_tray_icon: Option<bool>,
|
||||||
|
|
||||||
|
/// clash tun mode
|
||||||
|
pub enable_tun_mode: Option<bool>,
|
||||||
|
|
||||||
|
/// windows service mode
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub enable_service_mode: Option<bool>,
|
||||||
|
|
||||||
|
/// can the app auto startup
|
||||||
|
pub enable_auto_launch: Option<bool>,
|
||||||
|
|
||||||
|
/// not show the window on launch
|
||||||
|
pub enable_silent_start: Option<bool>,
|
||||||
|
|
||||||
|
/// set system proxy
|
||||||
|
pub enable_system_proxy: Option<bool>,
|
||||||
|
|
||||||
|
/// enable proxy guard
|
||||||
|
pub enable_proxy_guard: Option<bool>,
|
||||||
|
|
||||||
|
/// set system proxy bypass
|
||||||
|
pub system_proxy_bypass: Option<String>,
|
||||||
|
|
||||||
|
/// proxy guard duration
|
||||||
|
pub proxy_guard_duration: Option<u64>,
|
||||||
|
|
||||||
|
/// theme setting
|
||||||
|
pub theme_setting: Option<IVergeTheme>,
|
||||||
|
|
||||||
|
/// web ui list
|
||||||
|
pub web_ui_list: Option<Vec<String>>,
|
||||||
|
|
||||||
|
/// clash core path
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub clash_core: Option<String>,
|
||||||
|
|
||||||
|
/// hotkey map
|
||||||
|
/// format: {func},{key}
|
||||||
|
pub hotkeys: Option<Vec<String>>,
|
||||||
|
|
||||||
|
/// 切换代理时自动关闭连接
|
||||||
|
pub auto_close_connection: Option<bool>,
|
||||||
|
|
||||||
|
/// 是否自动检查更新
|
||||||
|
pub auto_check_update: Option<bool>,
|
||||||
|
|
||||||
|
/// 默认的延迟测试连接
|
||||||
|
pub default_latency_test: Option<String>,
|
||||||
|
|
||||||
|
/// 默认的延迟测试超时时间
|
||||||
|
pub default_latency_timeout: Option<i32>,
|
||||||
|
|
||||||
|
/// 是否使用内部的脚本支持,默认为真
|
||||||
|
pub enable_builtin_enhanced: Option<bool>,
|
||||||
|
|
||||||
|
/// proxy 页面布局 列数
|
||||||
|
pub proxy_layout_column: Option<i32>,
|
||||||
|
|
||||||
|
/// 测试网站列表
|
||||||
|
pub test_list: Option<Vec<IVergeTestItem>>,
|
||||||
|
|
||||||
|
/// 日志清理
|
||||||
|
/// 0: 不清理; 1: 7天; 2: 30天; 3: 90天
|
||||||
|
pub auto_log_clean: Option<i32>,
|
||||||
|
|
||||||
|
/// window size and position
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub window_size_position: Option<Vec<f64>>,
|
||||||
|
|
||||||
|
/// window size and position
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub window_is_maximized: Option<bool>,
|
||||||
|
|
||||||
|
/// 是否启用随机端口
|
||||||
|
pub enable_random_port: Option<bool>,
|
||||||
|
|
||||||
|
/// verge 的各种 port 用于覆盖 clash 的各种 port
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
pub verge_redir_port: Option<u16>,
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub verge_tproxy_port: Option<u16>,
|
||||||
|
|
||||||
|
pub verge_mixed_port: Option<u16>,
|
||||||
|
|
||||||
|
pub verge_socks_port: Option<u16>,
|
||||||
|
|
||||||
|
pub verge_port: Option<u16>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct IVergeTestItem {
|
||||||
|
pub uid: Option<String>,
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub icon: Option<String>,
|
||||||
|
pub url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct IVergeTheme {
|
||||||
|
pub primary_color: Option<String>,
|
||||||
|
pub secondary_color: Option<String>,
|
||||||
|
pub primary_text: Option<String>,
|
||||||
|
pub secondary_text: Option<String>,
|
||||||
|
|
||||||
|
pub info_color: Option<String>,
|
||||||
|
pub error_color: Option<String>,
|
||||||
|
pub warning_color: Option<String>,
|
||||||
|
pub success_color: Option<String>,
|
||||||
|
|
||||||
|
pub font_family: Option<String>,
|
||||||
|
pub css_injection: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IVerge {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
match dirs::verge_path().and_then(|path| help::read_yaml::<IVerge>(&path)) {
|
||||||
|
Ok(config) => config,
|
||||||
|
Err(err) => {
|
||||||
|
log::error!(target: "app", "{err}");
|
||||||
|
Self::template()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn template() -> Self {
|
||||||
|
Self {
|
||||||
|
clash_core: Some("clash-meta".into()),
|
||||||
|
language: Some("zh".into()),
|
||||||
|
theme_mode: Some("system".into()),
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
env_type: Some("bash".into()),
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
env_type: Some("powershell".into()),
|
||||||
|
start_page: Some("/".into()),
|
||||||
|
traffic_graph: Some(true),
|
||||||
|
enable_memory_usage: Some(true),
|
||||||
|
enable_group_icon: Some(true),
|
||||||
|
menu_icon: Some("monochrome".into()),
|
||||||
|
common_tray_icon: Some(false),
|
||||||
|
sysproxy_tray_icon: Some(false),
|
||||||
|
tun_tray_icon: Some(false),
|
||||||
|
enable_auto_launch: Some(false),
|
||||||
|
enable_silent_start: Some(false),
|
||||||
|
enable_system_proxy: Some(false),
|
||||||
|
enable_random_port: Some(false),
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
verge_redir_port: Some(7895),
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
verge_tproxy_port: Some(7896),
|
||||||
|
verge_mixed_port: Some(7897),
|
||||||
|
verge_socks_port: Some(7898),
|
||||||
|
verge_port: Some(7899),
|
||||||
|
enable_proxy_guard: Some(false),
|
||||||
|
proxy_guard_duration: Some(30),
|
||||||
|
auto_close_connection: Some(true),
|
||||||
|
auto_check_update: Some(true),
|
||||||
|
enable_builtin_enhanced: Some(true),
|
||||||
|
auto_log_clean: Some(3),
|
||||||
|
..Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save IVerge App Config
|
||||||
|
pub fn save_file(&self) -> Result<()> {
|
||||||
|
help::save_yaml(&dirs::verge_path()?, &self, Some("# Clash Verge Config"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// patch verge config
|
||||||
|
/// only save to file
|
||||||
|
pub fn patch_config(&mut self, patch: IVerge) {
|
||||||
|
macro_rules! patch {
|
||||||
|
($key: tt) => {
|
||||||
|
if patch.$key.is_some() {
|
||||||
|
self.$key = patch.$key;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
patch!(app_log_level);
|
||||||
|
patch!(language);
|
||||||
|
patch!(theme_mode);
|
||||||
|
patch!(tray_event);
|
||||||
|
patch!(env_type);
|
||||||
|
patch!(start_page);
|
||||||
|
patch!(startup_script);
|
||||||
|
patch!(traffic_graph);
|
||||||
|
patch!(enable_memory_usage);
|
||||||
|
patch!(enable_group_icon);
|
||||||
|
patch!(menu_icon);
|
||||||
|
patch!(common_tray_icon);
|
||||||
|
patch!(sysproxy_tray_icon);
|
||||||
|
patch!(tun_tray_icon);
|
||||||
|
|
||||||
|
patch!(enable_tun_mode);
|
||||||
|
patch!(enable_service_mode);
|
||||||
|
patch!(enable_auto_launch);
|
||||||
|
patch!(enable_silent_start);
|
||||||
|
patch!(enable_random_port);
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
patch!(verge_redir_port);
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
patch!(verge_tproxy_port);
|
||||||
|
patch!(verge_mixed_port);
|
||||||
|
patch!(verge_socks_port);
|
||||||
|
patch!(verge_port);
|
||||||
|
patch!(enable_system_proxy);
|
||||||
|
patch!(enable_proxy_guard);
|
||||||
|
patch!(system_proxy_bypass);
|
||||||
|
patch!(proxy_guard_duration);
|
||||||
|
|
||||||
|
patch!(theme_setting);
|
||||||
|
patch!(web_ui_list);
|
||||||
|
patch!(clash_core);
|
||||||
|
patch!(hotkeys);
|
||||||
|
|
||||||
|
patch!(auto_close_connection);
|
||||||
|
patch!(auto_check_update);
|
||||||
|
patch!(default_latency_test);
|
||||||
|
patch!(default_latency_timeout);
|
||||||
|
patch!(enable_builtin_enhanced);
|
||||||
|
patch!(proxy_layout_column);
|
||||||
|
patch!(test_list);
|
||||||
|
patch!(auto_log_clean);
|
||||||
|
patch!(window_size_position);
|
||||||
|
patch!(window_is_maximized);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 在初始化前尝试拿到单例端口的值
|
||||||
|
pub fn get_singleton_port() -> u16 {
|
||||||
|
#[cfg(not(feature = "verge-dev"))]
|
||||||
|
const SERVER_PORT: u16 = 33331;
|
||||||
|
#[cfg(feature = "verge-dev")]
|
||||||
|
const SERVER_PORT: u16 = 11233;
|
||||||
|
|
||||||
|
match dirs::verge_path().and_then(|path| help::read_yaml::<IVerge>(&path)) {
|
||||||
|
Ok(config) => config.app_singleton_port.unwrap_or(SERVER_PORT),
|
||||||
|
Err(_) => SERVER_PORT, // 这里就不log错误了
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取日志等级
|
||||||
|
pub fn get_log_level(&self) -> LevelFilter {
|
||||||
|
if let Some(level) = self.app_log_level.as_ref() {
|
||||||
|
match level.to_lowercase().as_str() {
|
||||||
|
"silent" => LevelFilter::Off,
|
||||||
|
"error" => LevelFilter::Error,
|
||||||
|
"warn" => LevelFilter::Warn,
|
||||||
|
"info" => LevelFilter::Info,
|
||||||
|
"debug" => LevelFilter::Debug,
|
||||||
|
"trace" => LevelFilter::Trace,
|
||||||
|
_ => LevelFilter::Info,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LevelFilter::Info
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,518 +0,0 @@
|
|||||||
use super::{PrfEnhancedResult, Profiles, Verge, VergeConfig};
|
|
||||||
use crate::log_if_err;
|
|
||||||
use crate::utils::{config, dirs, help};
|
|
||||||
use anyhow::{bail, Result};
|
|
||||||
use reqwest::header::HeaderMap;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use serde_yaml::{Mapping, Value};
|
|
||||||
use std::{collections::HashMap, time::Duration};
|
|
||||||
use tauri::api::process::{Command, CommandChild, CommandEvent};
|
|
||||||
use tauri::Window;
|
|
||||||
use tokio::time::sleep;
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
|
||||||
pub struct ClashInfo {
|
|
||||||
/// clash sidecar status
|
|
||||||
pub status: String,
|
|
||||||
|
|
||||||
/// clash core port
|
|
||||||
pub port: Option<String>,
|
|
||||||
|
|
||||||
/// same as `external-controller`
|
|
||||||
pub server: Option<String>,
|
|
||||||
|
|
||||||
/// clash secret
|
|
||||||
pub secret: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Clash {
|
|
||||||
/// maintain the clash config
|
|
||||||
pub config: Mapping,
|
|
||||||
|
|
||||||
/// some info
|
|
||||||
pub info: ClashInfo,
|
|
||||||
|
|
||||||
/// clash sidecar
|
|
||||||
pub sidecar: Option<CommandChild>,
|
|
||||||
|
|
||||||
/// save the main window
|
|
||||||
pub window: Option<Window>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Clash {
|
|
||||||
pub fn new() -> Clash {
|
|
||||||
let config = Clash::read_config();
|
|
||||||
let info = Clash::get_info(&config);
|
|
||||||
|
|
||||||
Clash {
|
|
||||||
config,
|
|
||||||
info,
|
|
||||||
sidecar: None,
|
|
||||||
window: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// get clash config
|
|
||||||
fn read_config() -> Mapping {
|
|
||||||
config::read_yaml::<Mapping>(dirs::clash_path())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// save the clash config
|
|
||||||
fn save_config(&self) -> Result<()> {
|
|
||||||
config::save_yaml(
|
|
||||||
dirs::clash_path(),
|
|
||||||
&self.config,
|
|
||||||
Some("# Default Config For Clash Core\n\n"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// parse the clash's config.yaml
|
|
||||||
/// get some information
|
|
||||||
fn get_info(clash_config: &Mapping) -> ClashInfo {
|
|
||||||
let key_port_1 = Value::String("port".to_string());
|
|
||||||
let key_port_2 = Value::String("mixed-port".to_string());
|
|
||||||
let key_server = Value::String("external-controller".to_string());
|
|
||||||
let key_secret = Value::String("secret".to_string());
|
|
||||||
|
|
||||||
let port = match clash_config.get(&key_port_1) {
|
|
||||||
Some(value) => match value {
|
|
||||||
Value::String(val_str) => Some(val_str.clone()),
|
|
||||||
Value::Number(val_num) => Some(val_num.to_string()),
|
|
||||||
_ => None,
|
|
||||||
},
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
let port = match port {
|
|
||||||
Some(_) => port,
|
|
||||||
None => match clash_config.get(&key_port_2) {
|
|
||||||
Some(value) => match value {
|
|
||||||
Value::String(val_str) => Some(val_str.clone()),
|
|
||||||
Value::Number(val_num) => Some(val_num.to_string()),
|
|
||||||
_ => None,
|
|
||||||
},
|
|
||||||
_ => None,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let server = match clash_config.get(&key_server) {
|
|
||||||
Some(value) => match value {
|
|
||||||
Value::String(val_str) => {
|
|
||||||
// `external-controller` could be
|
|
||||||
// "127.0.0.1:9090" or ":9090"
|
|
||||||
// Todo: maybe it could support single port
|
|
||||||
let server = val_str.clone();
|
|
||||||
let server = match server.starts_with(":") {
|
|
||||||
true => format!("127.0.0.1{server}"),
|
|
||||||
false => server,
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(server)
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
},
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
let secret = match clash_config.get(&key_secret) {
|
|
||||||
Some(value) => match value {
|
|
||||||
Value::String(val_str) => Some(val_str.clone()),
|
|
||||||
Value::Bool(val_bool) => Some(val_bool.to_string()),
|
|
||||||
Value::Number(val_num) => Some(val_num.to_string()),
|
|
||||||
_ => None,
|
|
||||||
},
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
ClashInfo {
|
|
||||||
status: "init".into(),
|
|
||||||
port,
|
|
||||||
server,
|
|
||||||
secret,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// save the main window
|
|
||||||
pub fn set_window(&mut self, win: Option<Window>) {
|
|
||||||
self.window = win;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// run clash sidecar
|
|
||||||
pub fn run_sidecar(&mut self) -> Result<()> {
|
|
||||||
let app_dir = dirs::app_home_dir();
|
|
||||||
let app_dir = app_dir.as_os_str().to_str().unwrap();
|
|
||||||
|
|
||||||
match Command::new_sidecar("clash") {
|
|
||||||
Ok(cmd) => match cmd.args(["-d", app_dir]).spawn() {
|
|
||||||
Ok((mut rx, cmd_child)) => {
|
|
||||||
self.sidecar = Some(cmd_child);
|
|
||||||
|
|
||||||
// clash log
|
|
||||||
tauri::async_runtime::spawn(async move {
|
|
||||||
while let Some(event) = rx.recv().await {
|
|
||||||
match event {
|
|
||||||
CommandEvent::Stdout(line) => log::info!("[clash]: {}", line),
|
|
||||||
CommandEvent::Stderr(err) => log::error!("[clash]: {}", err),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Err(err) => bail!(err.to_string()),
|
|
||||||
},
|
|
||||||
Err(err) => bail!(err.to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// drop clash sidecar
|
|
||||||
pub fn drop_sidecar(&mut self) -> Result<()> {
|
|
||||||
if let Some(sidecar) = self.sidecar.take() {
|
|
||||||
sidecar.kill()?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// restart clash sidecar
|
|
||||||
/// should reactivate profile after restart
|
|
||||||
pub fn restart_sidecar(&mut self, profiles: &mut Profiles) -> Result<()> {
|
|
||||||
self.update_config();
|
|
||||||
self.drop_sidecar()?;
|
|
||||||
self.run_sidecar()?;
|
|
||||||
self.activate(profiles)?;
|
|
||||||
self.activate_enhanced(profiles, false, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// update the clash info
|
|
||||||
pub fn update_config(&mut self) {
|
|
||||||
self.config = Clash::read_config();
|
|
||||||
self.info = Clash::get_info(&self.config);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// patch update the clash config
|
|
||||||
pub fn patch_config(
|
|
||||||
&mut self,
|
|
||||||
patch: Mapping,
|
|
||||||
verge: &mut Verge,
|
|
||||||
profiles: &mut Profiles,
|
|
||||||
) -> Result<()> {
|
|
||||||
let mix_port_key = Value::from("mixed-port");
|
|
||||||
let mut port = None;
|
|
||||||
|
|
||||||
for (key, value) in patch.into_iter() {
|
|
||||||
let value = value.clone();
|
|
||||||
|
|
||||||
// check whether the mix_port is changed
|
|
||||||
if key == mix_port_key {
|
|
||||||
if value.is_number() {
|
|
||||||
port = value.as_i64().as_ref().map(|n| n.to_string());
|
|
||||||
} else {
|
|
||||||
port = value.as_str().as_ref().map(|s| s.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.config.insert(key.clone(), value);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.save_config()?;
|
|
||||||
|
|
||||||
if let Some(port) = port {
|
|
||||||
self.restart_sidecar(profiles)?;
|
|
||||||
verge.init_sysproxy(Some(port));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// revise the `tun` and `dns` config
|
|
||||||
fn _tun_mode(mut config: Mapping, enable: bool) -> Mapping {
|
|
||||||
macro_rules! revise {
|
|
||||||
($map: expr, $key: expr, $val: expr) => {
|
|
||||||
let ret_key = Value::String($key.into());
|
|
||||||
$map.insert(ret_key, Value::from($val));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// if key not exists then append value
|
|
||||||
macro_rules! append {
|
|
||||||
($map: expr, $key: expr, $val: expr) => {
|
|
||||||
let ret_key = Value::String($key.into());
|
|
||||||
if !$map.contains_key(&ret_key) {
|
|
||||||
$map.insert(ret_key, Value::from($val));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// tun config
|
|
||||||
let tun_val = config.get(&Value::from("tun"));
|
|
||||||
let mut new_tun = Mapping::new();
|
|
||||||
|
|
||||||
if tun_val.is_some() && tun_val.as_ref().unwrap().is_mapping() {
|
|
||||||
new_tun = tun_val.as_ref().unwrap().as_mapping().unwrap().clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
revise!(new_tun, "enable", enable);
|
|
||||||
|
|
||||||
if enable {
|
|
||||||
append!(new_tun, "stack", "gvisor");
|
|
||||||
append!(new_tun, "dns-hijack", vec!["198.18.0.2:53"]);
|
|
||||||
append!(new_tun, "auto-route", true);
|
|
||||||
append!(new_tun, "auto-detect-interface", true);
|
|
||||||
}
|
|
||||||
|
|
||||||
revise!(config, "tun", new_tun);
|
|
||||||
|
|
||||||
// dns config
|
|
||||||
let dns_val = config.get(&Value::from("dns"));
|
|
||||||
let mut new_dns = Mapping::new();
|
|
||||||
|
|
||||||
if dns_val.is_some() && dns_val.as_ref().unwrap().is_mapping() {
|
|
||||||
new_dns = dns_val.as_ref().unwrap().as_mapping().unwrap().clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 借鉴cfw的默认配置
|
|
||||||
revise!(new_dns, "enable", enable);
|
|
||||||
|
|
||||||
if enable {
|
|
||||||
append!(new_dns, "enhanced-mode", "fake-ip");
|
|
||||||
append!(
|
|
||||||
new_dns,
|
|
||||||
"nameserver",
|
|
||||||
vec!["114.114.114.114", "223.5.5.5", "8.8.8.8"]
|
|
||||||
);
|
|
||||||
append!(new_dns, "fallback", vec![] as Vec<&str>);
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
append!(
|
|
||||||
new_dns,
|
|
||||||
"fake-ip-filter",
|
|
||||||
vec![
|
|
||||||
"dns.msftncsi.com",
|
|
||||||
"www.msftncsi.com",
|
|
||||||
"www.msftconnecttest.com"
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
revise!(config, "dns", new_dns);
|
|
||||||
config
|
|
||||||
}
|
|
||||||
|
|
||||||
/// activate the profile
|
|
||||||
/// generate a new profile to the temp_dir
|
|
||||||
/// then put the path to the clash core
|
|
||||||
fn _activate(info: ClashInfo, config: Mapping, window: Option<Window>) -> Result<()> {
|
|
||||||
let verge_config = VergeConfig::new();
|
|
||||||
let tun_enable = verge_config.enable_tun_mode.unwrap_or(false);
|
|
||||||
|
|
||||||
let config = Clash::_tun_mode(config, tun_enable);
|
|
||||||
|
|
||||||
let temp_path = dirs::profiles_temp_path();
|
|
||||||
config::save_yaml(temp_path.clone(), &config, Some("# Clash Verge Temp File"))?;
|
|
||||||
|
|
||||||
tauri::async_runtime::spawn(async move {
|
|
||||||
let server = info.server.unwrap();
|
|
||||||
let server = format!("http://{server}/configs");
|
|
||||||
|
|
||||||
let mut headers = HeaderMap::new();
|
|
||||||
headers.insert("Content-Type", "application/json".parse().unwrap());
|
|
||||||
|
|
||||||
if let Some(secret) = info.secret.as_ref() {
|
|
||||||
let secret = format!("Bearer {}", secret.clone()).parse().unwrap();
|
|
||||||
headers.insert("Authorization", secret);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut data = HashMap::new();
|
|
||||||
data.insert("path", temp_path.as_os_str().to_str().unwrap());
|
|
||||||
|
|
||||||
// retry 5 times
|
|
||||||
for _ in 0..5 {
|
|
||||||
match reqwest::ClientBuilder::new().no_proxy().build() {
|
|
||||||
Ok(client) => {
|
|
||||||
let builder = client.put(&server).headers(headers.clone()).json(&data);
|
|
||||||
|
|
||||||
match builder.send().await {
|
|
||||||
Ok(resp) => {
|
|
||||||
if resp.status() != 204 {
|
|
||||||
log::error!("failed to activate clash for status \"{}\"", resp.status());
|
|
||||||
}
|
|
||||||
|
|
||||||
// emit the window to update something
|
|
||||||
if let Some(window) = window {
|
|
||||||
window.emit("verge://refresh-clash-config", "yes").unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// do not retry
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Err(err) => log::error!("failed to activate for `{err}`"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => log::error!("failed to activate for `{err}`"),
|
|
||||||
}
|
|
||||||
sleep(Duration::from_millis(500)).await;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// enhanced profiles mode
|
|
||||||
/// - (sync) refresh config if enhance chain is null
|
|
||||||
/// - (async) enhanced config
|
|
||||||
pub fn activate_enhanced(&self, profiles: &Profiles, delay: bool, skip: bool) -> Result<()> {
|
|
||||||
if self.window.is_none() {
|
|
||||||
bail!("failed to get the main window");
|
|
||||||
}
|
|
||||||
|
|
||||||
let event_name = help::get_uid("e");
|
|
||||||
let event_name = format!("enhanced-cb-{event_name}");
|
|
||||||
|
|
||||||
// generate the payload
|
|
||||||
let payload = profiles.gen_enhanced(event_name.clone())?;
|
|
||||||
|
|
||||||
let info = self.info.clone();
|
|
||||||
|
|
||||||
// do not run enhanced
|
|
||||||
if payload.chain.len() == 0 {
|
|
||||||
if skip {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut config = self.config.clone();
|
|
||||||
let filter_data = Clash::strict_filter(payload.current);
|
|
||||||
|
|
||||||
for (key, value) in filter_data.into_iter() {
|
|
||||||
config.insert(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Clash::_activate(info, config, self.window.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
let window = self.window.clone().unwrap();
|
|
||||||
let window_move = self.window.clone();
|
|
||||||
|
|
||||||
window.once(&event_name, move |event| {
|
|
||||||
if let Some(result) = event.payload() {
|
|
||||||
let result: PrfEnhancedResult = serde_json::from_str(result).unwrap();
|
|
||||||
|
|
||||||
if let Some(data) = result.data {
|
|
||||||
let mut config = Clash::read_config();
|
|
||||||
let filter_data = Clash::loose_filter(data); // loose filter
|
|
||||||
|
|
||||||
for (key, value) in filter_data.into_iter() {
|
|
||||||
config.insert(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
log_if_err!(Clash::_activate(info, config, window_move));
|
|
||||||
log::info!("profile enhanced status {}", result.status);
|
|
||||||
}
|
|
||||||
|
|
||||||
result.error.map(|err| log::error!("{err}"));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tauri::async_runtime::spawn(async move {
|
|
||||||
// wait the window setup during resolve app
|
|
||||||
if delay {
|
|
||||||
sleep(Duration::from_secs(2)).await;
|
|
||||||
}
|
|
||||||
window.emit("script-handler", payload).unwrap();
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// activate the profile
|
|
||||||
/// auto activate enhanced profile
|
|
||||||
pub fn activate(&self, profiles: &Profiles) -> Result<()> {
|
|
||||||
let data = profiles.gen_activate()?;
|
|
||||||
let data = Clash::strict_filter(data);
|
|
||||||
|
|
||||||
let info = self.info.clone();
|
|
||||||
let mut config = self.config.clone();
|
|
||||||
|
|
||||||
for (key, value) in data.into_iter() {
|
|
||||||
config.insert(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
Clash::_activate(info, config, self.window.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// only 5 default fields available (clash config fields)
|
|
||||||
/// convert to lowercase
|
|
||||||
fn strict_filter(config: Mapping) -> Mapping {
|
|
||||||
// Only the following fields are allowed:
|
|
||||||
// proxies/proxy-providers/proxy-groups/rule-providers/rules
|
|
||||||
let valid_keys = vec![
|
|
||||||
"proxies",
|
|
||||||
"proxy-providers",
|
|
||||||
"proxy-groups",
|
|
||||||
"rules",
|
|
||||||
"rule-providers",
|
|
||||||
];
|
|
||||||
|
|
||||||
let mut new_config = Mapping::new();
|
|
||||||
|
|
||||||
for (key, value) in config.into_iter() {
|
|
||||||
key.as_str().map(|key_str| {
|
|
||||||
// change to lowercase
|
|
||||||
let mut key_str = String::from(key_str);
|
|
||||||
key_str.make_ascii_lowercase();
|
|
||||||
|
|
||||||
// filter
|
|
||||||
if valid_keys.contains(&&*key_str) {
|
|
||||||
new_config.insert(Value::String(key_str), value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
new_config
|
|
||||||
}
|
|
||||||
|
|
||||||
/// more clash config fields available
|
|
||||||
/// convert to lowercase
|
|
||||||
fn loose_filter(config: Mapping) -> Mapping {
|
|
||||||
// all of these can not be revised by script or merge
|
|
||||||
// http/https/socks port should be under control
|
|
||||||
let not_allow = vec![
|
|
||||||
"port",
|
|
||||||
"socks-port",
|
|
||||||
"mixed-port",
|
|
||||||
"allow-lan",
|
|
||||||
"mode",
|
|
||||||
"external-controller",
|
|
||||||
"secret",
|
|
||||||
"log-level",
|
|
||||||
];
|
|
||||||
|
|
||||||
let mut new_config = Mapping::new();
|
|
||||||
|
|
||||||
for (key, value) in config.into_iter() {
|
|
||||||
key.as_str().map(|key_str| {
|
|
||||||
// change to lowercase
|
|
||||||
let mut key_str = String::from(key_str);
|
|
||||||
key_str.make_ascii_lowercase();
|
|
||||||
|
|
||||||
// filter
|
|
||||||
if !not_allow.contains(&&*key_str) {
|
|
||||||
new_config.insert(Value::String(key_str), value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
new_config
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Clash {
|
|
||||||
fn default() -> Self {
|
|
||||||
Clash::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for Clash {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
if let Err(err) = self.drop_sidecar() {
|
|
||||||
log::error!("{err}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
145
src-tauri/src/core/clash_api.rs
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
use crate::config::Config;
|
||||||
|
use anyhow::{bail, Result};
|
||||||
|
use reqwest::header::HeaderMap;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_yaml::Mapping;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
/// PUT /configs
|
||||||
|
/// path 是绝对路径
|
||||||
|
pub async fn put_configs(path: &str) -> Result<()> {
|
||||||
|
let (url, headers) = clash_client_info()?;
|
||||||
|
let url = format!("{url}/configs");
|
||||||
|
|
||||||
|
let mut data = HashMap::new();
|
||||||
|
data.insert("path", path);
|
||||||
|
|
||||||
|
let client = reqwest::ClientBuilder::new().no_proxy().build()?;
|
||||||
|
let builder = client.put(&url).headers(headers).json(&data);
|
||||||
|
let response = builder.send().await?;
|
||||||
|
|
||||||
|
match response.status().as_u16() {
|
||||||
|
204 => Ok(()),
|
||||||
|
status => {
|
||||||
|
bail!("failed to put configs with status \"{status}\"")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// PATCH /configs
|
||||||
|
pub async fn patch_configs(config: &Mapping) -> Result<()> {
|
||||||
|
let (url, headers) = clash_client_info()?;
|
||||||
|
let url = format!("{url}/configs");
|
||||||
|
|
||||||
|
let client = reqwest::ClientBuilder::new().no_proxy().build()?;
|
||||||
|
let builder = client.patch(&url).headers(headers.clone()).json(config);
|
||||||
|
builder.send().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct DelayRes {
|
||||||
|
delay: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GET /proxies/{name}/delay
|
||||||
|
/// 获取代理延迟
|
||||||
|
pub async fn get_proxy_delay(
|
||||||
|
name: String,
|
||||||
|
test_url: Option<String>,
|
||||||
|
timeout: i32,
|
||||||
|
) -> Result<DelayRes> {
|
||||||
|
let (url, headers) = clash_client_info()?;
|
||||||
|
let url = format!("{url}/proxies/{name}/delay");
|
||||||
|
|
||||||
|
let default_url = "http://1.1.1.1";
|
||||||
|
let test_url = test_url
|
||||||
|
.map(|s| if s.is_empty() { default_url.into() } else { s })
|
||||||
|
.unwrap_or(default_url.into());
|
||||||
|
|
||||||
|
let client = reqwest::ClientBuilder::new().no_proxy().build()?;
|
||||||
|
let builder = client
|
||||||
|
.get(&url)
|
||||||
|
.headers(headers)
|
||||||
|
.query(&[("timeout", &format!("{timeout}")), ("url", &test_url)]);
|
||||||
|
let response = builder.send().await?;
|
||||||
|
|
||||||
|
Ok(response.json::<DelayRes>().await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 根据clash info获取clash服务地址和请求头
|
||||||
|
fn clash_client_info() -> Result<(String, HeaderMap)> {
|
||||||
|
let client = { Config::clash().data().get_client_info() };
|
||||||
|
|
||||||
|
let server = format!("http://{}", client.server);
|
||||||
|
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
headers.insert("Content-Type", "application/json".parse()?);
|
||||||
|
|
||||||
|
if let Some(secret) = client.secret {
|
||||||
|
let secret = format!("Bearer {}", secret).parse()?;
|
||||||
|
headers.insert("Authorization", secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((server, headers))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 缩短clash的日志
|
||||||
|
pub fn parse_log(log: String) -> String {
|
||||||
|
if log.starts_with("time=") && log.len() > 33 {
|
||||||
|
return (log[33..]).to_owned();
|
||||||
|
}
|
||||||
|
if log.len() > 9 {
|
||||||
|
return (log[9..]).to_owned();
|
||||||
|
}
|
||||||
|
log
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 缩短clash -t的错误输出
|
||||||
|
/// 仅适配 clash p核 8-26、clash meta 1.13.1
|
||||||
|
pub fn parse_check_output(log: String) -> String {
|
||||||
|
let t = log.find("time=");
|
||||||
|
let m = log.find("msg=");
|
||||||
|
let mr = log.rfind('"');
|
||||||
|
|
||||||
|
if let (Some(_), Some(m), Some(mr)) = (t, m, mr) {
|
||||||
|
let e = match log.find("level=error msg=") {
|
||||||
|
Some(e) => e + 17,
|
||||||
|
None => m + 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
if mr > m {
|
||||||
|
return (log[e..mr]).to_owned();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let l = log.find("error=");
|
||||||
|
let r = log.find("path=").or(Some(log.len()));
|
||||||
|
|
||||||
|
if let (Some(l), Some(r)) = (l, r) {
|
||||||
|
return (log[(l + 6)..(r - 1)]).to_owned();
|
||||||
|
}
|
||||||
|
|
||||||
|
log
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_check_output() {
|
||||||
|
let str1 = r#"xxxx\n time="2022-11-18T20:42:58+08:00" level=error msg="proxy 0: 'alpn' expected type 'string', got unconvertible type '[]interface {}'""#;
|
||||||
|
let str2 = r#"20:43:49 ERR [Config] configuration file test failed error=proxy 0: unsupport proxy type: hysteria path=xxx"#;
|
||||||
|
let str3 = r#"
|
||||||
|
"time="2022-11-18T21:38:01+08:00" level=info msg="Start initial configuration in progress"
|
||||||
|
time="2022-11-18T21:38:01+08:00" level=error msg="proxy 0: 'alpn' expected type 'string', got unconvertible type '[]interface {}'"
|
||||||
|
configuration file xxx\n
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let res1 = parse_check_output(str1.into());
|
||||||
|
let res2 = parse_check_output(str2.into());
|
||||||
|
let res3 = parse_check_output(str3.into());
|
||||||
|
|
||||||
|
println!("res1: {res1}");
|
||||||
|
println!("res2: {res2}");
|
||||||
|
println!("res3: {res3}");
|
||||||
|
|
||||||
|
assert_eq!(res1, res3);
|
||||||
|
}
|
||||||
319
src-tauri/src/core/core.rs
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
use super::service;
|
||||||
|
use super::{clash_api, logger::Logger};
|
||||||
|
use crate::log_err;
|
||||||
|
use crate::{config::*, utils::dirs};
|
||||||
|
use anyhow::{bail, Context, Result};
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use std::{fs, io::Write, sync::Arc, time::Duration};
|
||||||
|
use sysinfo::{Pid, System};
|
||||||
|
use tauri::api::process::{Command, CommandChild, CommandEvent};
|
||||||
|
use tokio::time::sleep;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CoreManager {
|
||||||
|
sidecar: Arc<Mutex<Option<CommandChild>>>,
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
use_service_mode: Arc<Mutex<bool>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CoreManager {
|
||||||
|
pub fn global() -> &'static CoreManager {
|
||||||
|
static CORE_MANAGER: OnceCell<CoreManager> = OnceCell::new();
|
||||||
|
|
||||||
|
CORE_MANAGER.get_or_init(|| CoreManager {
|
||||||
|
sidecar: Arc::new(Mutex::new(None)),
|
||||||
|
use_service_mode: Arc::new(Mutex::new(false)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init(&self) -> Result<()> {
|
||||||
|
// kill old clash process
|
||||||
|
let _ = dirs::clash_pid_path()
|
||||||
|
.and_then(|path| fs::read(path).map(|p| p.to_vec()).context(""))
|
||||||
|
.and_then(|pid| String::from_utf8_lossy(&pid).parse().context(""))
|
||||||
|
.map(|pid| {
|
||||||
|
let mut system = System::new();
|
||||||
|
system.refresh_all();
|
||||||
|
if let Some(proc) = system.process(Pid::from_u32(pid)) {
|
||||||
|
if proc.name().contains("clash") {
|
||||||
|
log::debug!(target: "app", "kill old clash process");
|
||||||
|
proc.kill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tauri::async_runtime::spawn(async {
|
||||||
|
// 启动clash
|
||||||
|
log_err!(Self::global().run_core().await);
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 检查订阅是否正确
|
||||||
|
pub fn check_config(&self) -> Result<()> {
|
||||||
|
let config_path = Config::generate_file(ConfigType::Check)?;
|
||||||
|
let config_path = dirs::path_to_str(&config_path)?;
|
||||||
|
|
||||||
|
let clash_core = { Config::verge().latest().clash_core.clone() };
|
||||||
|
let clash_core = clash_core.unwrap_or("clash".into());
|
||||||
|
|
||||||
|
let app_dir = dirs::app_home_dir()?;
|
||||||
|
let app_dir = dirs::path_to_str(&app_dir)?;
|
||||||
|
|
||||||
|
let output = Command::new_sidecar(clash_core)?
|
||||||
|
.args(["-t", "-d", app_dir, "-f", config_path])
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
let error = clash_api::parse_check_output(output.stdout.clone());
|
||||||
|
let error = match !error.is_empty() {
|
||||||
|
true => error,
|
||||||
|
false => output.stdout.clone(),
|
||||||
|
};
|
||||||
|
Logger::global().set_log(output.stdout);
|
||||||
|
bail!("{error}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 启动核心
|
||||||
|
pub async fn run_core(&self) -> Result<()> {
|
||||||
|
let config_path = Config::generate_file(ConfigType::Run)?;
|
||||||
|
|
||||||
|
#[allow(unused_mut)]
|
||||||
|
let mut should_kill = match self.sidecar.lock().take() {
|
||||||
|
Some(child) => {
|
||||||
|
log::debug!(target: "app", "stop the core by sidecar");
|
||||||
|
let _ = child.kill();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
None => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if *self.use_service_mode.lock() {
|
||||||
|
log::debug!(target: "app", "stop the core by service");
|
||||||
|
log_err!(service::stop_core_by_service().await);
|
||||||
|
should_kill = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 这里得等一会儿
|
||||||
|
if should_kill {
|
||||||
|
sleep(Duration::from_millis(500)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 服务模式
|
||||||
|
let enable = { Config::verge().latest().enable_service_mode };
|
||||||
|
let enable = enable.unwrap_or(false);
|
||||||
|
|
||||||
|
*self.use_service_mode.lock() = enable;
|
||||||
|
|
||||||
|
if enable {
|
||||||
|
// 服务模式启动失败就直接运行sidecar
|
||||||
|
log::debug!(target: "app", "try to run core in service mode");
|
||||||
|
|
||||||
|
match (|| async {
|
||||||
|
service::check_service().await?;
|
||||||
|
service::run_core_by_service(&config_path).await
|
||||||
|
})()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => return Ok(()),
|
||||||
|
Err(err) => {
|
||||||
|
// 修改这个值,免得stop出错
|
||||||
|
*self.use_service_mode.lock() = false;
|
||||||
|
log::error!(target: "app", "{err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let app_dir = dirs::app_home_dir()?;
|
||||||
|
let app_dir = dirs::path_to_str(&app_dir)?;
|
||||||
|
|
||||||
|
let clash_core = { Config::verge().latest().clash_core.clone() };
|
||||||
|
let clash_core = clash_core.unwrap_or("clash".into());
|
||||||
|
let is_clash = clash_core == "clash";
|
||||||
|
|
||||||
|
let config_path = dirs::path_to_str(&config_path)?;
|
||||||
|
|
||||||
|
let args = match clash_core.as_str() {
|
||||||
|
"clash-meta" => vec!["-d", app_dir, "-f", config_path],
|
||||||
|
"clash-meta-alpha" => vec!["-d", app_dir, "-f", config_path],
|
||||||
|
_ => vec!["-d", app_dir, "-f", config_path],
|
||||||
|
};
|
||||||
|
|
||||||
|
let cmd = Command::new_sidecar(clash_core)?;
|
||||||
|
let (mut rx, cmd_child) = cmd.args(args).spawn()?;
|
||||||
|
|
||||||
|
// 将pid写入文件中
|
||||||
|
crate::log_err!((|| {
|
||||||
|
let pid = cmd_child.pid();
|
||||||
|
let path = dirs::clash_pid_path()?;
|
||||||
|
fs::File::create(path)
|
||||||
|
.context("failed to create the pid file")?
|
||||||
|
.write(format!("{pid}").as_bytes())
|
||||||
|
.context("failed to write pid to the file")?;
|
||||||
|
<Result<()>>::Ok(())
|
||||||
|
})());
|
||||||
|
|
||||||
|
let mut sidecar = self.sidecar.lock();
|
||||||
|
*sidecar = Some(cmd_child);
|
||||||
|
drop(sidecar);
|
||||||
|
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
while let Some(event) = rx.recv().await {
|
||||||
|
match event {
|
||||||
|
CommandEvent::Stdout(line) => {
|
||||||
|
if is_clash {
|
||||||
|
let stdout = clash_api::parse_log(line.clone());
|
||||||
|
log::info!(target: "app", "[clash]: {stdout}");
|
||||||
|
} else {
|
||||||
|
log::info!(target: "app", "[clash]: {line}");
|
||||||
|
};
|
||||||
|
Logger::global().set_log(line);
|
||||||
|
}
|
||||||
|
CommandEvent::Stderr(err) => {
|
||||||
|
// let stdout = clash_api::parse_log(err.clone());
|
||||||
|
log::error!(target: "app", "[clash]: {err}");
|
||||||
|
Logger::global().set_log(err);
|
||||||
|
}
|
||||||
|
CommandEvent::Error(err) => {
|
||||||
|
log::error!(target: "app", "[clash]: {err}");
|
||||||
|
Logger::global().set_log(err);
|
||||||
|
}
|
||||||
|
CommandEvent::Terminated(_) => {
|
||||||
|
log::info!(target: "app", "clash core terminated");
|
||||||
|
let _ = CoreManager::global().recover_core();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 重启内核
|
||||||
|
pub fn recover_core(&'static self) -> Result<()> {
|
||||||
|
// 服务模式不管
|
||||||
|
if *self.use_service_mode.lock() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空原来的sidecar值
|
||||||
|
if let Some(sidecar) = self.sidecar.lock().take() {
|
||||||
|
let _ = sidecar.kill();
|
||||||
|
}
|
||||||
|
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
// 6秒之后再查看服务是否正常 (时间随便搞的)
|
||||||
|
// terminated 可能是切换内核 (切换内核已经有500ms的延迟)
|
||||||
|
sleep(Duration::from_millis(6666)).await;
|
||||||
|
|
||||||
|
if self.sidecar.lock().is_none() {
|
||||||
|
log::info!(target: "app", "recover clash core");
|
||||||
|
|
||||||
|
// 重新启动app
|
||||||
|
if let Err(err) = self.run_core().await {
|
||||||
|
log::error!(target: "app", "failed to recover clash core");
|
||||||
|
log::error!(target: "app", "{err}");
|
||||||
|
|
||||||
|
let _ = self.recover_core();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 停止核心运行
|
||||||
|
pub fn stop_core(&self) -> Result<()> {
|
||||||
|
if *self.use_service_mode.lock() {
|
||||||
|
log::debug!(target: "app", "stop the core by service");
|
||||||
|
tauri::async_runtime::block_on(async move {
|
||||||
|
log_err!(service::stop_core_by_service().await);
|
||||||
|
});
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut sidecar = self.sidecar.lock();
|
||||||
|
if let Some(child) = sidecar.take() {
|
||||||
|
log::debug!(target: "app", "stop the core by sidecar");
|
||||||
|
let _ = child.kill();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 切换核心
|
||||||
|
pub async fn change_core(&self, clash_core: Option<String>) -> Result<()> {
|
||||||
|
let clash_core = clash_core.ok_or(anyhow::anyhow!("clash core is null"))?;
|
||||||
|
const CLASH_CORES: [&str; 2] = ["clash-meta", "clash-meta-alpha"];
|
||||||
|
|
||||||
|
if !CLASH_CORES.contains(&clash_core.as_str()) {
|
||||||
|
bail!("invalid clash core name \"{clash_core}\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
log::debug!(target: "app", "change core to `{clash_core}`");
|
||||||
|
|
||||||
|
Config::verge().draft().clash_core = Some(clash_core);
|
||||||
|
|
||||||
|
// 更新订阅
|
||||||
|
Config::generate()?;
|
||||||
|
|
||||||
|
self.check_config()?;
|
||||||
|
|
||||||
|
// 清掉旧日志
|
||||||
|
Logger::global().clear_log();
|
||||||
|
|
||||||
|
match self.run_core().await {
|
||||||
|
Ok(_) => {
|
||||||
|
Config::verge().apply();
|
||||||
|
Config::runtime().apply();
|
||||||
|
log_err!(Config::verge().latest().save_file());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
Config::verge().discard();
|
||||||
|
Config::runtime().discard();
|
||||||
|
Err(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 更新proxies那些
|
||||||
|
/// 如果涉及端口和外部控制则需要重启
|
||||||
|
pub async fn update_config(&self) -> Result<()> {
|
||||||
|
log::debug!(target: "app", "try to update clash config");
|
||||||
|
|
||||||
|
// 更新订阅
|
||||||
|
Config::generate()?;
|
||||||
|
|
||||||
|
// 检查订阅是否正常
|
||||||
|
self.check_config()?;
|
||||||
|
|
||||||
|
// 更新运行时订阅
|
||||||
|
let path = Config::generate_file(ConfigType::Run)?;
|
||||||
|
let path = dirs::path_to_str(&path)?;
|
||||||
|
|
||||||
|
// 发送请求 发送5次
|
||||||
|
for i in 0..5 {
|
||||||
|
match clash_api::put_configs(path).await {
|
||||||
|
Ok(_) => break,
|
||||||
|
Err(err) => {
|
||||||
|
if i < 4 {
|
||||||
|
log::info!(target: "app", "{err}");
|
||||||
|
} else {
|
||||||
|
bail!(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sleep(Duration::from_millis(250)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
77
src-tauri/src/core/handle.rs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
use super::tray::Tray;
|
||||||
|
use crate::log_err;
|
||||||
|
use anyhow::{bail, Result};
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tauri::{AppHandle, Manager, Window};
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct Handle {
|
||||||
|
pub app_handle: Arc<Mutex<Option<AppHandle>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Handle {
|
||||||
|
pub fn global() -> &'static Handle {
|
||||||
|
static HANDLE: OnceCell<Handle> = OnceCell::new();
|
||||||
|
|
||||||
|
HANDLE.get_or_init(|| Handle {
|
||||||
|
app_handle: Arc::new(Mutex::new(None)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init(&self, app_handle: AppHandle) {
|
||||||
|
*self.app_handle.lock() = Some(app_handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_window(&self) -> Option<Window> {
|
||||||
|
self.app_handle
|
||||||
|
.lock()
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|a| a.get_window("main"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn refresh_clash() {
|
||||||
|
if let Some(window) = Self::global().get_window() {
|
||||||
|
log_err!(window.emit("verge://refresh-clash-config", "yes"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn refresh_verge() {
|
||||||
|
if let Some(window) = Self::global().get_window() {
|
||||||
|
log_err!(window.emit("verge://refresh-verge-config", "yes"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
pub fn refresh_profiles() {
|
||||||
|
if let Some(window) = Self::global().get_window() {
|
||||||
|
log_err!(window.emit("verge://refresh-profiles-config", "yes"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn notice_message<S: Into<String>, M: Into<String>>(status: S, msg: M) {
|
||||||
|
if let Some(window) = Self::global().get_window() {
|
||||||
|
log_err!(window.emit("verge://notice-message", (status.into(), msg.into())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_systray() -> Result<()> {
|
||||||
|
let app_handle = Self::global().app_handle.lock();
|
||||||
|
if app_handle.is_none() {
|
||||||
|
bail!("update_systray unhandled error");
|
||||||
|
}
|
||||||
|
Tray::update_systray(app_handle.as_ref().unwrap())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// update the system tray state
|
||||||
|
pub fn update_systray_part() -> Result<()> {
|
||||||
|
let app_handle = Self::global().app_handle.lock();
|
||||||
|
if app_handle.is_none() {
|
||||||
|
bail!("update_systray unhandled error");
|
||||||
|
}
|
||||||
|
Tray::update_part(app_handle.as_ref().unwrap())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
160
src-tauri/src/core/hotkey.rs
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
use crate::{config::Config, feat, log_err};
|
||||||
|
use anyhow::{bail, Result};
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
use tauri::{AppHandle, GlobalShortcutManager};
|
||||||
|
|
||||||
|
pub struct Hotkey {
|
||||||
|
current: Arc<Mutex<Vec<String>>>, // 保存当前的热键设置
|
||||||
|
|
||||||
|
app_handle: Arc<Mutex<Option<AppHandle>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hotkey {
|
||||||
|
pub fn global() -> &'static Hotkey {
|
||||||
|
static HOTKEY: OnceCell<Hotkey> = OnceCell::new();
|
||||||
|
|
||||||
|
HOTKEY.get_or_init(|| Hotkey {
|
||||||
|
current: Arc::new(Mutex::new(Vec::new())),
|
||||||
|
app_handle: Arc::new(Mutex::new(None)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init(&self, app_handle: AppHandle) -> Result<()> {
|
||||||
|
*self.app_handle.lock() = Some(app_handle);
|
||||||
|
|
||||||
|
let verge = Config::verge();
|
||||||
|
|
||||||
|
if let Some(hotkeys) = verge.latest().hotkeys.as_ref() {
|
||||||
|
for hotkey in hotkeys.iter() {
|
||||||
|
let mut iter = hotkey.split(',');
|
||||||
|
let func = iter.next();
|
||||||
|
let key = iter.next();
|
||||||
|
|
||||||
|
match (key, func) {
|
||||||
|
(Some(key), Some(func)) => {
|
||||||
|
log_err!(self.register(key, func));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let key = key.unwrap_or("None");
|
||||||
|
let func = func.unwrap_or("None");
|
||||||
|
log::error!(target: "app", "invalid hotkey `{key}`:`{func}`");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*self.current.lock() = hotkeys.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_manager(&self) -> Result<impl GlobalShortcutManager> {
|
||||||
|
let app_handle = self.app_handle.lock();
|
||||||
|
if app_handle.is_none() {
|
||||||
|
bail!("failed to get the hotkey manager");
|
||||||
|
}
|
||||||
|
Ok(app_handle.as_ref().unwrap().global_shortcut_manager())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register(&self, hotkey: &str, func: &str) -> Result<()> {
|
||||||
|
let mut manager = self.get_manager()?;
|
||||||
|
|
||||||
|
if manager.is_registered(hotkey)? {
|
||||||
|
manager.unregister(hotkey)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let f = match func.trim() {
|
||||||
|
"open_or_close_dashboard" => feat::open_or_close_dashboard,
|
||||||
|
"clash_mode_rule" => || feat::change_clash_mode("rule".into()),
|
||||||
|
"clash_mode_global" => || feat::change_clash_mode("global".into()),
|
||||||
|
"clash_mode_direct" => || feat::change_clash_mode("direct".into()),
|
||||||
|
"toggle_system_proxy" => feat::toggle_system_proxy,
|
||||||
|
"toggle_tun_mode" => feat::toggle_tun_mode,
|
||||||
|
|
||||||
|
_ => bail!("invalid function \"{func}\""),
|
||||||
|
};
|
||||||
|
|
||||||
|
manager.register(hotkey, f)?;
|
||||||
|
log::info!(target: "app", "register hotkey {hotkey} {func}");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unregister(&self, hotkey: &str) -> Result<()> {
|
||||||
|
self.get_manager()?.unregister(hotkey)?;
|
||||||
|
log::info!(target: "app", "unregister hotkey {hotkey}");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&self, new_hotkeys: Vec<String>) -> Result<()> {
|
||||||
|
let mut current = self.current.lock();
|
||||||
|
let old_map = Self::get_map_from_vec(¤t);
|
||||||
|
let new_map = Self::get_map_from_vec(&new_hotkeys);
|
||||||
|
|
||||||
|
let (del, add) = Self::get_diff(old_map, new_map);
|
||||||
|
|
||||||
|
del.iter().for_each(|key| {
|
||||||
|
let _ = self.unregister(key);
|
||||||
|
});
|
||||||
|
|
||||||
|
add.iter().for_each(|(key, func)| {
|
||||||
|
log_err!(self.register(key, func));
|
||||||
|
});
|
||||||
|
|
||||||
|
*current = new_hotkeys;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_map_from_vec(hotkeys: &Vec<String>) -> HashMap<&str, &str> {
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
|
||||||
|
hotkeys.iter().for_each(|hotkey| {
|
||||||
|
let mut iter = hotkey.split(',');
|
||||||
|
let func = iter.next();
|
||||||
|
let key = iter.next();
|
||||||
|
|
||||||
|
if func.is_some() && key.is_some() {
|
||||||
|
let func = func.unwrap().trim();
|
||||||
|
let key = key.unwrap().trim();
|
||||||
|
map.insert(key, func);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
map
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_diff<'a>(
|
||||||
|
old_map: HashMap<&'a str, &'a str>,
|
||||||
|
new_map: HashMap<&'a str, &'a str>,
|
||||||
|
) -> (Vec<&'a str>, Vec<(&'a str, &'a str)>) {
|
||||||
|
let mut del_list = vec![];
|
||||||
|
let mut add_list = vec![];
|
||||||
|
|
||||||
|
old_map.iter().for_each(|(&key, func)| {
|
||||||
|
match new_map.get(key) {
|
||||||
|
Some(new_func) => {
|
||||||
|
if new_func != func {
|
||||||
|
del_list.push(key);
|
||||||
|
add_list.push((key, *new_func));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => del_list.push(key),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
new_map.iter().for_each(|(&key, &func)| {
|
||||||
|
if old_map.get(key).is_none() {
|
||||||
|
add_list.push((key, func));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
(del_list, add_list)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Hotkey {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Ok(mut manager) = self.get_manager() {
|
||||||
|
let _ = manager.unregister_all();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
src-tauri/src/core/logger.rs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
use once_cell::sync::OnceCell;
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use std::{collections::VecDeque, sync::Arc};
|
||||||
|
|
||||||
|
const LOGS_QUEUE_LEN: usize = 100;
|
||||||
|
|
||||||
|
pub struct Logger {
|
||||||
|
log_data: Arc<Mutex<VecDeque<String>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Logger {
|
||||||
|
pub fn global() -> &'static Logger {
|
||||||
|
static LOGGER: OnceCell<Logger> = OnceCell::new();
|
||||||
|
|
||||||
|
LOGGER.get_or_init(|| Logger {
|
||||||
|
log_data: Arc::new(Mutex::new(VecDeque::with_capacity(LOGS_QUEUE_LEN + 10))),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_log(&self) -> VecDeque<String> {
|
||||||
|
self.log_data.lock().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_log(&self, text: String) {
|
||||||
|
let mut logs = self.log_data.lock();
|
||||||
|
if logs.len() > LOGS_QUEUE_LEN {
|
||||||
|
logs.pop_front();
|
||||||
|
}
|
||||||
|
logs.push_back(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_log(&self) {
|
||||||
|
let mut logs = self.log_data.lock();
|
||||||
|
logs.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
47
src-tauri/src/core/manager.rs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
/// 给clash内核的tun模式授权
|
||||||
|
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||||
|
pub fn grant_permission(core: String) -> anyhow::Result<()> {
|
||||||
|
use std::process::Command;
|
||||||
|
use tauri::utils::platform::current_exe;
|
||||||
|
|
||||||
|
let path = current_exe()?.with_file_name(core).canonicalize()?;
|
||||||
|
let path = path.display().to_string();
|
||||||
|
|
||||||
|
log::debug!("grant_permission path: {path}");
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
let output = {
|
||||||
|
let path = path.replace(' ', "\\\\ ");
|
||||||
|
let shell = format!("chown root:admin {path}\nchmod +sx {path}");
|
||||||
|
let command = format!(r#"do shell script "{shell}" with administrator privileges"#);
|
||||||
|
Command::new("osascript")
|
||||||
|
.args(vec!["-e", &command])
|
||||||
|
.output()?
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
let output = {
|
||||||
|
let path = path.replace(' ', "\\ "); // 避免路径中有空格
|
||||||
|
let shell = format!("setcap cap_net_bind_service,cap_net_admin,cap_dac_override=+ep {path}");
|
||||||
|
|
||||||
|
let sudo = match Command::new("which").arg("pkexec").output() {
|
||||||
|
Ok(output) => {
|
||||||
|
if output.stdout.is_empty() {
|
||||||
|
"sudo"
|
||||||
|
} else {
|
||||||
|
"pkexec"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => "sudo",
|
||||||
|
};
|
||||||
|
|
||||||
|
Command::new(sudo).arg("sh").arg("-c").arg(shell).output()?
|
||||||
|
};
|
||||||
|
|
||||||
|
if output.status.success() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
let stderr = std::str::from_utf8(&output.stderr).unwrap_or("");
|
||||||
|
anyhow::bail!("{stderr}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,13 @@
|
|||||||
mod clash;
|
pub mod clash_api;
|
||||||
mod profiles;
|
mod core;
|
||||||
mod verge;
|
pub mod handle;
|
||||||
|
pub mod hotkey;
|
||||||
|
pub mod logger;
|
||||||
|
pub mod manager;
|
||||||
|
pub mod sysopt;
|
||||||
|
pub mod timer;
|
||||||
|
pub mod tray;
|
||||||
|
pub mod service;
|
||||||
|
pub mod win_uwp;
|
||||||
|
|
||||||
pub use self::clash::*;
|
pub use self::core::*;
|
||||||
pub use self::profiles::*;
|
|
||||||
pub use self::verge::*;
|
|
||||||
|
|||||||
@@ -1,667 +0,0 @@
|
|||||||
use crate::utils::{config, dirs, help, tmpl};
|
|
||||||
use anyhow::{bail, Context, Result};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use serde_yaml::Mapping;
|
|
||||||
use std::{fs, io::Write};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
||||||
pub struct PrfItem {
|
|
||||||
pub uid: Option<String>,
|
|
||||||
|
|
||||||
/// profile item type
|
|
||||||
/// enum value: remote | local | script | merge
|
|
||||||
#[serde(rename = "type")]
|
|
||||||
pub itype: Option<String>,
|
|
||||||
|
|
||||||
/// profile name
|
|
||||||
pub name: Option<String>,
|
|
||||||
|
|
||||||
/// profile description
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub desc: Option<String>,
|
|
||||||
|
|
||||||
/// profile file
|
|
||||||
pub file: Option<String>,
|
|
||||||
|
|
||||||
/// source url
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub url: Option<String>,
|
|
||||||
|
|
||||||
/// selected infomation
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub selected: Option<Vec<PrfSelected>>,
|
|
||||||
|
|
||||||
/// subscription user info
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub extra: Option<PrfExtra>,
|
|
||||||
|
|
||||||
/// updated time
|
|
||||||
pub updated: Option<usize>,
|
|
||||||
|
|
||||||
/// some options of the item
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub option: Option<PrfOption>,
|
|
||||||
|
|
||||||
/// the file data
|
|
||||||
#[serde(skip)]
|
|
||||||
pub file_data: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
|
||||||
pub struct PrfSelected {
|
|
||||||
pub name: Option<String>,
|
|
||||||
pub now: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Copy, Deserialize, Serialize)]
|
|
||||||
pub struct PrfExtra {
|
|
||||||
pub upload: usize,
|
|
||||||
pub download: usize,
|
|
||||||
pub total: usize,
|
|
||||||
pub expire: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
|
||||||
pub struct PrfOption {
|
|
||||||
/// for `remote` profile's http request
|
|
||||||
/// see issue #13
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub user_agent: Option<String>,
|
|
||||||
|
|
||||||
/// for `remote` profile
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub with_proxy: Option<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PrfOption {
|
|
||||||
pub fn merge(one: Option<Self>, other: Option<Self>) -> Option<Self> {
|
|
||||||
if one.is_some() && other.is_some() {
|
|
||||||
let mut one = one.unwrap();
|
|
||||||
let other = other.unwrap();
|
|
||||||
|
|
||||||
if let Some(val) = other.user_agent {
|
|
||||||
one.user_agent = Some(val);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(val) = other.with_proxy {
|
|
||||||
one.with_proxy = Some(val);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Some(one);
|
|
||||||
}
|
|
||||||
|
|
||||||
if one.is_none() {
|
|
||||||
return other;
|
|
||||||
}
|
|
||||||
|
|
||||||
return one;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for PrfItem {
|
|
||||||
fn default() -> Self {
|
|
||||||
PrfItem {
|
|
||||||
uid: None,
|
|
||||||
itype: None,
|
|
||||||
name: None,
|
|
||||||
desc: None,
|
|
||||||
file: None,
|
|
||||||
url: None,
|
|
||||||
selected: None,
|
|
||||||
extra: None,
|
|
||||||
updated: None,
|
|
||||||
option: None,
|
|
||||||
file_data: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PrfItem {
|
|
||||||
/// From partial item
|
|
||||||
/// must contain `itype`
|
|
||||||
pub async fn from(item: PrfItem, file_data: Option<String>) -> Result<PrfItem> {
|
|
||||||
if item.itype.is_none() {
|
|
||||||
bail!("type should not be null");
|
|
||||||
}
|
|
||||||
|
|
||||||
match item.itype.unwrap().as_str() {
|
|
||||||
"remote" => {
|
|
||||||
if item.url.is_none() {
|
|
||||||
bail!("url should not be null");
|
|
||||||
}
|
|
||||||
let url = item.url.as_ref().unwrap().as_str();
|
|
||||||
let name = item.name;
|
|
||||||
let desc = item.desc;
|
|
||||||
PrfItem::from_url(url, name, desc, item.option).await
|
|
||||||
}
|
|
||||||
"local" => {
|
|
||||||
let name = item.name.unwrap_or("Local File".into());
|
|
||||||
let desc = item.desc.unwrap_or("".into());
|
|
||||||
PrfItem::from_local(name, desc, file_data)
|
|
||||||
}
|
|
||||||
"merge" => {
|
|
||||||
let name = item.name.unwrap_or("Merge".into());
|
|
||||||
let desc = item.desc.unwrap_or("".into());
|
|
||||||
PrfItem::from_merge(name, desc)
|
|
||||||
}
|
|
||||||
"script" => {
|
|
||||||
let name = item.name.unwrap_or("Script".into());
|
|
||||||
let desc = item.desc.unwrap_or("".into());
|
|
||||||
PrfItem::from_script(name, desc)
|
|
||||||
}
|
|
||||||
typ @ _ => bail!("invalid type \"{typ}\""),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ## Local type
|
|
||||||
/// create a new item from name/desc
|
|
||||||
pub fn from_local(name: String, desc: String, file_data: Option<String>) -> Result<PrfItem> {
|
|
||||||
let uid = help::get_uid("l");
|
|
||||||
let file = format!("{uid}.yaml");
|
|
||||||
|
|
||||||
Ok(PrfItem {
|
|
||||||
uid: Some(uid),
|
|
||||||
itype: Some("local".into()),
|
|
||||||
name: Some(name),
|
|
||||||
desc: Some(desc),
|
|
||||||
file: Some(file),
|
|
||||||
url: None,
|
|
||||||
selected: None,
|
|
||||||
extra: None,
|
|
||||||
option: None,
|
|
||||||
updated: Some(help::get_now()),
|
|
||||||
file_data: Some(file_data.unwrap_or(tmpl::ITEM_LOCAL.into())),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ## Remote type
|
|
||||||
/// create a new item from url
|
|
||||||
pub async fn from_url(
|
|
||||||
url: &str,
|
|
||||||
name: Option<String>,
|
|
||||||
desc: Option<String>,
|
|
||||||
option: Option<PrfOption>,
|
|
||||||
) -> Result<PrfItem> {
|
|
||||||
let with_proxy = match option.as_ref() {
|
|
||||||
Some(opt) => opt.with_proxy.unwrap_or(false),
|
|
||||||
None => false,
|
|
||||||
};
|
|
||||||
let user_agent = match option.as_ref() {
|
|
||||||
Some(opt) => opt.user_agent.clone(),
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut builder = reqwest::ClientBuilder::new();
|
|
||||||
|
|
||||||
if !with_proxy {
|
|
||||||
builder = builder.no_proxy();
|
|
||||||
}
|
|
||||||
if let Some(user_agent) = user_agent {
|
|
||||||
builder = builder.user_agent(user_agent);
|
|
||||||
}
|
|
||||||
|
|
||||||
let resp = builder.build()?.get(url).send().await?;
|
|
||||||
let header = resp.headers();
|
|
||||||
|
|
||||||
// parse the Subscription Userinfo
|
|
||||||
let extra = match header.get("Subscription-Userinfo") {
|
|
||||||
Some(value) => {
|
|
||||||
let sub_info = value.to_str().unwrap_or("");
|
|
||||||
|
|
||||||
Some(PrfExtra {
|
|
||||||
upload: help::parse_str(sub_info, "upload=").unwrap_or(0),
|
|
||||||
download: help::parse_str(sub_info, "download=").unwrap_or(0),
|
|
||||||
total: help::parse_str(sub_info, "total=").unwrap_or(0),
|
|
||||||
expire: help::parse_str(sub_info, "expire=").unwrap_or(0),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let uid = help::get_uid("r");
|
|
||||||
let file = format!("{uid}.yaml");
|
|
||||||
let name = name.unwrap_or(uid.clone());
|
|
||||||
let data = resp.text_with_charset("utf-8").await?;
|
|
||||||
|
|
||||||
Ok(PrfItem {
|
|
||||||
uid: Some(uid),
|
|
||||||
itype: Some("remote".into()),
|
|
||||||
name: Some(name),
|
|
||||||
desc,
|
|
||||||
file: Some(file),
|
|
||||||
url: Some(url.into()),
|
|
||||||
selected: None,
|
|
||||||
extra,
|
|
||||||
option,
|
|
||||||
updated: Some(help::get_now()),
|
|
||||||
file_data: Some(data),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ## Merge type (enhance)
|
|
||||||
/// create the enhanced item by using `merge` rule
|
|
||||||
pub fn from_merge(name: String, desc: String) -> Result<PrfItem> {
|
|
||||||
let uid = help::get_uid("m");
|
|
||||||
let file = format!("{uid}.yaml");
|
|
||||||
|
|
||||||
Ok(PrfItem {
|
|
||||||
uid: Some(uid),
|
|
||||||
itype: Some("merge".into()),
|
|
||||||
name: Some(name),
|
|
||||||
desc: Some(desc),
|
|
||||||
file: Some(file),
|
|
||||||
url: None,
|
|
||||||
selected: None,
|
|
||||||
extra: None,
|
|
||||||
option: None,
|
|
||||||
updated: Some(help::get_now()),
|
|
||||||
file_data: Some(tmpl::ITEM_MERGE.into()),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ## Script type (enhance)
|
|
||||||
/// create the enhanced item by using javascript(browserjs)
|
|
||||||
pub fn from_script(name: String, desc: String) -> Result<PrfItem> {
|
|
||||||
let uid = help::get_uid("s");
|
|
||||||
let file = format!("{uid}.js"); // js ext
|
|
||||||
|
|
||||||
Ok(PrfItem {
|
|
||||||
uid: Some(uid),
|
|
||||||
itype: Some("script".into()),
|
|
||||||
name: Some(name),
|
|
||||||
desc: Some(desc),
|
|
||||||
file: Some(file),
|
|
||||||
url: None,
|
|
||||||
selected: None,
|
|
||||||
extra: None,
|
|
||||||
option: None,
|
|
||||||
updated: Some(help::get_now()),
|
|
||||||
file_data: Some(tmpl::ITEM_SCRIPT.into()),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// get the file data
|
|
||||||
pub fn read_file(&self) -> Result<String> {
|
|
||||||
if self.file.is_none() {
|
|
||||||
bail!("could not find the file");
|
|
||||||
}
|
|
||||||
|
|
||||||
let file = self.file.clone().unwrap();
|
|
||||||
let path = dirs::app_profiles_dir().join(file);
|
|
||||||
fs::read_to_string(path).context("failed to read the file")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// save the file data
|
|
||||||
pub fn save_file(&self, data: String) -> Result<()> {
|
|
||||||
if self.file.is_none() {
|
|
||||||
bail!("could not find the file");
|
|
||||||
}
|
|
||||||
|
|
||||||
let file = self.file.clone().unwrap();
|
|
||||||
let path = dirs::app_profiles_dir().join(file);
|
|
||||||
fs::write(path, data.as_bytes()).context("failed to save the file")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
///
|
|
||||||
/// ## Profiles Config
|
|
||||||
///
|
|
||||||
/// Define the `profiles.yaml` schema
|
|
||||||
///
|
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
|
||||||
pub struct Profiles {
|
|
||||||
/// same as PrfConfig.current
|
|
||||||
current: Option<String>,
|
|
||||||
|
|
||||||
/// same as PrfConfig.chain
|
|
||||||
chain: Option<Vec<String>>,
|
|
||||||
|
|
||||||
/// profile list
|
|
||||||
items: Option<Vec<PrfItem>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! patch {
|
|
||||||
($lv: expr, $rv: expr, $key: tt) => {
|
|
||||||
if ($rv.$key).is_some() {
|
|
||||||
$lv.$key = $rv.$key;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Profiles {
|
|
||||||
/// read the config from the file
|
|
||||||
pub fn read_file() -> Self {
|
|
||||||
let mut profiles = config::read_yaml::<Self>(dirs::profiles_path());
|
|
||||||
|
|
||||||
if profiles.items.is_none() {
|
|
||||||
profiles.items = Some(vec![]);
|
|
||||||
}
|
|
||||||
|
|
||||||
profiles.items.as_mut().map(|items| {
|
|
||||||
for mut item in items.iter_mut() {
|
|
||||||
if item.uid.is_none() {
|
|
||||||
item.uid = Some(help::get_uid("d"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
profiles
|
|
||||||
}
|
|
||||||
|
|
||||||
/// save the config to the file
|
|
||||||
pub fn save_file(&self) -> Result<()> {
|
|
||||||
config::save_yaml(
|
|
||||||
dirs::profiles_path(),
|
|
||||||
self,
|
|
||||||
Some("# Profiles Config for Clash Verge\n\n"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// sync the config between file and memory
|
|
||||||
pub fn sync_file(&mut self) -> Result<()> {
|
|
||||||
let data = Self::read_file();
|
|
||||||
if data.current.is_none() && data.items.is_none() {
|
|
||||||
bail!("failed to read profiles.yaml");
|
|
||||||
}
|
|
||||||
|
|
||||||
self.current = data.current;
|
|
||||||
self.chain = data.chain;
|
|
||||||
self.items = data.items;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// get the current uid
|
|
||||||
pub fn get_current(&self) -> Option<String> {
|
|
||||||
self.current.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// only change the main to the target id
|
|
||||||
pub fn put_current(&mut self, uid: String) -> Result<()> {
|
|
||||||
if self.items.is_none() {
|
|
||||||
self.items = Some(vec![]);
|
|
||||||
}
|
|
||||||
|
|
||||||
let items = self.items.as_ref().unwrap();
|
|
||||||
let some_uid = Some(uid.clone());
|
|
||||||
|
|
||||||
for each in items.iter() {
|
|
||||||
if each.uid == some_uid {
|
|
||||||
self.current = some_uid;
|
|
||||||
return self.save_file();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bail!("invalid uid \"{uid}\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// just change the `chain`
|
|
||||||
pub fn put_chain(&mut self, chain: Option<Vec<String>>) {
|
|
||||||
self.chain = chain;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// find the item by the uid
|
|
||||||
pub fn get_item(&self, uid: &String) -> Result<&PrfItem> {
|
|
||||||
if self.items.is_some() {
|
|
||||||
let items = self.items.as_ref().unwrap();
|
|
||||||
let some_uid = Some(uid.clone());
|
|
||||||
|
|
||||||
for each in items.iter() {
|
|
||||||
if each.uid == some_uid {
|
|
||||||
return Ok(each);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bail!("failed to get the item by \"{}\"", uid);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// append new item
|
|
||||||
/// if the file_data is some
|
|
||||||
/// then should save the data to file
|
|
||||||
pub fn append_item(&mut self, mut item: PrfItem) -> Result<()> {
|
|
||||||
if item.uid.is_none() {
|
|
||||||
bail!("the uid should not be null");
|
|
||||||
}
|
|
||||||
|
|
||||||
// save the file data
|
|
||||||
// move the field value after save
|
|
||||||
if let Some(file_data) = item.file_data.take() {
|
|
||||||
if item.file.is_none() {
|
|
||||||
bail!("the file should not be null");
|
|
||||||
}
|
|
||||||
|
|
||||||
let file = item.file.clone().unwrap();
|
|
||||||
let path = dirs::app_profiles_dir().join(&file);
|
|
||||||
|
|
||||||
fs::File::create(path)
|
|
||||||
.context(format!("failed to create file \"{}\"", file))?
|
|
||||||
.write(file_data.as_bytes())
|
|
||||||
.context(format!("failed to write to file \"{}\"", file))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.items.is_none() {
|
|
||||||
self.items = Some(vec![]);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.items.as_mut().map(|items| items.push(item));
|
|
||||||
self.save_file()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// update the item's value
|
|
||||||
pub fn patch_item(&mut self, uid: String, item: PrfItem) -> Result<()> {
|
|
||||||
let mut items = self.items.take().unwrap_or(vec![]);
|
|
||||||
|
|
||||||
for mut each in items.iter_mut() {
|
|
||||||
if each.uid == Some(uid.clone()) {
|
|
||||||
patch!(each, item, itype);
|
|
||||||
patch!(each, item, name);
|
|
||||||
patch!(each, item, desc);
|
|
||||||
patch!(each, item, file);
|
|
||||||
patch!(each, item, url);
|
|
||||||
patch!(each, item, selected);
|
|
||||||
patch!(each, item, extra);
|
|
||||||
patch!(each, item, updated);
|
|
||||||
patch!(each, item, option);
|
|
||||||
|
|
||||||
self.items = Some(items);
|
|
||||||
return self.save_file();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.items = Some(items);
|
|
||||||
bail!("failed to found the uid \"{uid}\"")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// be used to update the remote item
|
|
||||||
/// only patch `updated` `extra` `file_data`
|
|
||||||
pub fn update_item(&mut self, uid: String, mut item: PrfItem) -> Result<()> {
|
|
||||||
if self.items.is_none() {
|
|
||||||
self.items = Some(vec![]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// find the item
|
|
||||||
let _ = self.get_item(&uid)?;
|
|
||||||
|
|
||||||
self.items.as_mut().map(|items| {
|
|
||||||
let some_uid = Some(uid.clone());
|
|
||||||
|
|
||||||
for mut each in items.iter_mut() {
|
|
||||||
if each.uid == some_uid {
|
|
||||||
each.extra = item.extra;
|
|
||||||
each.updated = item.updated;
|
|
||||||
|
|
||||||
// save the file data
|
|
||||||
// move the field value after save
|
|
||||||
if let Some(file_data) = item.file_data.take() {
|
|
||||||
let file = each.file.take();
|
|
||||||
let file = file.unwrap_or(item.file.take().unwrap_or(format!("{}.yaml", &uid)));
|
|
||||||
|
|
||||||
// the file must exists
|
|
||||||
each.file = Some(file.clone());
|
|
||||||
|
|
||||||
let path = dirs::app_profiles_dir().join(&file);
|
|
||||||
|
|
||||||
fs::File::create(path)
|
|
||||||
.unwrap()
|
|
||||||
.write(file_data.as_bytes())
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
self.save_file()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// delete item
|
|
||||||
/// if delete the current then return true
|
|
||||||
pub fn delete_item(&mut self, uid: String) -> Result<bool> {
|
|
||||||
let current = self.current.as_ref().unwrap_or(&uid);
|
|
||||||
let current = current.clone();
|
|
||||||
|
|
||||||
let mut items = self.items.take().unwrap_or(vec![]);
|
|
||||||
let mut index = None;
|
|
||||||
|
|
||||||
// get the index
|
|
||||||
for i in 0..items.len() {
|
|
||||||
if items[i].uid == Some(uid.clone()) {
|
|
||||||
index = Some(i);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(index) = index {
|
|
||||||
items.remove(index).file.map(|file| {
|
|
||||||
let path = dirs::app_profiles_dir().join(file);
|
|
||||||
if path.exists() {
|
|
||||||
let _ = fs::remove_file(path);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// delete the original uid
|
|
||||||
if current == uid {
|
|
||||||
self.current = match items.len() > 0 {
|
|
||||||
true => items[0].uid.clone(),
|
|
||||||
false => None,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
self.items = Some(items);
|
|
||||||
self.save_file()?;
|
|
||||||
Ok(current == uid)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// only generate config mapping
|
|
||||||
pub fn gen_activate(&self) -> Result<Mapping> {
|
|
||||||
let config = Mapping::new();
|
|
||||||
|
|
||||||
if self.current.is_none() || self.items.is_none() {
|
|
||||||
return Ok(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
let current = self.current.clone().unwrap();
|
|
||||||
|
|
||||||
for item in self.items.as_ref().unwrap().iter() {
|
|
||||||
if item.uid == Some(current.clone()) {
|
|
||||||
let file_path = match item.file.clone() {
|
|
||||||
Some(file) => dirs::app_profiles_dir().join(file),
|
|
||||||
None => bail!("failed to get the file field"),
|
|
||||||
};
|
|
||||||
|
|
||||||
if !file_path.exists() {
|
|
||||||
bail!("failed to read the file \"{}\"", file_path.display());
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(config::read_yaml::<Mapping>(file_path.clone()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bail!("failed to found the uid \"{current}\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// gen the enhanced profiles
|
|
||||||
pub fn gen_enhanced(&self, callback: String) -> Result<PrfEnhanced> {
|
|
||||||
let current = self.gen_activate()?;
|
|
||||||
|
|
||||||
let chain = match self.chain.as_ref() {
|
|
||||||
Some(chain) => chain
|
|
||||||
.iter()
|
|
||||||
.map(|uid| self.get_item(uid))
|
|
||||||
.filter(|item| item.is_ok())
|
|
||||||
.map(|item| item.unwrap())
|
|
||||||
.map(|item| PrfData::from_item(item))
|
|
||||||
.filter(|o| o.is_some())
|
|
||||||
.map(|o| o.unwrap())
|
|
||||||
.collect::<Vec<PrfData>>(),
|
|
||||||
None => vec![],
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(PrfEnhanced {
|
|
||||||
current,
|
|
||||||
chain,
|
|
||||||
callback,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct PrfEnhanced {
|
|
||||||
pub current: Mapping,
|
|
||||||
|
|
||||||
pub chain: Vec<PrfData>,
|
|
||||||
|
|
||||||
pub callback: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct PrfEnhancedResult {
|
|
||||||
pub data: Option<Mapping>,
|
|
||||||
|
|
||||||
pub status: String,
|
|
||||||
|
|
||||||
pub error: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct PrfData {
|
|
||||||
item: PrfItem,
|
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
merge: Option<Mapping>,
|
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
script: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PrfData {
|
|
||||||
pub fn from_item(item: &PrfItem) -> Option<PrfData> {
|
|
||||||
match item.itype.as_ref() {
|
|
||||||
Some(itype) => {
|
|
||||||
let file = item.file.clone()?;
|
|
||||||
let path = dirs::app_profiles_dir().join(file);
|
|
||||||
|
|
||||||
if !path.exists() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
match itype.as_str() {
|
|
||||||
"script" => Some(PrfData {
|
|
||||||
item: item.clone(),
|
|
||||||
script: Some(fs::read_to_string(path).unwrap_or("".into())),
|
|
||||||
merge: None,
|
|
||||||
}),
|
|
||||||
"merge" => Some(PrfData {
|
|
||||||
item: item.clone(),
|
|
||||||
merge: Some(config::read_yaml::<Mapping>(path)),
|
|
||||||
script: None,
|
|
||||||
}),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
299
src-tauri/src/core/service.rs
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
use crate::config::Config;
|
||||||
|
use crate::utils::dirs;
|
||||||
|
use anyhow::{bail, Context, Result};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::time::Duration;
|
||||||
|
use std::{env::current_exe, process::Command as StdCommand};
|
||||||
|
use tokio::time::sleep;
|
||||||
|
|
||||||
|
// Windows only
|
||||||
|
|
||||||
|
const SERVICE_URL: &str = "http://127.0.0.1:33211";
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
|
pub struct ResponseBody {
|
||||||
|
pub core_type: Option<String>,
|
||||||
|
pub bin_path: String,
|
||||||
|
pub config_dir: String,
|
||||||
|
pub log_file: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
|
pub struct JsonResponse {
|
||||||
|
pub code: u64,
|
||||||
|
pub msg: String,
|
||||||
|
pub data: Option<ResponseBody>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Install the Clash Verge Service
|
||||||
|
/// 该函数应该在协程或者线程中执行,避免UAC弹窗阻塞主线程
|
||||||
|
///
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub async fn install_service() -> Result<()> {
|
||||||
|
use deelevate::{PrivilegeLevel, Token};
|
||||||
|
use runas::Command as RunasCommand;
|
||||||
|
use std::os::windows::process::CommandExt;
|
||||||
|
|
||||||
|
let binary_path = dirs::service_path()?;
|
||||||
|
let install_path = binary_path.with_file_name("install-service.exe");
|
||||||
|
|
||||||
|
if !install_path.exists() {
|
||||||
|
bail!("installer exe not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
let token = Token::with_current_process()?;
|
||||||
|
let level = token.privilege_level()?;
|
||||||
|
|
||||||
|
let status = match level {
|
||||||
|
PrivilegeLevel::NotPrivileged => RunasCommand::new(install_path).show(false).status()?,
|
||||||
|
_ => StdCommand::new(install_path)
|
||||||
|
.creation_flags(0x08000000)
|
||||||
|
.status()?,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !status.success() {
|
||||||
|
bail!(
|
||||||
|
"failed to install service with status {}",
|
||||||
|
status.code().unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub async fn install_service() -> Result<()> {
|
||||||
|
use users::get_effective_uid;
|
||||||
|
|
||||||
|
let binary_path = dirs::service_path()?;
|
||||||
|
let installer_path = binary_path.with_file_name("install-service");
|
||||||
|
|
||||||
|
if !installer_path.exists() {
|
||||||
|
bail!("installer not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
let elevator = crate::utils::unix_helper::linux_elevator();
|
||||||
|
let status = match get_effective_uid() {
|
||||||
|
0 => StdCommand::new(installer_path).status()?,
|
||||||
|
_ => StdCommand::new(elevator)
|
||||||
|
.arg("sh")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(installer_path)
|
||||||
|
.status()?,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !status.success() {
|
||||||
|
bail!(
|
||||||
|
"failed to install service with status {}",
|
||||||
|
status.code().unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
pub async fn install_service() -> Result<()> {
|
||||||
|
let binary_path = dirs::service_path()?;
|
||||||
|
let installer_path = binary_path.with_file_name("install-service");
|
||||||
|
|
||||||
|
if !installer_path.exists() {
|
||||||
|
bail!("installer not found");
|
||||||
|
}
|
||||||
|
let shell = installer_path.to_string_lossy().replace(" ", "\\\\ ");
|
||||||
|
let command = format!(r#"do shell script "{shell}" with administrator privileges"#);
|
||||||
|
|
||||||
|
let status = StdCommand::new("osascript")
|
||||||
|
.args(vec!["-e", &command])
|
||||||
|
.status()?;
|
||||||
|
|
||||||
|
if !status.success() {
|
||||||
|
bail!(
|
||||||
|
"failed to install service with status {}",
|
||||||
|
status.code().unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
/// Uninstall the Clash Verge Service
|
||||||
|
/// 该函数应该在协程或者线程中执行,避免UAC弹窗阻塞主线程
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub async fn uninstall_service() -> Result<()> {
|
||||||
|
use deelevate::{PrivilegeLevel, Token};
|
||||||
|
use runas::Command as RunasCommand;
|
||||||
|
use std::os::windows::process::CommandExt;
|
||||||
|
|
||||||
|
let binary_path = dirs::service_path()?;
|
||||||
|
let uninstall_path = binary_path.with_file_name("uninstall-service.exe");
|
||||||
|
|
||||||
|
if !uninstall_path.exists() {
|
||||||
|
bail!("uninstaller exe not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
let token = Token::with_current_process()?;
|
||||||
|
let level = token.privilege_level()?;
|
||||||
|
|
||||||
|
let status = match level {
|
||||||
|
PrivilegeLevel::NotPrivileged => RunasCommand::new(uninstall_path).show(false).status()?,
|
||||||
|
_ => StdCommand::new(uninstall_path)
|
||||||
|
.creation_flags(0x08000000)
|
||||||
|
.status()?,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !status.success() {
|
||||||
|
bail!(
|
||||||
|
"failed to uninstall service with status {}",
|
||||||
|
status.code().unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub async fn uninstall_service() -> Result<()> {
|
||||||
|
use users::get_effective_uid;
|
||||||
|
|
||||||
|
let binary_path = dirs::service_path()?;
|
||||||
|
let uninstaller_path = binary_path.with_file_name("uninstall-service");
|
||||||
|
|
||||||
|
if !uninstaller_path.exists() {
|
||||||
|
bail!("uninstaller not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
let elevator = crate::utils::unix_helper::linux_elevator();
|
||||||
|
let status = match get_effective_uid() {
|
||||||
|
0 => StdCommand::new(uninstaller_path).status()?,
|
||||||
|
_ => StdCommand::new(elevator)
|
||||||
|
.arg("sh")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(uninstaller_path)
|
||||||
|
.status()?,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !status.success() {
|
||||||
|
bail!(
|
||||||
|
"failed to install service with status {}",
|
||||||
|
status.code().unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
pub async fn uninstall_service() -> Result<()> {
|
||||||
|
let binary_path = dirs::service_path()?;
|
||||||
|
let uninstaller_path = binary_path.with_file_name("uninstall-service");
|
||||||
|
|
||||||
|
if !uninstaller_path.exists() {
|
||||||
|
bail!("uninstaller not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
let shell = uninstaller_path.to_string_lossy().replace(" ", "\\\\ ");
|
||||||
|
let command = format!(r#"do shell script "{shell}" with administrator privileges"#);
|
||||||
|
|
||||||
|
let status = StdCommand::new("osascript")
|
||||||
|
.args(vec!["-e", &command])
|
||||||
|
.status()?;
|
||||||
|
|
||||||
|
if !status.success() {
|
||||||
|
bail!(
|
||||||
|
"failed to install service with status {}",
|
||||||
|
status.code().unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// check the windows service status
|
||||||
|
pub async fn check_service() -> Result<JsonResponse> {
|
||||||
|
let url = format!("{SERVICE_URL}/get_clash");
|
||||||
|
let response = reqwest::ClientBuilder::new()
|
||||||
|
.no_proxy()
|
||||||
|
.build()?
|
||||||
|
.get(url)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.context("failed to connect to the Clash Verge Service")?
|
||||||
|
.json::<JsonResponse>()
|
||||||
|
.await
|
||||||
|
.context("failed to parse the Clash Verge Service response")?;
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// start the clash by service
|
||||||
|
pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
||||||
|
let status = check_service().await?;
|
||||||
|
|
||||||
|
if status.code == 0 {
|
||||||
|
stop_core_by_service().await?;
|
||||||
|
sleep(Duration::from_secs(1)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
let clash_core = { Config::verge().latest().clash_core.clone() };
|
||||||
|
let clash_core = clash_core.unwrap_or("clash".into());
|
||||||
|
|
||||||
|
let bin_ext = if cfg!(windows) { ".exe" } else { "" };
|
||||||
|
let clash_bin = format!("{clash_core}{bin_ext}");
|
||||||
|
let bin_path = current_exe()?.with_file_name(clash_bin);
|
||||||
|
let bin_path = dirs::path_to_str(&bin_path)?;
|
||||||
|
|
||||||
|
let config_dir = dirs::app_home_dir()?;
|
||||||
|
let config_dir = dirs::path_to_str(&config_dir)?;
|
||||||
|
|
||||||
|
let log_path = dirs::service_log_file()?;
|
||||||
|
let log_path = dirs::path_to_str(&log_path)?;
|
||||||
|
|
||||||
|
let config_file = dirs::path_to_str(config_file)?;
|
||||||
|
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
map.insert("core_type", clash_core.as_str());
|
||||||
|
map.insert("bin_path", bin_path);
|
||||||
|
map.insert("config_dir", config_dir);
|
||||||
|
map.insert("config_file", config_file);
|
||||||
|
map.insert("log_file", log_path);
|
||||||
|
|
||||||
|
let url = format!("{SERVICE_URL}/start_clash");
|
||||||
|
let res = reqwest::ClientBuilder::new()
|
||||||
|
.no_proxy()
|
||||||
|
.build()?
|
||||||
|
.post(url)
|
||||||
|
.json(&map)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.json::<JsonResponse>()
|
||||||
|
.await
|
||||||
|
.context("failed to connect to the Clash Verge Service")?;
|
||||||
|
|
||||||
|
if res.code != 0 {
|
||||||
|
bail!(res.msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// stop the clash by service
|
||||||
|
pub(super) async fn stop_core_by_service() -> Result<()> {
|
||||||
|
let url = format!("{SERVICE_URL}/stop_clash");
|
||||||
|
let res = reqwest::ClientBuilder::new()
|
||||||
|
.no_proxy()
|
||||||
|
.build()?
|
||||||
|
.post(url)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.json::<JsonResponse>()
|
||||||
|
.await
|
||||||
|
.context("failed to connect to the Clash Verge Service")?;
|
||||||
|
|
||||||
|
if res.code != 0 {
|
||||||
|
bail!(res.msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
295
src-tauri/src/core/sysopt.rs
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
use crate::{config::Config, log_err};
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use auto_launch::{AutoLaunch, AutoLaunchBuilder};
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use std::env::current_exe;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use sysproxy::Sysproxy;
|
||||||
|
use tauri::async_runtime::Mutex as TokioMutex;
|
||||||
|
|
||||||
|
pub struct Sysopt {
|
||||||
|
/// current system proxy setting
|
||||||
|
cur_sysproxy: Arc<Mutex<Option<Sysproxy>>>,
|
||||||
|
|
||||||
|
/// record the original system proxy
|
||||||
|
/// recover it when exit
|
||||||
|
old_sysproxy: Arc<Mutex<Option<Sysproxy>>>,
|
||||||
|
|
||||||
|
/// helps to auto launch the app
|
||||||
|
auto_launch: Arc<Mutex<Option<AutoLaunch>>>,
|
||||||
|
|
||||||
|
/// record whether the guard async is running or not
|
||||||
|
guard_state: Arc<TokioMutex<bool>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
static DEFAULT_BYPASS: &str = "localhost;127.*;192.168.*;10.*;172.16.*;<local>";
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
static DEFAULT_BYPASS: &str = "localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,::1";
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
static DEFAULT_BYPASS: &str =
|
||||||
|
"127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,localhost,*.local,*.crashlytics.com,<local>";
|
||||||
|
|
||||||
|
impl Sysopt {
|
||||||
|
pub fn global() -> &'static Sysopt {
|
||||||
|
static SYSOPT: OnceCell<Sysopt> = OnceCell::new();
|
||||||
|
|
||||||
|
SYSOPT.get_or_init(|| Sysopt {
|
||||||
|
cur_sysproxy: Arc::new(Mutex::new(None)),
|
||||||
|
old_sysproxy: Arc::new(Mutex::new(None)),
|
||||||
|
auto_launch: Arc::new(Mutex::new(None)),
|
||||||
|
guard_state: Arc::new(TokioMutex::new(false)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// init the sysproxy
|
||||||
|
pub fn init_sysproxy(&self) -> Result<()> {
|
||||||
|
let port = Config::verge()
|
||||||
|
.latest()
|
||||||
|
.verge_mixed_port
|
||||||
|
.unwrap_or(Config::clash().data().get_mixed_port());
|
||||||
|
|
||||||
|
let (enable, bypass) = {
|
||||||
|
let verge = Config::verge();
|
||||||
|
let verge = verge.latest();
|
||||||
|
(
|
||||||
|
verge.enable_system_proxy.unwrap_or(false),
|
||||||
|
verge.system_proxy_bypass.clone(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let current = Sysproxy {
|
||||||
|
enable,
|
||||||
|
host: String::from("127.0.0.1"),
|
||||||
|
port,
|
||||||
|
bypass: bypass.unwrap_or(DEFAULT_BYPASS.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if enable {
|
||||||
|
let old = Sysproxy::get_system_proxy().ok();
|
||||||
|
current.set_system_proxy()?;
|
||||||
|
|
||||||
|
*self.old_sysproxy.lock() = old;
|
||||||
|
*self.cur_sysproxy.lock() = Some(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// run the system proxy guard
|
||||||
|
self.guard_proxy();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// update the system proxy
|
||||||
|
pub fn update_sysproxy(&self) -> Result<()> {
|
||||||
|
let mut cur_sysproxy = self.cur_sysproxy.lock();
|
||||||
|
let old_sysproxy = self.old_sysproxy.lock();
|
||||||
|
|
||||||
|
if cur_sysproxy.is_none() || old_sysproxy.is_none() {
|
||||||
|
drop(cur_sysproxy);
|
||||||
|
drop(old_sysproxy);
|
||||||
|
return self.init_sysproxy();
|
||||||
|
}
|
||||||
|
|
||||||
|
let (enable, bypass) = {
|
||||||
|
let verge = Config::verge();
|
||||||
|
let verge = verge.latest();
|
||||||
|
(
|
||||||
|
verge.enable_system_proxy.unwrap_or(false),
|
||||||
|
verge.system_proxy_bypass.clone(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let mut sysproxy = cur_sysproxy.take().unwrap();
|
||||||
|
|
||||||
|
sysproxy.enable = enable;
|
||||||
|
sysproxy.bypass = bypass.unwrap_or(DEFAULT_BYPASS.into());
|
||||||
|
|
||||||
|
let port = Config::verge()
|
||||||
|
.latest()
|
||||||
|
.verge_mixed_port
|
||||||
|
.unwrap_or(Config::clash().data().get_mixed_port());
|
||||||
|
sysproxy.port = port;
|
||||||
|
|
||||||
|
sysproxy.set_system_proxy()?;
|
||||||
|
*cur_sysproxy = Some(sysproxy);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// reset the sysproxy
|
||||||
|
pub fn reset_sysproxy(&self) -> Result<()> {
|
||||||
|
let mut cur_sysproxy = self.cur_sysproxy.lock();
|
||||||
|
let mut old_sysproxy = self.old_sysproxy.lock();
|
||||||
|
|
||||||
|
let cur_sysproxy = cur_sysproxy.take();
|
||||||
|
|
||||||
|
if let Some(mut old) = old_sysproxy.take() {
|
||||||
|
// 如果原代理和当前代理 端口一致,就disable关闭,否则就恢复原代理设置
|
||||||
|
// 当前没有设置代理的时候,不确定旧设置是否和当前一致,全关了
|
||||||
|
let port_same = cur_sysproxy.map_or(true, |cur| old.port == cur.port);
|
||||||
|
|
||||||
|
if old.enable && port_same {
|
||||||
|
old.enable = false;
|
||||||
|
log::info!(target: "app", "reset proxy by disabling the original proxy");
|
||||||
|
} else {
|
||||||
|
log::info!(target: "app", "reset proxy to the original proxy");
|
||||||
|
}
|
||||||
|
|
||||||
|
old.set_system_proxy()?;
|
||||||
|
} else if let Some(mut cur @ Sysproxy { enable: true, .. }) = cur_sysproxy {
|
||||||
|
// 没有原代理,就按现在的代理设置disable即可
|
||||||
|
log::info!(target: "app", "reset proxy by disabling the current proxy");
|
||||||
|
cur.enable = false;
|
||||||
|
cur.set_system_proxy()?;
|
||||||
|
} else {
|
||||||
|
log::info!(target: "app", "reset proxy with no action");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// init the auto launch
|
||||||
|
pub fn init_launch(&self) -> Result<()> {
|
||||||
|
let app_exe = current_exe()?;
|
||||||
|
// let app_exe = dunce::canonicalize(app_exe)?;
|
||||||
|
let app_name = app_exe
|
||||||
|
.file_stem()
|
||||||
|
.and_then(|f| f.to_str())
|
||||||
|
.ok_or(anyhow!("failed to get file stem"))?;
|
||||||
|
|
||||||
|
let app_path = app_exe
|
||||||
|
.as_os_str()
|
||||||
|
.to_str()
|
||||||
|
.ok_or(anyhow!("failed to get app_path"))?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
// fix issue #26
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
let app_path = format!("\"{app_path}\"");
|
||||||
|
|
||||||
|
// use the /Applications/Clash Verge.app path
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
let app_path = (|| -> Option<String> {
|
||||||
|
let path = std::path::PathBuf::from(&app_path);
|
||||||
|
let path = path.parent()?.parent()?.parent()?;
|
||||||
|
let extension = path.extension()?.to_str()?;
|
||||||
|
match extension == "app" {
|
||||||
|
true => Some(path.as_os_str().to_str()?.to_string()),
|
||||||
|
false => None,
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
.unwrap_or(app_path);
|
||||||
|
|
||||||
|
// fix #403
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
let app_path = {
|
||||||
|
use crate::core::handle::Handle;
|
||||||
|
use tauri::Manager;
|
||||||
|
|
||||||
|
let handle = Handle::global();
|
||||||
|
match handle.app_handle.lock().as_ref() {
|
||||||
|
Some(app_handle) => {
|
||||||
|
let appimage = app_handle.env().appimage;
|
||||||
|
appimage
|
||||||
|
.and_then(|p| p.to_str().map(|s| s.to_string()))
|
||||||
|
.unwrap_or(app_path)
|
||||||
|
}
|
||||||
|
None => app_path,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let auto = AutoLaunchBuilder::new()
|
||||||
|
.set_app_name(app_name)
|
||||||
|
.set_app_path(&app_path)
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
*self.auto_launch.lock() = Some(auto);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// update the startup
|
||||||
|
pub fn update_launch(&self) -> Result<()> {
|
||||||
|
let auto_launch = self.auto_launch.lock();
|
||||||
|
|
||||||
|
if auto_launch.is_none() {
|
||||||
|
drop(auto_launch);
|
||||||
|
return self.init_launch();
|
||||||
|
}
|
||||||
|
let enable = { Config::verge().latest().enable_auto_launch };
|
||||||
|
let enable = enable.unwrap_or(false);
|
||||||
|
let auto_launch = auto_launch.as_ref().unwrap();
|
||||||
|
|
||||||
|
match enable {
|
||||||
|
true => auto_launch.enable()?,
|
||||||
|
false => log_err!(auto_launch.disable()), // 忽略关闭的错误
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// launch a system proxy guard
|
||||||
|
/// read config from file directly
|
||||||
|
pub fn guard_proxy(&self) {
|
||||||
|
use tokio::time::{sleep, Duration};
|
||||||
|
|
||||||
|
let guard_state = self.guard_state.clone();
|
||||||
|
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
// if it is running, exit
|
||||||
|
let mut state = guard_state.lock().await;
|
||||||
|
if *state {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
*state = true;
|
||||||
|
drop(state);
|
||||||
|
|
||||||
|
// default duration is 10s
|
||||||
|
let mut wait_secs = 10u64;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
sleep(Duration::from_secs(wait_secs)).await;
|
||||||
|
|
||||||
|
let (enable, guard, guard_duration, bypass) = {
|
||||||
|
let verge = Config::verge();
|
||||||
|
let verge = verge.latest();
|
||||||
|
(
|
||||||
|
verge.enable_system_proxy.unwrap_or(false),
|
||||||
|
verge.enable_proxy_guard.unwrap_or(false),
|
||||||
|
verge.proxy_guard_duration.unwrap_or(10),
|
||||||
|
verge.system_proxy_bypass.clone(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
// stop loop
|
||||||
|
if !enable || !guard {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// update duration
|
||||||
|
wait_secs = guard_duration;
|
||||||
|
|
||||||
|
log::debug!(target: "app", "try to guard the system proxy");
|
||||||
|
|
||||||
|
let port = {
|
||||||
|
Config::verge()
|
||||||
|
.latest()
|
||||||
|
.verge_mixed_port
|
||||||
|
.unwrap_or(Config::clash().data().get_mixed_port())
|
||||||
|
};
|
||||||
|
|
||||||
|
let sysproxy = Sysproxy {
|
||||||
|
enable: true,
|
||||||
|
host: "127.0.0.1".into(),
|
||||||
|
port,
|
||||||
|
bypass: bypass.unwrap_or(DEFAULT_BYPASS.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
log_err!(sysproxy.set_system_proxy());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut state = guard_state.lock().await;
|
||||||
|
*state = false;
|
||||||
|
drop(state);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
184
src-tauri/src/core/timer.rs
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
use crate::config::Config;
|
||||||
|
use crate::feat;
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use delay_timer::prelude::{DelayTimer, DelayTimerBuilder, TaskBuilder};
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
type TaskID = u64;
|
||||||
|
|
||||||
|
pub struct Timer {
|
||||||
|
/// cron manager
|
||||||
|
delay_timer: Arc<Mutex<DelayTimer>>,
|
||||||
|
|
||||||
|
/// save the current state
|
||||||
|
timer_map: Arc<Mutex<HashMap<String, (TaskID, u64)>>>,
|
||||||
|
|
||||||
|
/// increment id
|
||||||
|
timer_count: Arc<Mutex<TaskID>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Timer {
|
||||||
|
pub fn global() -> &'static Timer {
|
||||||
|
static TIMER: OnceCell<Timer> = OnceCell::new();
|
||||||
|
|
||||||
|
TIMER.get_or_init(|| Timer {
|
||||||
|
delay_timer: Arc::new(Mutex::new(DelayTimerBuilder::default().build())),
|
||||||
|
timer_map: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
timer_count: Arc::new(Mutex::new(1)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// restore timer
|
||||||
|
pub fn init(&self) -> Result<()> {
|
||||||
|
self.refresh()?;
|
||||||
|
|
||||||
|
let cur_timestamp = chrono::Local::now().timestamp();
|
||||||
|
|
||||||
|
let timer_map = self.timer_map.lock();
|
||||||
|
let delay_timer = self.delay_timer.lock();
|
||||||
|
|
||||||
|
if let Some(items) = Config::profiles().latest().get_items() {
|
||||||
|
items
|
||||||
|
.iter()
|
||||||
|
.filter_map(|item| {
|
||||||
|
// mins to seconds
|
||||||
|
let interval = ((item.option.as_ref()?.update_interval?) as i64) * 60;
|
||||||
|
let updated = item.updated? as i64;
|
||||||
|
|
||||||
|
if interval > 0 && cur_timestamp - updated >= interval {
|
||||||
|
Some(item)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.for_each(|item| {
|
||||||
|
if let Some(uid) = item.uid.as_ref() {
|
||||||
|
if let Some((task_id, _)) = timer_map.get(uid) {
|
||||||
|
crate::log_err!(delay_timer.advance_task(*task_id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Correctly update all cron tasks
|
||||||
|
pub fn refresh(&self) -> Result<()> {
|
||||||
|
let diff_map = self.gen_diff();
|
||||||
|
|
||||||
|
let mut timer_map = self.timer_map.lock();
|
||||||
|
let mut delay_timer = self.delay_timer.lock();
|
||||||
|
|
||||||
|
for (uid, diff) in diff_map.into_iter() {
|
||||||
|
match diff {
|
||||||
|
DiffFlag::Del(tid) => {
|
||||||
|
let _ = timer_map.remove(&uid);
|
||||||
|
crate::log_err!(delay_timer.remove_task(tid));
|
||||||
|
}
|
||||||
|
DiffFlag::Add(tid, val) => {
|
||||||
|
let _ = timer_map.insert(uid.clone(), (tid, val));
|
||||||
|
crate::log_err!(self.add_task(&mut delay_timer, uid, tid, val));
|
||||||
|
}
|
||||||
|
DiffFlag::Mod(tid, val) => {
|
||||||
|
let _ = timer_map.insert(uid.clone(), (tid, val));
|
||||||
|
crate::log_err!(delay_timer.remove_task(tid));
|
||||||
|
crate::log_err!(self.add_task(&mut delay_timer, uid, tid, val));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// generate a uid -> update_interval map
|
||||||
|
fn gen_map(&self) -> HashMap<String, u64> {
|
||||||
|
let mut new_map = HashMap::new();
|
||||||
|
|
||||||
|
if let Some(items) = Config::profiles().latest().get_items() {
|
||||||
|
for item in items.iter() {
|
||||||
|
if item.option.is_some() {
|
||||||
|
let option = item.option.as_ref().unwrap();
|
||||||
|
let interval = option.update_interval.unwrap_or(0);
|
||||||
|
|
||||||
|
if interval > 0 {
|
||||||
|
new_map.insert(item.uid.clone().unwrap(), interval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new_map
|
||||||
|
}
|
||||||
|
|
||||||
|
/// generate the diff map for refresh
|
||||||
|
fn gen_diff(&self) -> HashMap<String, DiffFlag> {
|
||||||
|
let mut diff_map = HashMap::new();
|
||||||
|
|
||||||
|
let timer_map = self.timer_map.lock();
|
||||||
|
|
||||||
|
let new_map = self.gen_map();
|
||||||
|
let cur_map = &timer_map;
|
||||||
|
|
||||||
|
cur_map.iter().for_each(|(uid, (tid, val))| {
|
||||||
|
let new_val = new_map.get(uid).unwrap_or(&0);
|
||||||
|
|
||||||
|
if *new_val == 0 {
|
||||||
|
diff_map.insert(uid.clone(), DiffFlag::Del(*tid));
|
||||||
|
} else if new_val != val {
|
||||||
|
diff_map.insert(uid.clone(), DiffFlag::Mod(*tid, *new_val));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut count = self.timer_count.lock();
|
||||||
|
|
||||||
|
new_map.iter().for_each(|(uid, val)| {
|
||||||
|
if cur_map.get(uid).is_none() {
|
||||||
|
diff_map.insert(uid.clone(), DiffFlag::Add(*count, *val));
|
||||||
|
|
||||||
|
*count += 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
diff_map
|
||||||
|
}
|
||||||
|
|
||||||
|
/// add a cron task
|
||||||
|
fn add_task(
|
||||||
|
&self,
|
||||||
|
delay_timer: &mut DelayTimer,
|
||||||
|
uid: String,
|
||||||
|
tid: TaskID,
|
||||||
|
minutes: u64,
|
||||||
|
) -> Result<()> {
|
||||||
|
let task = TaskBuilder::default()
|
||||||
|
.set_task_id(tid)
|
||||||
|
.set_maximum_parallel_runnable_num(1)
|
||||||
|
.set_frequency_repeated_by_minutes(minutes)
|
||||||
|
// .set_frequency_repeated_by_seconds(minutes) // for test
|
||||||
|
.spawn_async_routine(move || Self::async_task(uid.to_owned()))
|
||||||
|
.context("failed to create timer task")?;
|
||||||
|
|
||||||
|
delay_timer
|
||||||
|
.add_task(task)
|
||||||
|
.context("failed to add timer task")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// the task runner
|
||||||
|
async fn async_task(uid: String) {
|
||||||
|
log::info!(target: "app", "running timer task `{uid}`");
|
||||||
|
crate::log_err!(feat::update_profile(uid, None).await);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum DiffFlag {
|
||||||
|
Del(TaskID),
|
||||||
|
Add(TaskID, u64),
|
||||||
|
Mod(TaskID, u64),
|
||||||
|
}
|
||||||
325
src-tauri/src/core/tray.rs
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
use crate::{
|
||||||
|
cmds,
|
||||||
|
config::Config,
|
||||||
|
feat,
|
||||||
|
utils::{dirs, resolve},
|
||||||
|
};
|
||||||
|
use anyhow::Result;
|
||||||
|
use tauri::{
|
||||||
|
api, AppHandle, CustomMenuItem, Manager, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem,
|
||||||
|
SystemTraySubmenu,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct Tray {}
|
||||||
|
|
||||||
|
impl Tray {
|
||||||
|
pub fn tray_menu(app_handle: &AppHandle) -> SystemTrayMenu {
|
||||||
|
let zh = { Config::verge().latest().language == Some("zh".into()) };
|
||||||
|
|
||||||
|
let version = app_handle.package_info().version.to_string();
|
||||||
|
|
||||||
|
macro_rules! t {
|
||||||
|
($en: expr, $zh: expr) => {
|
||||||
|
if zh {
|
||||||
|
$zh
|
||||||
|
} else {
|
||||||
|
$en
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemTrayMenu::new()
|
||||||
|
.add_item(CustomMenuItem::new(
|
||||||
|
"open_window",
|
||||||
|
t!("Dashboard", "打开面板"),
|
||||||
|
))
|
||||||
|
.add_native_item(SystemTrayMenuItem::Separator)
|
||||||
|
.add_item(CustomMenuItem::new(
|
||||||
|
"rule_mode",
|
||||||
|
t!("Rule Mode", "规则模式"),
|
||||||
|
))
|
||||||
|
.add_item(CustomMenuItem::new(
|
||||||
|
"global_mode",
|
||||||
|
t!("Global Mode", "全局模式"),
|
||||||
|
))
|
||||||
|
.add_item(CustomMenuItem::new(
|
||||||
|
"direct_mode",
|
||||||
|
t!("Direct Mode", "直连模式"),
|
||||||
|
))
|
||||||
|
.add_native_item(SystemTrayMenuItem::Separator)
|
||||||
|
.add_item(CustomMenuItem::new(
|
||||||
|
"system_proxy",
|
||||||
|
t!("System Proxy", "系统代理"),
|
||||||
|
))
|
||||||
|
.add_item(CustomMenuItem::new("tun_mode", t!("TUN Mode", "Tun 模式")))
|
||||||
|
.add_item(CustomMenuItem::new(
|
||||||
|
"copy_env",
|
||||||
|
t!("Copy Env", "复制环境变量"),
|
||||||
|
))
|
||||||
|
.add_submenu(SystemTraySubmenu::new(
|
||||||
|
t!("Open Dir", "打开目录"),
|
||||||
|
SystemTrayMenu::new()
|
||||||
|
.add_item(CustomMenuItem::new(
|
||||||
|
"open_app_dir",
|
||||||
|
t!("App Dir", "应用目录"),
|
||||||
|
))
|
||||||
|
.add_item(CustomMenuItem::new(
|
||||||
|
"open_core_dir",
|
||||||
|
t!("Core Dir", "内核目录"),
|
||||||
|
))
|
||||||
|
.add_item(CustomMenuItem::new(
|
||||||
|
"open_logs_dir",
|
||||||
|
t!("Logs Dir", "日志目录"),
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
.add_submenu(SystemTraySubmenu::new(
|
||||||
|
t!("More", "更多"),
|
||||||
|
SystemTrayMenu::new()
|
||||||
|
.add_item(CustomMenuItem::new(
|
||||||
|
"restart_clash",
|
||||||
|
t!("Restart Clash", "重启 Clash"),
|
||||||
|
))
|
||||||
|
.add_item(CustomMenuItem::new(
|
||||||
|
"restart_app",
|
||||||
|
t!("Restart App", "重启应用"),
|
||||||
|
))
|
||||||
|
.add_item(
|
||||||
|
CustomMenuItem::new("app_version", format!("Version {version}")).disabled(),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
.add_native_item(SystemTrayMenuItem::Separator)
|
||||||
|
.add_item(CustomMenuItem::new("quit", t!("Quit", "退出")).accelerator("CmdOrControl+Q"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_systray(app_handle: &AppHandle) -> Result<()> {
|
||||||
|
app_handle
|
||||||
|
.tray_handle()
|
||||||
|
.set_menu(Tray::tray_menu(app_handle))?;
|
||||||
|
Tray::update_part(app_handle)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_part(app_handle: &AppHandle) -> Result<()> {
|
||||||
|
let zh = { Config::verge().latest().language == Some("zh".into()) };
|
||||||
|
|
||||||
|
let version = app_handle.package_info().version.to_string();
|
||||||
|
|
||||||
|
macro_rules! t {
|
||||||
|
($en: expr, $zh: expr) => {
|
||||||
|
if zh {
|
||||||
|
$zh
|
||||||
|
} else {
|
||||||
|
$en
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let mode = {
|
||||||
|
Config::clash()
|
||||||
|
.latest()
|
||||||
|
.0
|
||||||
|
.get("mode")
|
||||||
|
.map(|val| val.as_str().unwrap_or("rule"))
|
||||||
|
.unwrap_or("rule")
|
||||||
|
.to_owned()
|
||||||
|
};
|
||||||
|
|
||||||
|
let tray = app_handle.tray_handle();
|
||||||
|
|
||||||
|
let _ = tray.get_item("rule_mode").set_selected(mode == "rule");
|
||||||
|
let _ = tray.get_item("global_mode").set_selected(mode == "global");
|
||||||
|
let _ = tray.get_item("direct_mode").set_selected(mode == "direct");
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
match mode.as_str() {
|
||||||
|
"rule" => {
|
||||||
|
let _ = tray
|
||||||
|
.get_item("rule_mode")
|
||||||
|
.set_title(t!("Rule Mode ✔", "规则模式 ✔"));
|
||||||
|
let _ = tray
|
||||||
|
.get_item("global_mode")
|
||||||
|
.set_title(t!("Global Mode", "全局模式"));
|
||||||
|
let _ = tray
|
||||||
|
.get_item("direct_mode")
|
||||||
|
.set_title(t!("Direct Mode", "直连模式"));
|
||||||
|
}
|
||||||
|
"global" => {
|
||||||
|
let _ = tray
|
||||||
|
.get_item("rule_mode")
|
||||||
|
.set_title(t!("Rule Mode", "规则模式"));
|
||||||
|
let _ = tray
|
||||||
|
.get_item("global_mode")
|
||||||
|
.set_title(t!("Global Mode ✔", "全局模式 ✔"));
|
||||||
|
let _ = tray
|
||||||
|
.get_item("direct_mode")
|
||||||
|
.set_title(t!("Direct Mode", "直连模式"));
|
||||||
|
}
|
||||||
|
"direct" => {
|
||||||
|
let _ = tray
|
||||||
|
.get_item("rule_mode")
|
||||||
|
.set_title(t!("Rule Mode", "规则模式"));
|
||||||
|
let _ = tray
|
||||||
|
.get_item("global_mode")
|
||||||
|
.set_title(t!("Global Mode", "全局模式"));
|
||||||
|
let _ = tray
|
||||||
|
.get_item("direct_mode")
|
||||||
|
.set_title(t!("Direct Mode ✔", "直连模式 ✔"));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let verge = Config::verge();
|
||||||
|
let verge = verge.latest();
|
||||||
|
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
|
||||||
|
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
|
||||||
|
let common_tray_icon = verge.common_tray_icon.as_ref().unwrap_or(&false);
|
||||||
|
let sysproxy_tray_icon = verge.sysproxy_tray_icon.as_ref().unwrap_or(&false);
|
||||||
|
let tun_tray_icon = verge.tun_tray_icon.as_ref().unwrap_or(&false);
|
||||||
|
|
||||||
|
let mut indication_icon = if *system_proxy {
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
let mut icon = include_bytes!("../../icons/tray-icon-sys.png").to_vec();
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
let mut icon = include_bytes!("../../icons/mac-tray-icon-sys.png").to_vec();
|
||||||
|
if *sysproxy_tray_icon {
|
||||||
|
let icon_dir_path = dirs::app_home_dir()?.join("icons");
|
||||||
|
let png_path = icon_dir_path.join("sysproxy.png");
|
||||||
|
let ico_path = icon_dir_path.join("sysproxy.ico");
|
||||||
|
if ico_path.exists() {
|
||||||
|
icon = std::fs::read(ico_path).unwrap();
|
||||||
|
} else if png_path.exists() {
|
||||||
|
icon = std::fs::read(png_path).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
icon
|
||||||
|
} else {
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
let mut icon = include_bytes!("../../icons/tray-icon.png").to_vec();
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
let mut icon = include_bytes!("../../icons/mac-tray-icon.png").to_vec();
|
||||||
|
if *common_tray_icon {
|
||||||
|
let icon_dir_path = dirs::app_home_dir()?.join("icons");
|
||||||
|
let png_path = icon_dir_path.join("common.png");
|
||||||
|
let ico_path = icon_dir_path.join("common.ico");
|
||||||
|
if ico_path.exists() {
|
||||||
|
icon = std::fs::read(ico_path).unwrap();
|
||||||
|
} else if png_path.exists() {
|
||||||
|
icon = std::fs::read(png_path).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
icon
|
||||||
|
};
|
||||||
|
|
||||||
|
if *tun_mode {
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
let mut icon = include_bytes!("../../icons/tray-icon-tun.png").to_vec();
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
let mut icon = include_bytes!("../../icons/mac-tray-icon-tun.png").to_vec();
|
||||||
|
if *tun_tray_icon {
|
||||||
|
let icon_dir_path = dirs::app_home_dir()?.join("icons");
|
||||||
|
let png_path = icon_dir_path.join("tun.png");
|
||||||
|
let ico_path = icon_dir_path.join("tun.ico");
|
||||||
|
if ico_path.exists() {
|
||||||
|
icon = std::fs::read(ico_path).unwrap();
|
||||||
|
} else if png_path.exists() {
|
||||||
|
icon = std::fs::read(png_path).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
indication_icon = icon
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = tray.set_icon(tauri::Icon::Raw(indication_icon));
|
||||||
|
|
||||||
|
let _ = tray.get_item("system_proxy").set_selected(*system_proxy);
|
||||||
|
let _ = tray.get_item("tun_mode").set_selected(*tun_mode);
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
if *system_proxy {
|
||||||
|
let _ = tray
|
||||||
|
.get_item("system_proxy")
|
||||||
|
.set_title(t!("System Proxy ✔", "系统代理 ✔"));
|
||||||
|
} else {
|
||||||
|
let _ = tray
|
||||||
|
.get_item("system_proxy")
|
||||||
|
.set_title(t!("System Proxy", "系统代理"));
|
||||||
|
}
|
||||||
|
if *tun_mode {
|
||||||
|
let _ = tray
|
||||||
|
.get_item("tun_mode")
|
||||||
|
.set_title(t!("TUN Mode ✔", "Tun 模式 ✔"));
|
||||||
|
} else {
|
||||||
|
let _ = tray
|
||||||
|
.get_item("tun_mode")
|
||||||
|
.set_title(t!("TUN Mode", "Tun 模式"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let switch_map = {
|
||||||
|
let mut map = std::collections::HashMap::new();
|
||||||
|
map.insert(true, "on");
|
||||||
|
map.insert(false, "off");
|
||||||
|
map
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut current_profile_name = "None".to_string();
|
||||||
|
let profiles = Config::profiles();
|
||||||
|
let profiles = profiles.latest();
|
||||||
|
if let Some(current_profile_uid) = profiles.get_current() {
|
||||||
|
let current_profile = profiles.get_item(¤t_profile_uid);
|
||||||
|
current_profile_name = match ¤t_profile.unwrap().name {
|
||||||
|
Some(profile_name) => profile_name.to_string(),
|
||||||
|
None => current_profile_name,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
let _ = tray.set_tooltip(&format!(
|
||||||
|
"Clash Verge {version}\n{}: {}\n{}: {}\n{}: {}",
|
||||||
|
t!("System Proxy", "系统代理"),
|
||||||
|
switch_map[system_proxy],
|
||||||
|
t!("TUN Mode", "Tun 模式"),
|
||||||
|
switch_map[tun_mode],
|
||||||
|
t!("Curent Profile", "当前订阅"),
|
||||||
|
current_profile_name
|
||||||
|
));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_click(app_handle: &AppHandle) {
|
||||||
|
let tray_event = { Config::verge().latest().tray_event.clone() };
|
||||||
|
let tray_event = tray_event.unwrap_or("main_window".into());
|
||||||
|
match tray_event.as_str() {
|
||||||
|
"system_proxy" => feat::toggle_system_proxy(),
|
||||||
|
"tun_mode" => feat::toggle_tun_mode(),
|
||||||
|
"main_window" => resolve::create_window(app_handle),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_system_tray_event(app_handle: &AppHandle, event: SystemTrayEvent) {
|
||||||
|
match event {
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
SystemTrayEvent::LeftClick { .. } => Tray::on_click(app_handle),
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
SystemTrayEvent::RightClick { .. } => Tray::on_click(app_handle),
|
||||||
|
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
|
||||||
|
mode @ ("rule_mode" | "global_mode" | "direct_mode") => {
|
||||||
|
let mode = &mode[0..mode.len() - 5];
|
||||||
|
feat::change_clash_mode(mode.into());
|
||||||
|
}
|
||||||
|
"open_window" => resolve::create_window(app_handle),
|
||||||
|
"system_proxy" => feat::toggle_system_proxy(),
|
||||||
|
"tun_mode" => feat::toggle_tun_mode(),
|
||||||
|
"copy_env" => feat::copy_clash_env(app_handle),
|
||||||
|
"open_app_dir" => crate::log_err!(cmds::open_app_dir()),
|
||||||
|
"open_core_dir" => crate::log_err!(cmds::open_core_dir()),
|
||||||
|
"open_logs_dir" => crate::log_err!(cmds::open_logs_dir()),
|
||||||
|
"restart_clash" => feat::restart_clash_core(),
|
||||||
|
"restart_app" => api::process::restart(&app_handle.env()),
|
||||||
|
"quit" => cmds::exit_app(app_handle.clone()),
|
||||||
|
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,354 +0,0 @@
|
|||||||
use crate::log_if_err;
|
|
||||||
use crate::{
|
|
||||||
core::Clash,
|
|
||||||
utils::{config, dirs, sysopt::SysProxyConfig},
|
|
||||||
};
|
|
||||||
use anyhow::{bail, Result};
|
|
||||||
use auto_launch::{AutoLaunch, AutoLaunchBuilder};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::sync::Arc;
|
|
||||||
use tauri::{async_runtime::Mutex, utils::platform::current_exe};
|
|
||||||
|
|
||||||
/// ### `verge.yaml` schema
|
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
|
||||||
pub struct VergeConfig {
|
|
||||||
// i18n
|
|
||||||
pub language: Option<String>,
|
|
||||||
|
|
||||||
/// `light` or `dark`
|
|
||||||
pub theme_mode: Option<String>,
|
|
||||||
|
|
||||||
/// enable blur mode
|
|
||||||
/// maybe be able to set the alpha
|
|
||||||
pub theme_blur: Option<bool>,
|
|
||||||
|
|
||||||
/// enable traffic graph default is true
|
|
||||||
pub traffic_graph: Option<bool>,
|
|
||||||
|
|
||||||
/// clash tun mode
|
|
||||||
pub enable_tun_mode: Option<bool>,
|
|
||||||
|
|
||||||
/// can the app auto startup
|
|
||||||
pub enable_auto_launch: Option<bool>,
|
|
||||||
|
|
||||||
/// not show the window on launch
|
|
||||||
pub enable_silent_start: Option<bool>,
|
|
||||||
|
|
||||||
/// set system proxy
|
|
||||||
pub enable_system_proxy: Option<bool>,
|
|
||||||
|
|
||||||
/// enable proxy guard
|
|
||||||
pub enable_proxy_guard: Option<bool>,
|
|
||||||
|
|
||||||
/// set system proxy bypass
|
|
||||||
pub system_proxy_bypass: Option<String>,
|
|
||||||
|
|
||||||
/// proxy guard duration
|
|
||||||
pub proxy_guard_duration: Option<u64>,
|
|
||||||
|
|
||||||
/// theme setting
|
|
||||||
pub theme_setting: Option<VergeTheme>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
|
||||||
pub struct VergeTheme {
|
|
||||||
pub primary_color: Option<String>,
|
|
||||||
pub secondary_color: Option<String>,
|
|
||||||
pub primary_text: Option<String>,
|
|
||||||
pub secondary_text: Option<String>,
|
|
||||||
|
|
||||||
pub info_color: Option<String>,
|
|
||||||
pub error_color: Option<String>,
|
|
||||||
pub warning_color: Option<String>,
|
|
||||||
pub success_color: Option<String>,
|
|
||||||
|
|
||||||
pub font_family: Option<String>,
|
|
||||||
pub css_injection: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl VergeConfig {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
config::read_yaml::<VergeConfig>(dirs::verge_path())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Save Verge App Config
|
|
||||||
pub fn save_file(&self) -> Result<()> {
|
|
||||||
config::save_yaml(
|
|
||||||
dirs::verge_path(),
|
|
||||||
self,
|
|
||||||
Some("# The Config for Clash Verge App\n\n"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Verge App abilities
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Verge {
|
|
||||||
/// manage the verge config
|
|
||||||
pub config: VergeConfig,
|
|
||||||
|
|
||||||
/// current system proxy setting
|
|
||||||
pub cur_sysproxy: Option<SysProxyConfig>,
|
|
||||||
|
|
||||||
/// record the original system proxy
|
|
||||||
/// recover it when exit
|
|
||||||
old_sysproxy: Option<SysProxyConfig>,
|
|
||||||
|
|
||||||
/// helps to auto launch the app
|
|
||||||
auto_launch: Option<AutoLaunch>,
|
|
||||||
|
|
||||||
/// record whether the guard async is running or not
|
|
||||||
guard_state: Arc<Mutex<bool>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Verge {
|
|
||||||
fn default() -> Self {
|
|
||||||
Verge::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Verge {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Verge {
|
|
||||||
config: VergeConfig::new(),
|
|
||||||
old_sysproxy: None,
|
|
||||||
cur_sysproxy: None,
|
|
||||||
auto_launch: None,
|
|
||||||
guard_state: Arc::new(Mutex::new(false)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// init the sysproxy
|
|
||||||
pub fn init_sysproxy(&mut self, port: Option<String>) {
|
|
||||||
if let Some(port) = port {
|
|
||||||
let enable = self.config.enable_system_proxy.clone().unwrap_or(false);
|
|
||||||
|
|
||||||
self.old_sysproxy = match SysProxyConfig::get_sys() {
|
|
||||||
Ok(proxy) => Some(proxy),
|
|
||||||
Err(_) => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let bypass = self.config.system_proxy_bypass.clone();
|
|
||||||
let sysproxy = SysProxyConfig::new(enable, port, bypass);
|
|
||||||
|
|
||||||
if enable {
|
|
||||||
if sysproxy.set_sys().is_err() {
|
|
||||||
log::error!("failed to set system proxy");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.cur_sysproxy = Some(sysproxy);
|
|
||||||
}
|
|
||||||
|
|
||||||
// launchs the system proxy guard
|
|
||||||
Verge::guard_proxy(self.guard_state.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// reset the sysproxy
|
|
||||||
pub fn reset_sysproxy(&mut self) {
|
|
||||||
if let Some(sysproxy) = self.old_sysproxy.take() {
|
|
||||||
match sysproxy.set_sys() {
|
|
||||||
Ok(_) => self.cur_sysproxy = None,
|
|
||||||
Err(_) => log::error!("failed to reset proxy for"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// init the auto launch
|
|
||||||
pub fn init_launch(&mut self) -> Result<()> {
|
|
||||||
let app_exe = current_exe().unwrap();
|
|
||||||
let app_exe = dunce::canonicalize(app_exe).unwrap();
|
|
||||||
let app_name = app_exe.file_stem().unwrap().to_str().unwrap();
|
|
||||||
let app_path = app_exe.as_os_str().to_str().unwrap();
|
|
||||||
|
|
||||||
// fix issue #26
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
let app_path = format!("\"{app_path}\"");
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
let app_path = app_path.as_str();
|
|
||||||
|
|
||||||
let auto = AutoLaunchBuilder::new()
|
|
||||||
.set_app_name(app_name)
|
|
||||||
.set_app_path(app_path)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
if let Some(enable) = self.config.enable_auto_launch.as_ref() {
|
|
||||||
// fix issue #26
|
|
||||||
if *enable {
|
|
||||||
auto.enable()?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.auto_launch = Some(auto);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// update the startup
|
|
||||||
fn update_launch(&mut self, enable: bool) -> Result<()> {
|
|
||||||
let conf_enable = self.config.enable_auto_launch.clone().unwrap_or(false);
|
|
||||||
|
|
||||||
if enable == conf_enable {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let auto_launch = self.auto_launch.clone().unwrap();
|
|
||||||
|
|
||||||
match enable {
|
|
||||||
true => auto_launch.enable()?,
|
|
||||||
false => auto_launch.disable()?,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// patch verge config
|
|
||||||
/// There should be only one update at a time here
|
|
||||||
/// so call the save_file at the end is savely
|
|
||||||
pub fn patch_config(&mut self, patch: VergeConfig) -> Result<()> {
|
|
||||||
// only change it
|
|
||||||
if patch.language.is_some() {
|
|
||||||
self.config.language = patch.language;
|
|
||||||
}
|
|
||||||
if patch.theme_mode.is_some() {
|
|
||||||
self.config.theme_mode = patch.theme_mode;
|
|
||||||
}
|
|
||||||
if patch.theme_blur.is_some() {
|
|
||||||
self.config.theme_blur = patch.theme_blur;
|
|
||||||
}
|
|
||||||
if patch.traffic_graph.is_some() {
|
|
||||||
self.config.traffic_graph = patch.traffic_graph;
|
|
||||||
}
|
|
||||||
if patch.enable_silent_start.is_some() {
|
|
||||||
self.config.enable_silent_start = patch.enable_silent_start;
|
|
||||||
}
|
|
||||||
if patch.theme_setting.is_some() {
|
|
||||||
self.config.theme_setting = patch.theme_setting;
|
|
||||||
}
|
|
||||||
|
|
||||||
// should update system startup
|
|
||||||
if patch.enable_auto_launch.is_some() {
|
|
||||||
let enable = patch.enable_auto_launch.unwrap();
|
|
||||||
self.update_launch(enable)?;
|
|
||||||
self.config.enable_auto_launch = Some(enable);
|
|
||||||
}
|
|
||||||
|
|
||||||
// should update system proxy
|
|
||||||
if patch.enable_system_proxy.is_some() {
|
|
||||||
let enable = patch.enable_system_proxy.unwrap();
|
|
||||||
|
|
||||||
if let Some(mut sysproxy) = self.cur_sysproxy.take() {
|
|
||||||
sysproxy.enable = enable;
|
|
||||||
if sysproxy.set_sys().is_err() {
|
|
||||||
self.cur_sysproxy = Some(sysproxy);
|
|
||||||
|
|
||||||
log::error!("failed to set system proxy");
|
|
||||||
bail!("failed to set system proxy");
|
|
||||||
}
|
|
||||||
self.cur_sysproxy = Some(sysproxy);
|
|
||||||
}
|
|
||||||
self.config.enable_system_proxy = Some(enable);
|
|
||||||
}
|
|
||||||
|
|
||||||
// should update system proxy too
|
|
||||||
if patch.system_proxy_bypass.is_some() {
|
|
||||||
let bypass = patch.system_proxy_bypass.unwrap();
|
|
||||||
|
|
||||||
if let Some(mut sysproxy) = self.cur_sysproxy.take() {
|
|
||||||
if sysproxy.enable {
|
|
||||||
sysproxy.bypass = bypass.clone();
|
|
||||||
|
|
||||||
if sysproxy.set_sys().is_err() {
|
|
||||||
self.cur_sysproxy = Some(sysproxy);
|
|
||||||
|
|
||||||
log::error!("failed to set system proxy");
|
|
||||||
bail!("failed to set system proxy");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.cur_sysproxy = Some(sysproxy);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.config.system_proxy_bypass = Some(bypass);
|
|
||||||
}
|
|
||||||
|
|
||||||
// proxy guard
|
|
||||||
// only change it
|
|
||||||
if patch.enable_proxy_guard.is_some() {
|
|
||||||
self.config.enable_proxy_guard = patch.enable_proxy_guard;
|
|
||||||
}
|
|
||||||
if patch.proxy_guard_duration.is_some() {
|
|
||||||
self.config.proxy_guard_duration = patch.proxy_guard_duration;
|
|
||||||
}
|
|
||||||
|
|
||||||
// relaunch the guard
|
|
||||||
if patch.enable_system_proxy.is_some() || patch.enable_proxy_guard.is_some() {
|
|
||||||
Verge::guard_proxy(self.guard_state.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle the tun mode
|
|
||||||
if patch.enable_tun_mode.is_some() {
|
|
||||||
self.config.enable_tun_mode = patch.enable_tun_mode;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.config.save_file()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Verge {
|
|
||||||
/// launch a system proxy guard
|
|
||||||
/// read config from file directly
|
|
||||||
pub fn guard_proxy(guard_state: Arc<Mutex<bool>>) {
|
|
||||||
use tokio::time::{sleep, Duration};
|
|
||||||
|
|
||||||
tauri::async_runtime::spawn(async move {
|
|
||||||
// if it is running, exit
|
|
||||||
let mut state = guard_state.lock().await;
|
|
||||||
if *state {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
*state = true;
|
|
||||||
std::mem::drop(state);
|
|
||||||
|
|
||||||
// default duration is 10s
|
|
||||||
let mut wait_secs = 10u64;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
sleep(Duration::from_secs(wait_secs)).await;
|
|
||||||
|
|
||||||
log::debug!("guard heartbeat detection");
|
|
||||||
|
|
||||||
let verge = Verge::new();
|
|
||||||
|
|
||||||
let enable_proxy = verge.config.enable_system_proxy.unwrap_or(false);
|
|
||||||
let enable_guard = verge.config.enable_proxy_guard.unwrap_or(false);
|
|
||||||
let guard_duration = verge.config.proxy_guard_duration.unwrap_or(10);
|
|
||||||
|
|
||||||
// update duration
|
|
||||||
wait_secs = guard_duration;
|
|
||||||
|
|
||||||
// stop loop
|
|
||||||
if !enable_guard || !enable_proxy {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
log::info!("try to guard proxy");
|
|
||||||
|
|
||||||
let clash = Clash::new();
|
|
||||||
|
|
||||||
match &clash.info.port {
|
|
||||||
Some(port) => {
|
|
||||||
let bypass = verge.config.system_proxy_bypass.clone();
|
|
||||||
let sysproxy = SysProxyConfig::new(true, port.clone(), bypass);
|
|
||||||
|
|
||||||
log_if_err!(sysproxy.set_sys());
|
|
||||||
}
|
|
||||||
None => log::error!("fail to parse clash port"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut state = guard_state.lock().await;
|
|
||||||
*state = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
26
src-tauri/src/core/win_uwp.rs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#![cfg(target_os = "windows")]
|
||||||
|
|
||||||
|
use crate::utils::dirs;
|
||||||
|
use anyhow::{bail, Result};
|
||||||
|
use deelevate::{PrivilegeLevel, Token};
|
||||||
|
use runas::Command as RunasCommand;
|
||||||
|
use std::process::Command as StdCommand;
|
||||||
|
|
||||||
|
pub async fn invoke_uwptools() -> Result<()> {
|
||||||
|
let resource_dir = dirs::app_resources_dir()?;
|
||||||
|
let tool_path = resource_dir.join("enableLoopback.exe");
|
||||||
|
|
||||||
|
if !tool_path.exists() {
|
||||||
|
bail!("enableLoopback exe not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
let token = Token::with_current_process()?;
|
||||||
|
let level = token.privilege_level()?;
|
||||||
|
|
||||||
|
match level {
|
||||||
|
PrivilegeLevel::NotPrivileged => RunasCommand::new(tool_path).status()?,
|
||||||
|
_ => StdCommand::new(tool_path).status()?,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
6
src-tauri/src/enhance/builtin/meta_guard.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
function main(config) {
|
||||||
|
if (config.mode === "script") {
|
||||||
|
config.mode = "rule";
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
}
|
||||||
10
src-tauri/src/enhance/builtin/meta_hy_alpn.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
function main(config) {
|
||||||
|
if (Array.isArray(config.proxies)) {
|
||||||
|
config.proxies.forEach((p, i) => {
|
||||||
|
if (p.type === "hysteria" && typeof p.alpn === "string") {
|
||||||
|
config.proxies[i].alpn = [p.alpn];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
}
|
||||||
101
src-tauri/src/enhance/chain.rs
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
use crate::{
|
||||||
|
config::PrfItem,
|
||||||
|
utils::{dirs, help},
|
||||||
|
};
|
||||||
|
use serde_yaml::Mapping;
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ChainItem {
|
||||||
|
pub uid: String,
|
||||||
|
pub data: ChainType,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum ChainType {
|
||||||
|
Merge(Mapping),
|
||||||
|
Script(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum ChainSupport {
|
||||||
|
Clash,
|
||||||
|
ClashMeta,
|
||||||
|
ClashMetaAlpha,
|
||||||
|
All,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&PrfItem> for Option<ChainItem> {
|
||||||
|
fn from(item: &PrfItem) -> Self {
|
||||||
|
let itype = item.itype.as_ref()?.as_str();
|
||||||
|
let file = item.file.clone()?;
|
||||||
|
let uid = item.uid.clone().unwrap_or("".into());
|
||||||
|
let path = dirs::app_profiles_dir().ok()?.join(file);
|
||||||
|
|
||||||
|
if !path.exists() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
match itype {
|
||||||
|
"script" => Some(ChainItem {
|
||||||
|
uid,
|
||||||
|
data: ChainType::Script(fs::read_to_string(path).ok()?),
|
||||||
|
}),
|
||||||
|
"merge" => Some(ChainItem {
|
||||||
|
uid,
|
||||||
|
data: ChainType::Merge(help::read_merge_mapping(&path).ok()?),
|
||||||
|
}),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChainItem {
|
||||||
|
/// 内建支持一些脚本
|
||||||
|
pub fn builtin() -> Vec<(ChainSupport, ChainItem)> {
|
||||||
|
// meta 的一些处理
|
||||||
|
let meta_guard =
|
||||||
|
ChainItem::to_script("verge_meta_guard", include_str!("./builtin/meta_guard.js"));
|
||||||
|
|
||||||
|
// meta 1.13.2 alpn string 转 数组
|
||||||
|
let hy_alpn =
|
||||||
|
ChainItem::to_script("verge_hy_alpn", include_str!("./builtin/meta_hy_alpn.js"));
|
||||||
|
|
||||||
|
// meta 的一些处理
|
||||||
|
let meta_guard_alpha =
|
||||||
|
ChainItem::to_script("verge_meta_guard", include_str!("./builtin/meta_guard.js"));
|
||||||
|
|
||||||
|
// meta 1.13.2 alpn string 转 数组
|
||||||
|
let hy_alpn_alpha =
|
||||||
|
ChainItem::to_script("verge_hy_alpn", include_str!("./builtin/meta_hy_alpn.js"));
|
||||||
|
|
||||||
|
vec![
|
||||||
|
(ChainSupport::ClashMeta, hy_alpn),
|
||||||
|
(ChainSupport::ClashMeta, meta_guard),
|
||||||
|
(ChainSupport::ClashMetaAlpha, hy_alpn_alpha),
|
||||||
|
(ChainSupport::ClashMetaAlpha, meta_guard_alpha),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_script<U: Into<String>, D: Into<String>>(uid: U, data: D) -> Self {
|
||||||
|
Self {
|
||||||
|
uid: uid.into(),
|
||||||
|
data: ChainType::Script(data.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChainSupport {
|
||||||
|
pub fn is_support(&self, core: Option<&String>) -> bool {
|
||||||
|
match core {
|
||||||
|
Some(core) => match (self, core.as_str()) {
|
||||||
|
(ChainSupport::All, _) => true,
|
||||||
|
(ChainSupport::Clash, "clash") => true,
|
||||||
|
(ChainSupport::ClashMeta, "clash-meta") => true,
|
||||||
|
(ChainSupport::ClashMetaAlpha, "clash-meta-alpha") => true,
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
None => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
95
src-tauri/src/enhance/field.rs
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
use serde_yaml::{Mapping, Value};
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
pub const HANDLE_FIELDS: [&str; 11] = [
|
||||||
|
"mode",
|
||||||
|
"redir-port",
|
||||||
|
"tproxy-port",
|
||||||
|
"mixed-port",
|
||||||
|
"socks-port",
|
||||||
|
"port",
|
||||||
|
"allow-lan",
|
||||||
|
"log-level",
|
||||||
|
"ipv6",
|
||||||
|
"secret",
|
||||||
|
"external-controller",
|
||||||
|
];
|
||||||
|
|
||||||
|
pub const DEFAULT_FIELDS: [&str; 5] = [
|
||||||
|
"proxies",
|
||||||
|
"proxy-providers",
|
||||||
|
"proxy-groups",
|
||||||
|
"rule-providers",
|
||||||
|
"rules",
|
||||||
|
];
|
||||||
|
|
||||||
|
pub fn use_filter(config: Mapping, filter: &Vec<String>) -> Mapping {
|
||||||
|
let mut ret = Mapping::new();
|
||||||
|
|
||||||
|
for (key, value) in config.into_iter() {
|
||||||
|
if let Some(key) = key.as_str() {
|
||||||
|
if filter.contains(&key.to_string()) {
|
||||||
|
ret.insert(Value::from(key), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn use_lowercase(config: Mapping) -> Mapping {
|
||||||
|
let mut ret = Mapping::new();
|
||||||
|
|
||||||
|
for (key, value) in config.into_iter() {
|
||||||
|
if let Some(key_str) = key.as_str() {
|
||||||
|
let mut key_str = String::from(key_str);
|
||||||
|
key_str.make_ascii_lowercase();
|
||||||
|
ret.insert(Value::from(key_str), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn use_sort(config: Mapping) -> Mapping {
|
||||||
|
let mut ret = Mapping::new();
|
||||||
|
HANDLE_FIELDS.into_iter().for_each(|key| {
|
||||||
|
let key = Value::from(key);
|
||||||
|
if let Some(value) = config.get(&key) {
|
||||||
|
ret.insert(key, value.clone());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let supported_keys: HashSet<&str> = HANDLE_FIELDS.into_iter().chain(DEFAULT_FIELDS).collect();
|
||||||
|
|
||||||
|
let config_keys: HashSet<&str> = config
|
||||||
|
.keys()
|
||||||
|
.filter_map(|e| e.as_str())
|
||||||
|
.into_iter()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
config_keys.difference(&supported_keys).for_each(|&key| {
|
||||||
|
let key = Value::from(key);
|
||||||
|
if let Some(value) = config.get(&key) {
|
||||||
|
ret.insert(key, value.clone());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
DEFAULT_FIELDS.into_iter().for_each(|key| {
|
||||||
|
let key = Value::from(key);
|
||||||
|
if let Some(value) = config.get(&key) {
|
||||||
|
ret.insert(key, value.clone());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn use_keys(config: &Mapping) -> Vec<String> {
|
||||||
|
config
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(key, _)| key.as_str())
|
||||||
|
.map(|s| {
|
||||||
|
let mut s = s.to_string();
|
||||||
|
s.make_ascii_lowercase();
|
||||||
|
s
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
132
src-tauri/src/enhance/merge.rs
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
use super::{use_filter, use_lowercase};
|
||||||
|
use serde_yaml::{self, Mapping, Sequence, Value};
|
||||||
|
|
||||||
|
const MERGE_FIELDS: [&str; 10] = [
|
||||||
|
"prepend-rules",
|
||||||
|
"append-rules",
|
||||||
|
"prepend-rule-providers",
|
||||||
|
"append-rule-providers",
|
||||||
|
"prepend-proxies",
|
||||||
|
"append-proxies",
|
||||||
|
"prepend-proxy-providers",
|
||||||
|
"append-proxy-providers",
|
||||||
|
"prepend-proxy-groups",
|
||||||
|
"append-proxy-groups",
|
||||||
|
];
|
||||||
|
|
||||||
|
pub fn use_merge(merge: Mapping, mut config: Mapping) -> Mapping {
|
||||||
|
// 直接覆盖原字段
|
||||||
|
use_lowercase(merge.clone())
|
||||||
|
.into_iter()
|
||||||
|
.filter(|(key, _)| !MERGE_FIELDS.contains(&key.as_str().unwrap_or_default()))
|
||||||
|
.for_each(|(key, value)| {
|
||||||
|
config.insert(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
let merge_list = MERGE_FIELDS.iter().map(|s| s.to_string());
|
||||||
|
let merge = use_filter(merge, &merge_list.collect());
|
||||||
|
|
||||||
|
["rule-providers", "proxy-providers"]
|
||||||
|
.iter()
|
||||||
|
.for_each(|key_str| {
|
||||||
|
let key_val = Value::from(key_str.to_string());
|
||||||
|
|
||||||
|
let mut map = Mapping::default();
|
||||||
|
|
||||||
|
map = config.get(&key_val).map_or(map.clone(), |val| {
|
||||||
|
val.as_mapping().map_or(map, |v| v.clone())
|
||||||
|
});
|
||||||
|
|
||||||
|
let pre_key = Value::from(format!("prepend-{key_str}"));
|
||||||
|
let post_key = Value::from(format!("append-{key_str}"));
|
||||||
|
|
||||||
|
if let Some(pre_val) = merge.get(&pre_key) {
|
||||||
|
if pre_val.is_mapping() {
|
||||||
|
let mut pre_val = pre_val.as_mapping().unwrap().clone();
|
||||||
|
pre_val.extend(map);
|
||||||
|
map = pre_val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(post_val) = merge.get(&post_key) {
|
||||||
|
if post_val.is_mapping() {
|
||||||
|
map.extend(post_val.as_mapping().unwrap().clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !map.is_empty() {
|
||||||
|
config.insert(key_val, Value::from(map));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
["rules", "proxies", "proxy-groups"]
|
||||||
|
.iter()
|
||||||
|
.for_each(|key_str| {
|
||||||
|
let key_val = Value::from(key_str.to_string());
|
||||||
|
|
||||||
|
let mut list = Sequence::default();
|
||||||
|
list = config.get(&key_val).map_or(list.clone(), |val| {
|
||||||
|
val.as_sequence().map_or(list, |v| v.clone())
|
||||||
|
});
|
||||||
|
|
||||||
|
let pre_key = Value::from(format!("prepend-{key_str}"));
|
||||||
|
let post_key = Value::from(format!("append-{key_str}"));
|
||||||
|
|
||||||
|
if let Some(pre_val) = merge.get(&pre_key) {
|
||||||
|
if pre_val.is_sequence() {
|
||||||
|
let mut pre_val = pre_val.as_sequence().unwrap().clone();
|
||||||
|
pre_val.extend(list);
|
||||||
|
list = pre_val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(post_val) = merge.get(&post_key) {
|
||||||
|
if post_val.is_sequence() {
|
||||||
|
list.extend(post_val.as_sequence().unwrap().clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !list.is_empty() {
|
||||||
|
config.insert(key_val, Value::from(list));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
config
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_merge() -> anyhow::Result<()> {
|
||||||
|
let merge = r"
|
||||||
|
prepend-rules:
|
||||||
|
- prepend
|
||||||
|
- 1123123
|
||||||
|
append-rules:
|
||||||
|
- append
|
||||||
|
prepend-proxies:
|
||||||
|
- 9999
|
||||||
|
append-proxies:
|
||||||
|
- 1111
|
||||||
|
rules:
|
||||||
|
- replace
|
||||||
|
proxy-groups:
|
||||||
|
- 123781923810
|
||||||
|
tun:
|
||||||
|
enable: true
|
||||||
|
dns:
|
||||||
|
enable: true
|
||||||
|
";
|
||||||
|
|
||||||
|
let config = r"
|
||||||
|
rules:
|
||||||
|
- aaaaa
|
||||||
|
script1: test
|
||||||
|
";
|
||||||
|
|
||||||
|
let merge = serde_yaml::from_str::<Mapping>(merge)?;
|
||||||
|
let config = serde_yaml::from_str::<Mapping>(config)?;
|
||||||
|
|
||||||
|
let result = serde_yaml::to_string(&use_merge(merge, config))?;
|
||||||
|
|
||||||
|
println!("{result}");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
126
src-tauri/src/enhance/mod.rs
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
mod chain;
|
||||||
|
pub mod field;
|
||||||
|
mod merge;
|
||||||
|
mod script;
|
||||||
|
mod tun;
|
||||||
|
|
||||||
|
use self::chain::*;
|
||||||
|
use self::field::*;
|
||||||
|
use self::merge::*;
|
||||||
|
use self::script::*;
|
||||||
|
use self::tun::*;
|
||||||
|
use crate::config::Config;
|
||||||
|
use serde_yaml::Mapping;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
type ResultLog = Vec<(String, String)>;
|
||||||
|
|
||||||
|
/// Enhance mode
|
||||||
|
/// 返回最终订阅、该订阅包含的键、和script执行的结果
|
||||||
|
pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||||
|
// config.yaml 的订阅
|
||||||
|
let clash_config = { Config::clash().latest().0.clone() };
|
||||||
|
|
||||||
|
let (clash_core, enable_tun, enable_builtin) = {
|
||||||
|
let verge = Config::verge();
|
||||||
|
let verge = verge.latest();
|
||||||
|
(
|
||||||
|
verge.clash_core.clone(),
|
||||||
|
verge.enable_tun_mode.unwrap_or(false),
|
||||||
|
verge.enable_builtin_enhanced.unwrap_or(true),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
// 从profiles里拿东西
|
||||||
|
let (mut config, chain) = {
|
||||||
|
let profiles = Config::profiles();
|
||||||
|
let profiles = profiles.latest();
|
||||||
|
|
||||||
|
let current = profiles.current_mapping().unwrap_or_default();
|
||||||
|
|
||||||
|
let chain = match profiles.chain.as_ref() {
|
||||||
|
Some(chain) => chain
|
||||||
|
.iter()
|
||||||
|
.filter_map(|uid| profiles.get_item(uid).ok())
|
||||||
|
.filter_map(<Option<ChainItem>>::from)
|
||||||
|
.collect::<Vec<ChainItem>>(),
|
||||||
|
None => vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
(current, chain)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut result_map = HashMap::new(); // 保存脚本日志
|
||||||
|
let mut exists_keys = use_keys(&config); // 保存出现过的keys
|
||||||
|
|
||||||
|
// 处理用户的profile
|
||||||
|
chain.into_iter().for_each(|item| match item.data {
|
||||||
|
ChainType::Merge(merge) => {
|
||||||
|
exists_keys.extend(use_keys(&merge));
|
||||||
|
config = use_merge(merge, config.to_owned());
|
||||||
|
}
|
||||||
|
ChainType::Script(script) => {
|
||||||
|
let mut logs = vec![];
|
||||||
|
|
||||||
|
match use_script(script, config.to_owned()) {
|
||||||
|
Ok((res_config, res_logs)) => {
|
||||||
|
exists_keys.extend(use_keys(&res_config));
|
||||||
|
config = res_config;
|
||||||
|
logs.extend(res_logs);
|
||||||
|
}
|
||||||
|
Err(err) => logs.push(("exception".into(), err.to_string())),
|
||||||
|
}
|
||||||
|
|
||||||
|
result_map.insert(item.uid, logs);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 合并默认的config
|
||||||
|
for (key, value) in clash_config.into_iter() {
|
||||||
|
if key.as_str() == Some("tun") {
|
||||||
|
let mut tun = config.get_mut("tun").map_or(Mapping::new(), |val| {
|
||||||
|
val.as_mapping().cloned().unwrap_or(Mapping::new())
|
||||||
|
});
|
||||||
|
let patch_tun = value.as_mapping().cloned().unwrap_or(Mapping::new());
|
||||||
|
for (key, value) in patch_tun.into_iter() {
|
||||||
|
tun.insert(key, value);
|
||||||
|
}
|
||||||
|
config.insert("tun".into(), tun.into());
|
||||||
|
} else {
|
||||||
|
config.insert(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 内建脚本最后跑
|
||||||
|
if enable_builtin {
|
||||||
|
ChainItem::builtin()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|(s, _)| s.is_support(clash_core.as_ref()))
|
||||||
|
.map(|(_, c)| c)
|
||||||
|
.for_each(|item| {
|
||||||
|
log::debug!(target: "app", "run builtin script {}", item.uid);
|
||||||
|
|
||||||
|
match item.data {
|
||||||
|
ChainType::Script(script) => match use_script(script, config.to_owned()) {
|
||||||
|
Ok((res_config, _)) => {
|
||||||
|
config = res_config;
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log::error!(target: "app", "builtin script error `{err}`");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
config = use_tun(config, enable_tun);
|
||||||
|
config = use_sort(config);
|
||||||
|
|
||||||
|
let mut exists_set = HashSet::new();
|
||||||
|
exists_set.extend(exists_keys.into_iter());
|
||||||
|
exists_keys = exists_set.into_iter().collect();
|
||||||
|
|
||||||
|
(config, exists_keys, result_map)
|
||||||
|
}
|
||||||
107
src-tauri/src/enhance/script.rs
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
use super::use_lowercase;
|
||||||
|
use anyhow::{Error, Result};
|
||||||
|
use serde_yaml::Mapping;
|
||||||
|
|
||||||
|
pub fn use_script(script: String, config: Mapping) -> Result<(Mapping, Vec<(String, String)>)> {
|
||||||
|
use boa_engine::{native_function::NativeFunction, Context, JsValue, Source};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
let mut context = Context::default();
|
||||||
|
|
||||||
|
let outputs = Arc::new(Mutex::new(vec![]));
|
||||||
|
|
||||||
|
let copy_outputs = outputs.clone();
|
||||||
|
unsafe {
|
||||||
|
let _ = context.register_global_builtin_callable(
|
||||||
|
"__verge_log__".into(),
|
||||||
|
2,
|
||||||
|
NativeFunction::from_closure(
|
||||||
|
move |_: &JsValue, args: &[JsValue], context: &mut Context| {
|
||||||
|
let level = args.get(0).unwrap().to_string(context)?;
|
||||||
|
let level = level.to_std_string().unwrap();
|
||||||
|
let data = args.get(1).unwrap().to_string(context)?;
|
||||||
|
let data = data.to_std_string().unwrap();
|
||||||
|
let mut out = copy_outputs.lock().unwrap();
|
||||||
|
out.push((level, data));
|
||||||
|
Ok(JsValue::undefined())
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let _ = context.eval(Source::from_bytes(
|
||||||
|
r#"var console = Object.freeze({
|
||||||
|
log(data){__verge_log__("log",JSON.stringify(data))},
|
||||||
|
info(data){__verge_log__("info",JSON.stringify(data))},
|
||||||
|
error(data){__verge_log__("error",JSON.stringify(data))},
|
||||||
|
debug(data){__verge_log__("debug",JSON.stringify(data))},
|
||||||
|
});"#,
|
||||||
|
));
|
||||||
|
|
||||||
|
let config = use_lowercase(config.clone());
|
||||||
|
let config_str = serde_json::to_string(&config)?;
|
||||||
|
|
||||||
|
let code = format!(
|
||||||
|
r#"try{{
|
||||||
|
{script};
|
||||||
|
JSON.stringify(main({config_str})||'')
|
||||||
|
}} catch(err) {{
|
||||||
|
`__error_flag__ ${{err.toString()}}`
|
||||||
|
}}"#
|
||||||
|
);
|
||||||
|
if let Ok(result) = context.eval(Source::from_bytes(code.as_str())) {
|
||||||
|
if !result.is_string() {
|
||||||
|
anyhow::bail!("main function should return object");
|
||||||
|
}
|
||||||
|
let result = result.to_string(&mut context).unwrap();
|
||||||
|
let result = result.to_std_string().unwrap();
|
||||||
|
if result.starts_with("__error_flag__") {
|
||||||
|
anyhow::bail!(result[15..].to_owned());
|
||||||
|
}
|
||||||
|
if result == "\"\"" {
|
||||||
|
anyhow::bail!("main function should return object");
|
||||||
|
}
|
||||||
|
let res: Result<Mapping, Error> = Ok(serde_json::from_str::<Mapping>(result.as_str())?);
|
||||||
|
let mut out = outputs.lock().unwrap();
|
||||||
|
match res {
|
||||||
|
Ok(config) => Ok((use_lowercase(config), out.to_vec())),
|
||||||
|
Err(err) => {
|
||||||
|
out.push(("exception".into(), err.to_string()));
|
||||||
|
Ok((config, out.to_vec()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
anyhow::bail!("main function should return object");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_script() {
|
||||||
|
let script = r#"
|
||||||
|
function main(config) {
|
||||||
|
if (Array.isArray(config.rules)) {
|
||||||
|
config.rules = [...config.rules, "add"];
|
||||||
|
}
|
||||||
|
console.log(config);
|
||||||
|
config.proxies = ["111"];
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let config = r#"
|
||||||
|
rules:
|
||||||
|
- 111
|
||||||
|
- 222
|
||||||
|
tun:
|
||||||
|
enable: false
|
||||||
|
dns:
|
||||||
|
enable: false
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let config = serde_yaml::from_str(config).unwrap();
|
||||||
|
let (config, results) = use_script(script.into(), config).unwrap();
|
||||||
|
|
||||||
|
let config_str = serde_yaml::to_string(&config).unwrap();
|
||||||
|
|
||||||
|
println!("{config_str}");
|
||||||
|
|
||||||
|
dbg!(results);
|
||||||
|
}
|
||||||
127
src-tauri/src/enhance/tun.rs
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
use serde_yaml::{Mapping, Value};
|
||||||
|
|
||||||
|
macro_rules! revise {
|
||||||
|
($map: expr, $key: expr, $val: expr) => {
|
||||||
|
let ret_key = Value::String($key.into());
|
||||||
|
$map.insert(ret_key, Value::from($val));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// if key not exists then append value
|
||||||
|
macro_rules! append {
|
||||||
|
($map: expr, $key: expr, $val: expr) => {
|
||||||
|
let ret_key = Value::String($key.into());
|
||||||
|
if !$map.contains_key(&ret_key) {
|
||||||
|
$map.insert(ret_key, Value::from($val));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
|
||||||
|
let tun_key = Value::from("tun");
|
||||||
|
let tun_val = config.get(&tun_key);
|
||||||
|
|
||||||
|
if !enable && tun_val.is_none() {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut tun_val = tun_val.map_or(Mapping::new(), |val| {
|
||||||
|
val.as_mapping().cloned().unwrap_or(Mapping::new())
|
||||||
|
});
|
||||||
|
|
||||||
|
revise!(tun_val, "enable", enable);
|
||||||
|
|
||||||
|
revise!(config, "tun", tun_val);
|
||||||
|
|
||||||
|
if enable {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
use crate::utils::dirs;
|
||||||
|
use tauri::api::process::Command;
|
||||||
|
log::info!(target: "app", "try to set system dns");
|
||||||
|
let resource_dir = dirs::app_resources_dir().unwrap();
|
||||||
|
let script = resource_dir.join("set_dns.sh");
|
||||||
|
let script = script.to_string_lossy();
|
||||||
|
match Command::new("bash")
|
||||||
|
.args([script])
|
||||||
|
.current_dir(resource_dir)
|
||||||
|
.status()
|
||||||
|
{
|
||||||
|
Ok(status) => {
|
||||||
|
if status.success() {
|
||||||
|
log::info!(target: "app", "set system dns successfully");
|
||||||
|
} else {
|
||||||
|
let code = status.code().unwrap_or(-1);
|
||||||
|
log::error!(target: "app", "set system dns failed: {code}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log::error!(target: "app", "set system dns failed: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
use_dns_for_tun(config)
|
||||||
|
} else {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
use crate::utils::dirs;
|
||||||
|
use tauri::api::process::Command;
|
||||||
|
log::info!(target: "app", "try to unset system dns");
|
||||||
|
let resource_dir = dirs::app_resources_dir().unwrap();
|
||||||
|
let script = resource_dir.join("unset_dns.sh");
|
||||||
|
let script = script.to_string_lossy();
|
||||||
|
match Command::new("bash")
|
||||||
|
.args([script])
|
||||||
|
.current_dir(resource_dir)
|
||||||
|
.status()
|
||||||
|
{
|
||||||
|
Ok(status) => {
|
||||||
|
if status.success() {
|
||||||
|
log::info!(target: "app", "unset system dns successfully");
|
||||||
|
} else {
|
||||||
|
let code = status.code().unwrap_or(-1);
|
||||||
|
log::error!(target: "app", "unset system dns failed: {code}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log::error!(target: "app", "unset system dns failed: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
config
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn use_dns_for_tun(mut config: Mapping) -> Mapping {
|
||||||
|
let dns_key = Value::from("dns");
|
||||||
|
let dns_val = config.get(&dns_key);
|
||||||
|
|
||||||
|
let mut dns_val = dns_val.map_or(Mapping::new(), |val| {
|
||||||
|
val.as_mapping().cloned().unwrap_or(Mapping::new())
|
||||||
|
});
|
||||||
|
|
||||||
|
// 开启tun将同时开启dns
|
||||||
|
revise!(dns_val, "enable", true);
|
||||||
|
|
||||||
|
append!(dns_val, "enhanced-mode", "fake-ip");
|
||||||
|
append!(dns_val, "fake-ip-range", "198.18.0.1/16");
|
||||||
|
append!(
|
||||||
|
dns_val,
|
||||||
|
"nameserver",
|
||||||
|
vec!["114.114.114.114", "223.5.5.5", "8.8.8.8"]
|
||||||
|
);
|
||||||
|
append!(dns_val, "fallback", vec![] as Vec<&str>);
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
append!(
|
||||||
|
dns_val,
|
||||||
|
"fake-ip-filter",
|
||||||
|
vec![
|
||||||
|
"dns.msftncsi.com",
|
||||||
|
"www.msftncsi.com",
|
||||||
|
"www.msftconnecttest.com"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
revise!(config, "dns", dns_val);
|
||||||
|
config
|
||||||
|
}
|
||||||
364
src-tauri/src/feat.rs
Normal file
@@ -0,0 +1,364 @@
|
|||||||
|
//!
|
||||||
|
//! feat mod 里的函数主要用于
|
||||||
|
//! - hotkey 快捷键
|
||||||
|
//! - timer 定时器
|
||||||
|
//! - cmds 页面调用
|
||||||
|
//!
|
||||||
|
use crate::config::*;
|
||||||
|
use crate::core::*;
|
||||||
|
use crate::log_err;
|
||||||
|
use crate::utils::resolve;
|
||||||
|
use anyhow::{bail, Result};
|
||||||
|
use serde_yaml::{Mapping, Value};
|
||||||
|
use tauri::{AppHandle, ClipboardManager, Manager};
|
||||||
|
|
||||||
|
// 打开面板
|
||||||
|
pub fn open_or_close_dashboard() {
|
||||||
|
let handle = handle::Handle::global();
|
||||||
|
let app_handle = handle.app_handle.lock();
|
||||||
|
if let Some(app_handle) = app_handle.as_ref() {
|
||||||
|
if let Some(window) = app_handle.get_window("main") {
|
||||||
|
if let Ok(true) = window.is_focused() {
|
||||||
|
let _ = window.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolve::create_window(app_handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重启clash
|
||||||
|
pub fn restart_clash_core() {
|
||||||
|
tauri::async_runtime::spawn(async {
|
||||||
|
match CoreManager::global().run_core().await {
|
||||||
|
Ok(_) => {
|
||||||
|
handle::Handle::refresh_clash();
|
||||||
|
handle::Handle::notice_message("set_config::ok", "ok");
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
handle::Handle::notice_message("set_config::error", format!("{err}"));
|
||||||
|
log::error!(target:"app", "{err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换模式 rule/global/direct/script mode
|
||||||
|
pub fn change_clash_mode(mode: String) {
|
||||||
|
let mut mapping = Mapping::new();
|
||||||
|
mapping.insert(Value::from("mode"), mode.clone().into());
|
||||||
|
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
log::debug!(target: "app", "change clash mode to {mode}");
|
||||||
|
|
||||||
|
match clash_api::patch_configs(&mapping).await {
|
||||||
|
Ok(_) => {
|
||||||
|
// 更新订阅
|
||||||
|
Config::clash().data().patch_config(mapping);
|
||||||
|
|
||||||
|
if Config::clash().data().save_config().is_ok() {
|
||||||
|
handle::Handle::refresh_clash();
|
||||||
|
log_err!(handle::Handle::update_systray_part());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => log::error!(target: "app", "{err}"),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换系统代理
|
||||||
|
pub fn toggle_system_proxy() {
|
||||||
|
let enable = Config::verge().draft().enable_system_proxy;
|
||||||
|
let enable = enable.unwrap_or(false);
|
||||||
|
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
match patch_verge(IVerge {
|
||||||
|
enable_system_proxy: Some(!enable),
|
||||||
|
..IVerge::default()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => handle::Handle::refresh_verge(),
|
||||||
|
Err(err) => log::error!(target: "app", "{err}"),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换tun模式
|
||||||
|
pub fn toggle_tun_mode() {
|
||||||
|
let enable = Config::verge().data().enable_tun_mode;
|
||||||
|
let enable = enable.unwrap_or(false);
|
||||||
|
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
match patch_verge(IVerge {
|
||||||
|
enable_tun_mode: Some(!enable),
|
||||||
|
..IVerge::default()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => handle::Handle::refresh_verge(),
|
||||||
|
Err(err) => log::error!(target: "app", "{err}"),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 修改clash的订阅
|
||||||
|
pub async fn patch_clash(patch: Mapping) -> Result<()> {
|
||||||
|
Config::clash().draft().patch_config(patch.clone());
|
||||||
|
|
||||||
|
match {
|
||||||
|
let redir_port = patch.get("redir-port");
|
||||||
|
let tproxy_port = patch.get("tproxy-port");
|
||||||
|
let mixed_port = patch.get("mixed-port");
|
||||||
|
let socks_port = patch.get("socks-port");
|
||||||
|
let port = patch.get("port");
|
||||||
|
let enable_random_port = Config::verge().latest().enable_random_port.unwrap_or(false);
|
||||||
|
if mixed_port.is_some() && !enable_random_port {
|
||||||
|
let changed = mixed_port.unwrap()
|
||||||
|
!= Config::verge()
|
||||||
|
.latest()
|
||||||
|
.verge_mixed_port
|
||||||
|
.unwrap_or(Config::clash().data().get_mixed_port());
|
||||||
|
// 检查端口占用
|
||||||
|
if changed {
|
||||||
|
if let Some(port) = mixed_port.unwrap().as_u64() {
|
||||||
|
if !port_scanner::local_port_available(port as u16) {
|
||||||
|
Config::clash().discard();
|
||||||
|
bail!("port already in use");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 激活订阅
|
||||||
|
if redir_port.is_some()
|
||||||
|
|| tproxy_port.is_some()
|
||||||
|
|| mixed_port.is_some()
|
||||||
|
|| socks_port.is_some()
|
||||||
|
|| port.is_some()
|
||||||
|
|| patch.get("secret").is_some()
|
||||||
|
|| patch.get("external-controller").is_some()
|
||||||
|
{
|
||||||
|
Config::generate()?;
|
||||||
|
CoreManager::global().run_core().await?;
|
||||||
|
handle::Handle::refresh_clash();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新系统代理
|
||||||
|
if mixed_port.is_some() {
|
||||||
|
log_err!(sysopt::Sysopt::global().init_sysproxy());
|
||||||
|
}
|
||||||
|
|
||||||
|
if patch.get("mode").is_some() {
|
||||||
|
log_err!(handle::Handle::update_systray_part());
|
||||||
|
}
|
||||||
|
|
||||||
|
Config::runtime().latest().patch_config(patch);
|
||||||
|
|
||||||
|
<Result<()>>::Ok(())
|
||||||
|
} {
|
||||||
|
Ok(()) => {
|
||||||
|
Config::clash().apply();
|
||||||
|
Config::clash().data().save_config()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
Config::clash().discard();
|
||||||
|
Err(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 修改verge的订阅
|
||||||
|
/// 一般都是一个个的修改
|
||||||
|
pub async fn patch_verge(patch: IVerge) -> Result<()> {
|
||||||
|
Config::verge().draft().patch_config(patch.clone());
|
||||||
|
|
||||||
|
let tun_mode = patch.enable_tun_mode;
|
||||||
|
let auto_launch = patch.enable_auto_launch;
|
||||||
|
let system_proxy = patch.enable_system_proxy;
|
||||||
|
let proxy_bypass = patch.system_proxy_bypass;
|
||||||
|
let language = patch.language;
|
||||||
|
let port = patch.verge_mixed_port;
|
||||||
|
let common_tray_icon = patch.common_tray_icon;
|
||||||
|
let sysproxy_tray_icon = patch.sysproxy_tray_icon;
|
||||||
|
let tun_tray_icon = patch.tun_tray_icon;
|
||||||
|
|
||||||
|
match {
|
||||||
|
let service_mode = patch.enable_service_mode;
|
||||||
|
|
||||||
|
if service_mode.is_some() {
|
||||||
|
log::debug!(target: "app", "change service mode to {}", service_mode.unwrap());
|
||||||
|
|
||||||
|
Config::generate()?;
|
||||||
|
CoreManager::global().run_core().await?;
|
||||||
|
} else if tun_mode.is_some() {
|
||||||
|
update_core_config().await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if auto_launch.is_some() {
|
||||||
|
sysopt::Sysopt::global().update_launch()?;
|
||||||
|
}
|
||||||
|
if system_proxy.is_some() || proxy_bypass.is_some() || port.is_some() {
|
||||||
|
sysopt::Sysopt::global().update_sysproxy()?;
|
||||||
|
sysopt::Sysopt::global().guard_proxy();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(true) = patch.enable_proxy_guard {
|
||||||
|
sysopt::Sysopt::global().guard_proxy();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(hotkeys) = patch.hotkeys {
|
||||||
|
hotkey::Hotkey::global().update(hotkeys)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if language.is_some() {
|
||||||
|
handle::Handle::update_systray()?;
|
||||||
|
} else if system_proxy.is_some()
|
||||||
|
|| tun_mode.is_some()
|
||||||
|
|| common_tray_icon.is_some()
|
||||||
|
|| sysproxy_tray_icon.is_some()
|
||||||
|
|| tun_tray_icon.is_some()
|
||||||
|
{
|
||||||
|
handle::Handle::update_systray_part()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
<Result<()>>::Ok(())
|
||||||
|
} {
|
||||||
|
Ok(()) => {
|
||||||
|
Config::verge().apply();
|
||||||
|
Config::verge().data().save_file()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
Config::verge().discard();
|
||||||
|
Err(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 更新某个profile
|
||||||
|
/// 如果更新当前订阅就激活订阅
|
||||||
|
pub async fn update_profile(uid: String, option: Option<PrfOption>) -> Result<()> {
|
||||||
|
let url_opt = {
|
||||||
|
let profiles = Config::profiles();
|
||||||
|
let profiles = profiles.latest();
|
||||||
|
let item = profiles.get_item(&uid)?;
|
||||||
|
let is_remote = item.itype.as_ref().map_or(false, |s| s == "remote");
|
||||||
|
|
||||||
|
if !is_remote {
|
||||||
|
None // 直接更新
|
||||||
|
} else if item.url.is_none() {
|
||||||
|
bail!("failed to get the profile item url");
|
||||||
|
} else {
|
||||||
|
Some((item.url.clone().unwrap(), item.option.clone()))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let should_update = match url_opt {
|
||||||
|
Some((url, opt)) => {
|
||||||
|
let merged_opt = PrfOption::merge(opt, option);
|
||||||
|
let item = PrfItem::from_url(&url, None, None, merged_opt).await?;
|
||||||
|
|
||||||
|
let profiles = Config::profiles();
|
||||||
|
let mut profiles = profiles.latest();
|
||||||
|
profiles.update_item(uid.clone(), item)?;
|
||||||
|
|
||||||
|
Some(uid) == profiles.get_current()
|
||||||
|
}
|
||||||
|
None => true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if should_update {
|
||||||
|
update_core_config().await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 更新订阅
|
||||||
|
async fn update_core_config() -> Result<()> {
|
||||||
|
match CoreManager::global().update_config().await {
|
||||||
|
Ok(_) => {
|
||||||
|
handle::Handle::refresh_clash();
|
||||||
|
handle::Handle::notice_message("set_config::ok", "ok");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
handle::Handle::notice_message("set_config::error", format!("{err}"));
|
||||||
|
Err(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// copy env variable
|
||||||
|
pub fn copy_clash_env(app_handle: &AppHandle) {
|
||||||
|
let port = { Config::verge().latest().verge_mixed_port.unwrap_or(7897) };
|
||||||
|
let http_proxy = format!("http://127.0.0.1:{}", port);
|
||||||
|
let socks5_proxy = format!("socks5://127.0.0.1:{}", port);
|
||||||
|
|
||||||
|
let sh =
|
||||||
|
format!("export https_proxy={http_proxy} http_proxy={http_proxy} all_proxy={socks5_proxy}");
|
||||||
|
let cmd: String = format!("set http_proxy={http_proxy}\r\nset https_proxy={http_proxy}");
|
||||||
|
let ps: String = format!("$env:HTTP_PROXY=\"{http_proxy}\"; $env:HTTPS_PROXY=\"{http_proxy}\"");
|
||||||
|
|
||||||
|
let mut cliboard = app_handle.clipboard_manager();
|
||||||
|
|
||||||
|
let env_type = { Config::verge().latest().env_type.clone() };
|
||||||
|
let env_type = match env_type {
|
||||||
|
Some(env_type) => env_type,
|
||||||
|
None => {
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
let default = "bash";
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
let default = "powershell";
|
||||||
|
|
||||||
|
default.to_string()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match env_type.as_str() {
|
||||||
|
"bash" => cliboard.write_text(sh).unwrap_or_default(),
|
||||||
|
"cmd" => cliboard.write_text(cmd).unwrap_or_default(),
|
||||||
|
"powershell" => cliboard.write_text(ps).unwrap_or_default(),
|
||||||
|
_ => log::error!(target: "app", "copy_clash_env: Invalid env type! {env_type}"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn test_delay(url: String) -> Result<u32> {
|
||||||
|
use tokio::time::{Duration, Instant};
|
||||||
|
let mut builder = reqwest::ClientBuilder::new().use_rustls_tls().no_proxy();
|
||||||
|
|
||||||
|
let port = Config::verge()
|
||||||
|
.latest()
|
||||||
|
.verge_mixed_port
|
||||||
|
.unwrap_or(Config::clash().data().get_mixed_port());
|
||||||
|
let tun_mode = Config::verge().latest().enable_tun_mode.unwrap_or(false);
|
||||||
|
|
||||||
|
let proxy_scheme = format!("http://127.0.0.1:{port}");
|
||||||
|
|
||||||
|
if !tun_mode {
|
||||||
|
if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) {
|
||||||
|
builder = builder.proxy(proxy);
|
||||||
|
}
|
||||||
|
if let Ok(proxy) = reqwest::Proxy::https(&proxy_scheme) {
|
||||||
|
builder = builder.proxy(proxy);
|
||||||
|
}
|
||||||
|
if let Ok(proxy) = reqwest::Proxy::all(&proxy_scheme) {
|
||||||
|
builder = builder.proxy(proxy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let request = builder
|
||||||
|
.timeout(Duration::from_millis(10000))
|
||||||
|
.build()?
|
||||||
|
.get(url).header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0");
|
||||||
|
let start = Instant::now();
|
||||||
|
|
||||||
|
let response = request.send().await?;
|
||||||
|
if response.status().is_success() {
|
||||||
|
let delay = start.elapsed().as_millis() as u32;
|
||||||
|
Ok(delay)
|
||||||
|
} else {
|
||||||
|
Ok(10000u32)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,163 +1,140 @@
|
|||||||
#![cfg_attr(
|
#![cfg_attr(
|
||||||
all(not(debug_assertions), target_os = "windows"),
|
all(not(debug_assertions), target_os = "windows"),
|
||||||
windows_subsystem = "windows"
|
windows_subsystem = "windows"
|
||||||
)]
|
)]
|
||||||
|
|
||||||
mod cmds;
|
mod cmds;
|
||||||
|
mod config;
|
||||||
mod core;
|
mod core;
|
||||||
mod states;
|
mod enhance;
|
||||||
|
mod feat;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
use crate::{
|
use crate::utils::{init, resolve, server};
|
||||||
core::VergeConfig,
|
use tauri::{api, SystemTray};
|
||||||
utils::{resolve, server},
|
|
||||||
};
|
|
||||||
use tauri::{
|
|
||||||
api, CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem,
|
|
||||||
};
|
|
||||||
|
|
||||||
fn main() -> std::io::Result<()> {
|
fn main() -> std::io::Result<()> {
|
||||||
if server::check_singleton().is_err() {
|
// 单例检测
|
||||||
println!("app exists");
|
if server::check_singleton().is_err() {
|
||||||
return Ok(());
|
println!("app exists");
|
||||||
}
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
let tray_menu = SystemTrayMenu::new()
|
#[cfg(target_os = "linux")]
|
||||||
.add_item(CustomMenuItem::new("open_window", "Show"))
|
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
|
||||||
.add_item(CustomMenuItem::new("system_proxy", "System Proxy"))
|
|
||||||
.add_item(CustomMenuItem::new("restart_clash", "Restart Clash"))
|
|
||||||
.add_native_item(SystemTrayMenuItem::Separator)
|
|
||||||
.add_item(CustomMenuItem::new("quit", "Quit").accelerator("CmdOrControl+Q"));
|
|
||||||
|
|
||||||
#[allow(unused_mut)]
|
crate::log_err!(init::init_config());
|
||||||
let mut builder = tauri::Builder::default()
|
|
||||||
.manage(states::VergeState::default())
|
#[allow(unused_mut)]
|
||||||
.manage(states::ClashState::default())
|
let mut builder = tauri::Builder::default()
|
||||||
.manage(states::ProfilesState::default())
|
.system_tray(SystemTray::new())
|
||||||
.setup(|app| Ok(resolve::resolve_setup(app)))
|
.setup(|app| {
|
||||||
.system_tray(SystemTray::new().with_menu(tray_menu))
|
resolve::resolve_setup(app);
|
||||||
.on_system_tray_event(move |app_handle, event| match event {
|
Ok(())
|
||||||
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
|
})
|
||||||
"open_window" => {
|
.on_system_tray_event(core::tray::Tray::on_system_tray_event)
|
||||||
let window = app_handle.get_window("main").unwrap();
|
.invoke_handler(tauri::generate_handler![
|
||||||
window.unminimize().unwrap();
|
// common
|
||||||
window.show().unwrap();
|
cmds::get_sys_proxy,
|
||||||
window.set_focus().unwrap();
|
cmds::open_app_dir,
|
||||||
|
cmds::open_logs_dir,
|
||||||
|
cmds::open_web_url,
|
||||||
|
cmds::open_core_dir,
|
||||||
|
cmds::get_portable_flag,
|
||||||
|
// cmds::kill_sidecar,
|
||||||
|
cmds::restart_sidecar,
|
||||||
|
cmds::grant_permission,
|
||||||
|
// clash
|
||||||
|
cmds::get_clash_info,
|
||||||
|
cmds::get_clash_logs,
|
||||||
|
cmds::patch_clash_config,
|
||||||
|
cmds::change_clash_core,
|
||||||
|
cmds::get_runtime_config,
|
||||||
|
cmds::get_runtime_yaml,
|
||||||
|
cmds::get_runtime_exists,
|
||||||
|
cmds::get_runtime_logs,
|
||||||
|
cmds::uwp::invoke_uwp_tool,
|
||||||
|
// verge
|
||||||
|
cmds::get_verge_config,
|
||||||
|
cmds::patch_verge_config,
|
||||||
|
cmds::test_delay,
|
||||||
|
cmds::get_app_dir,
|
||||||
|
cmds::copy_icon_file,
|
||||||
|
cmds::download_icon_cache,
|
||||||
|
cmds::open_devtools,
|
||||||
|
cmds::exit_app,
|
||||||
|
// cmds::update_hotkeys,
|
||||||
|
// profile
|
||||||
|
cmds::get_profiles,
|
||||||
|
cmds::enhance_profiles,
|
||||||
|
cmds::patch_profiles_config,
|
||||||
|
cmds::view_profile,
|
||||||
|
cmds::patch_profile,
|
||||||
|
cmds::create_profile,
|
||||||
|
cmds::import_profile,
|
||||||
|
cmds::reorder_profile,
|
||||||
|
cmds::update_profile,
|
||||||
|
cmds::delete_profile,
|
||||||
|
cmds::read_profile_file,
|
||||||
|
cmds::save_profile_file,
|
||||||
|
// service mode
|
||||||
|
cmds::service::check_service,
|
||||||
|
cmds::service::install_service,
|
||||||
|
cmds::service::uninstall_service,
|
||||||
|
// clash api
|
||||||
|
cmds::clash_api_get_proxy_delay
|
||||||
|
]);
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
use tauri::{Menu, MenuItem, Submenu};
|
||||||
|
|
||||||
|
builder = builder.menu(
|
||||||
|
Menu::new().add_submenu(Submenu::new(
|
||||||
|
"Edit",
|
||||||
|
Menu::new()
|
||||||
|
.add_native_item(MenuItem::Undo)
|
||||||
|
.add_native_item(MenuItem::Redo)
|
||||||
|
.add_native_item(MenuItem::Copy)
|
||||||
|
.add_native_item(MenuItem::Paste)
|
||||||
|
.add_native_item(MenuItem::Cut)
|
||||||
|
.add_native_item(MenuItem::SelectAll)
|
||||||
|
.add_native_item(MenuItem::CloseWindow)
|
||||||
|
.add_native_item(MenuItem::Quit),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let app = builder
|
||||||
|
.build(tauri::generate_context!())
|
||||||
|
.expect("error while running tauri application");
|
||||||
|
|
||||||
|
app.run(|app_handle, e| match e {
|
||||||
|
tauri::RunEvent::ExitRequested { api, .. } => {
|
||||||
|
api.prevent_exit();
|
||||||
}
|
}
|
||||||
"system_proxy" => {
|
tauri::RunEvent::Updater(tauri::UpdaterEvent::Downloaded) => {
|
||||||
let verge_state = app_handle.state::<states::VergeState>();
|
resolve::resolve_reset();
|
||||||
let mut verge = verge_state.0.lock().unwrap();
|
api::process::kill_children();
|
||||||
|
}
|
||||||
let old_value = verge.config.enable_system_proxy.clone().unwrap_or(false);
|
tauri::RunEvent::WindowEvent { label, event, .. } => {
|
||||||
let new_value = !old_value;
|
if label == "main" {
|
||||||
|
match event {
|
||||||
match verge.patch_config(VergeConfig {
|
tauri::WindowEvent::Destroyed => {
|
||||||
enable_system_proxy: Some(new_value),
|
let _ = resolve::save_window_size_position(app_handle, true);
|
||||||
..VergeConfig::default()
|
}
|
||||||
}) {
|
tauri::WindowEvent::CloseRequested { .. } => {
|
||||||
Ok(_) => {
|
let _ = resolve::save_window_size_position(app_handle, true);
|
||||||
app_handle
|
}
|
||||||
.tray_handle()
|
tauri::WindowEvent::Moved(_) | tauri::WindowEvent::Resized(_) => {
|
||||||
.get_item(id.as_str())
|
let _ = resolve::save_window_size_position(app_handle, false);
|
||||||
.set_selected(new_value)
|
}
|
||||||
.unwrap();
|
_ => {}
|
||||||
|
}
|
||||||
// update verge config
|
|
||||||
let window = app_handle.get_window("main").unwrap();
|
|
||||||
window.emit("verge://refresh-verge-config", "yes").unwrap();
|
|
||||||
}
|
}
|
||||||
Err(err) => log::error!("{err}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"restart_clash" => {
|
|
||||||
let clash_state = app_handle.state::<states::ClashState>();
|
|
||||||
let profiles_state = app_handle.state::<states::ProfilesState>();
|
|
||||||
let mut clash = clash_state.0.lock().unwrap();
|
|
||||||
let mut profiles = profiles_state.0.lock().unwrap();
|
|
||||||
|
|
||||||
crate::log_if_err!(clash.restart_sidecar(&mut profiles));
|
|
||||||
}
|
|
||||||
"quit" => {
|
|
||||||
resolve::resolve_reset(app_handle);
|
|
||||||
api::process::kill_children();
|
|
||||||
std::process::exit(0);
|
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
SystemTrayEvent::LeftClick { .. } => {
|
|
||||||
let window = app_handle.get_window("main").unwrap();
|
|
||||||
window.unminimize().unwrap();
|
|
||||||
window.show().unwrap();
|
|
||||||
window.set_focus().unwrap();
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
})
|
|
||||||
.invoke_handler(tauri::generate_handler![
|
|
||||||
// common
|
|
||||||
cmds::restart_sidecar,
|
|
||||||
cmds::get_sys_proxy,
|
|
||||||
cmds::get_cur_proxy,
|
|
||||||
cmds::kill_sidecars,
|
|
||||||
cmds::open_app_dir,
|
|
||||||
cmds::open_logs_dir,
|
|
||||||
// clash
|
|
||||||
cmds::get_clash_info,
|
|
||||||
cmds::patch_clash_config,
|
|
||||||
// verge
|
|
||||||
cmds::get_verge_config,
|
|
||||||
cmds::patch_verge_config,
|
|
||||||
// profile
|
|
||||||
cmds::view_profile,
|
|
||||||
cmds::patch_profile,
|
|
||||||
cmds::create_profile,
|
|
||||||
cmds::import_profile,
|
|
||||||
cmds::update_profile,
|
|
||||||
cmds::delete_profile,
|
|
||||||
cmds::select_profile,
|
|
||||||
cmds::get_profiles,
|
|
||||||
cmds::sync_profiles,
|
|
||||||
cmds::enhance_profiles,
|
|
||||||
cmds::change_profile_chain,
|
|
||||||
cmds::read_profile_file,
|
|
||||||
cmds::save_profile_file
|
|
||||||
]);
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
{
|
|
||||||
use tauri::{Menu, MenuItem, Submenu};
|
|
||||||
|
|
||||||
let submenu_file = Submenu::new(
|
|
||||||
"File",
|
|
||||||
Menu::new()
|
|
||||||
.add_native_item(MenuItem::Undo)
|
|
||||||
.add_native_item(MenuItem::Redo)
|
|
||||||
.add_native_item(MenuItem::Copy)
|
|
||||||
.add_native_item(MenuItem::Paste)
|
|
||||||
.add_native_item(MenuItem::Cut)
|
|
||||||
.add_native_item(MenuItem::SelectAll),
|
|
||||||
);
|
|
||||||
builder = builder.menu(Menu::new().add_submenu(submenu_file));
|
|
||||||
}
|
|
||||||
|
|
||||||
builder
|
|
||||||
.build(tauri::generate_context!())
|
|
||||||
.expect("error while running tauri application")
|
|
||||||
.run(|app_handle, e| match e {
|
|
||||||
tauri::RunEvent::WindowEvent { label, event, .. } => match event {
|
|
||||||
tauri::WindowEvent::CloseRequested { api, .. } => {
|
|
||||||
let app_handle = app_handle.clone();
|
|
||||||
api.prevent_close();
|
|
||||||
app_handle.get_window(&label).unwrap().hide().unwrap();
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
},
|
|
||||||
tauri::RunEvent::ExitRequested { .. } => {
|
|
||||||
resolve::resolve_reset(app_handle);
|
|
||||||
api::process::kill_children();
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
use crate::core::{Clash, Profiles, Verge};
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct ProfilesState(pub Arc<Mutex<Profiles>>);
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct ClashState(pub Arc<Mutex<Clash>>);
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct VergeState(pub Arc<Mutex<Verge>>);
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
use anyhow::{Context, Result};
|
|
||||||
use serde::{de::DeserializeOwned, Serialize};
|
|
||||||
use std::{fs, path::PathBuf};
|
|
||||||
|
|
||||||
/// read data from yaml as struct T
|
|
||||||
pub fn read_yaml<T: DeserializeOwned + Default>(path: PathBuf) -> T {
|
|
||||||
let yaml_str = fs::read_to_string(path).unwrap_or("".into());
|
|
||||||
serde_yaml::from_str::<T>(&yaml_str).unwrap_or(T::default())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// save the data to the file
|
|
||||||
/// can set `prefix` string to add some comments
|
|
||||||
pub fn save_yaml<T: Serialize>(path: PathBuf, data: &T, prefix: Option<&str>) -> Result<()> {
|
|
||||||
let data_str = serde_yaml::to_string(data)?;
|
|
||||||
|
|
||||||
let yaml_str = match prefix {
|
|
||||||
Some(prefix) => format!("{prefix}{data_str}"),
|
|
||||||
None => data_str,
|
|
||||||
};
|
|
||||||
|
|
||||||
let path_str = path.as_os_str().to_string_lossy().to_string();
|
|
||||||
fs::write(path, yaml_str.as_bytes()).context(format!("failed to save file \"{path_str}\""))
|
|
||||||
}
|
|
||||||
@@ -1,61 +1,125 @@
|
|||||||
use std::env::temp_dir;
|
use crate::core::handle;
|
||||||
use std::path::{Path, PathBuf};
|
use anyhow::Result;
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
|
use std::path::PathBuf;
|
||||||
use tauri::{
|
use tauri::{
|
||||||
api::path::{home_dir, resource_dir},
|
api::path::{data_dir, resource_dir},
|
||||||
Env, PackageInfo,
|
Env,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(not(feature = "verge-dev"))]
|
#[cfg(not(feature = "verge-dev"))]
|
||||||
static APP_DIR: &str = "clash-verge";
|
pub static APP_ID: &str = "io.github.clash-verge-rev.clash-verge-rev";
|
||||||
#[cfg(feature = "verge-dev")]
|
#[cfg(feature = "verge-dev")]
|
||||||
static APP_DIR: &str = "clash-verge-dev";
|
pub static APP_ID: &str = "io.github.clash-verge-rev.clash-verge-rev.dev";
|
||||||
|
|
||||||
|
pub static PORTABLE_FLAG: OnceCell<bool> = OnceCell::new();
|
||||||
|
|
||||||
static CLASH_CONFIG: &str = "config.yaml";
|
static CLASH_CONFIG: &str = "config.yaml";
|
||||||
static VERGE_CONFIG: &str = "verge.yaml";
|
static VERGE_CONFIG: &str = "verge.yaml";
|
||||||
static PROFILE_YAML: &str = "profiles.yaml";
|
static PROFILE_YAML: &str = "profiles.yaml";
|
||||||
static PROFILE_TEMP: &str = "clash-verge-runtime.yaml";
|
|
||||||
|
/// init portable flag
|
||||||
|
pub fn init_portable_flag() -> Result<()> {
|
||||||
|
use tauri::utils::platform::current_exe;
|
||||||
|
|
||||||
|
let app_exe = current_exe()?;
|
||||||
|
if let Some(dir) = app_exe.parent() {
|
||||||
|
let dir = PathBuf::from(dir).join(".config/PORTABLE");
|
||||||
|
|
||||||
|
if dir.exists() {
|
||||||
|
PORTABLE_FLAG.get_or_init(|| true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PORTABLE_FLAG.get_or_init(|| false);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// get the verge app home dir
|
/// get the verge app home dir
|
||||||
pub fn app_home_dir() -> PathBuf {
|
pub fn app_home_dir() -> Result<PathBuf> {
|
||||||
home_dir()
|
use tauri::utils::platform::current_exe;
|
||||||
.unwrap()
|
|
||||||
.join(Path::new(".config"))
|
let flag = PORTABLE_FLAG.get().unwrap_or(&false);
|
||||||
.join(Path::new(APP_DIR))
|
if *flag {
|
||||||
|
let app_exe = current_exe()?;
|
||||||
|
let app_exe = dunce::canonicalize(app_exe)?;
|
||||||
|
let app_dir = app_exe
|
||||||
|
.parent()
|
||||||
|
.ok_or(anyhow::anyhow!("failed to get the portable app dir"))?;
|
||||||
|
return Ok(PathBuf::from(app_dir).join(".config").join(APP_ID));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(data_dir()
|
||||||
|
.ok_or(anyhow::anyhow!("failed to get app home dir"))?
|
||||||
|
.join(APP_ID))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// get the resources dir
|
/// get the resources dir
|
||||||
pub fn app_resources_dir(package_info: &PackageInfo) -> PathBuf {
|
pub fn app_resources_dir() -> Result<PathBuf> {
|
||||||
resource_dir(package_info, &Env::default())
|
let handle = handle::Handle::global();
|
||||||
.unwrap()
|
let app_handle = handle.app_handle.lock();
|
||||||
.join("resources")
|
if let Some(app_handle) = app_handle.as_ref() {
|
||||||
|
let res_dir = resource_dir(app_handle.package_info(), &Env::default())
|
||||||
|
.ok_or(anyhow::anyhow!("failed to get the resource dir"))?
|
||||||
|
.join("resources");
|
||||||
|
return Ok(res_dir);
|
||||||
|
};
|
||||||
|
Err(anyhow::anyhow!("failed to get the resource dir"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// profiles dir
|
/// profiles dir
|
||||||
pub fn app_profiles_dir() -> PathBuf {
|
pub fn app_profiles_dir() -> Result<PathBuf> {
|
||||||
app_home_dir().join("profiles")
|
Ok(app_home_dir()?.join("profiles"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// logs dir
|
/// logs dir
|
||||||
pub fn app_logs_dir() -> PathBuf {
|
pub fn app_logs_dir() -> Result<PathBuf> {
|
||||||
app_home_dir().join("logs")
|
Ok(app_home_dir()?.join("logs"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clash_path() -> PathBuf {
|
pub fn clash_path() -> Result<PathBuf> {
|
||||||
app_home_dir().join(CLASH_CONFIG)
|
Ok(app_home_dir()?.join(CLASH_CONFIG))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn verge_path() -> PathBuf {
|
pub fn verge_path() -> Result<PathBuf> {
|
||||||
app_home_dir().join(VERGE_CONFIG)
|
Ok(app_home_dir()?.join(VERGE_CONFIG))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn profiles_path() -> PathBuf {
|
pub fn profiles_path() -> Result<PathBuf> {
|
||||||
app_home_dir().join(PROFILE_YAML)
|
Ok(app_home_dir()?.join(PROFILE_YAML))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn profiles_temp_path() -> PathBuf {
|
pub fn clash_pid_path() -> Result<PathBuf> {
|
||||||
#[cfg(not(feature = "debug-yml"))]
|
Ok(app_home_dir()?.join("clash.pid"))
|
||||||
return temp_dir().join(PROFILE_TEMP);
|
}
|
||||||
|
|
||||||
#[cfg(feature = "debug-yml")]
|
#[cfg(not(target_os = "windows"))]
|
||||||
return app_home_dir().join(PROFILE_TEMP);
|
pub fn service_path() -> Result<PathBuf> {
|
||||||
|
Ok(app_resources_dir()?.join("clash-verge-service"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub fn service_path() -> Result<PathBuf> {
|
||||||
|
Ok(app_resources_dir()?.join("clash-verge-service.exe"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn service_log_file() -> Result<PathBuf> {
|
||||||
|
use chrono::Local;
|
||||||
|
|
||||||
|
let log_dir = app_logs_dir()?.join("service");
|
||||||
|
|
||||||
|
let local_time = Local::now().format("%Y-%m-%d-%H%M").to_string();
|
||||||
|
let log_file = format!("{}.log", local_time);
|
||||||
|
let log_file = log_dir.join(log_file);
|
||||||
|
|
||||||
|
let _ = std::fs::create_dir_all(&log_dir);
|
||||||
|
|
||||||
|
Ok(log_file)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn path_to_str(path: &PathBuf) -> Result<&str> {
|
||||||
|
let path_str = path
|
||||||
|
.as_os_str()
|
||||||
|
.to_str()
|
||||||
|
.ok_or(anyhow::anyhow!("failed to get path from {:?}", path))?;
|
||||||
|
Ok(path_str)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,94 +1,188 @@
|
|||||||
|
use anyhow::{anyhow, bail, Context, Result};
|
||||||
use nanoid::nanoid;
|
use nanoid::nanoid;
|
||||||
use std::str::FromStr;
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use serde_yaml::{Mapping, Value};
|
||||||
|
use std::{fs, path::PathBuf, str::FromStr};
|
||||||
|
use tauri::{
|
||||||
|
api::shell::{open, Program},
|
||||||
|
Manager,
|
||||||
|
};
|
||||||
|
|
||||||
pub fn get_now() -> usize {
|
/// read data from yaml as struct T
|
||||||
SystemTime::now()
|
pub fn read_yaml<T: DeserializeOwned>(path: &PathBuf) -> Result<T> {
|
||||||
.duration_since(UNIX_EPOCH)
|
if !path.exists() {
|
||||||
.unwrap()
|
bail!("file not found \"{}\"", path.display());
|
||||||
.as_secs() as _
|
}
|
||||||
|
|
||||||
|
let yaml_str = fs::read_to_string(path)
|
||||||
|
.with_context(|| format!("failed to read the file \"{}\"", path.display()))?;
|
||||||
|
|
||||||
|
serde_yaml::from_str::<T>(&yaml_str).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"failed to read the file with yaml format \"{}\"",
|
||||||
|
path.display()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// read mapping from yaml fix #165
|
||||||
|
pub fn read_merge_mapping(path: &PathBuf) -> Result<Mapping> {
|
||||||
|
let mut val: Value = read_yaml(path)?;
|
||||||
|
val.apply_merge()
|
||||||
|
.with_context(|| format!("failed to apply merge \"{}\"", path.display()))?;
|
||||||
|
|
||||||
|
Ok(val
|
||||||
|
.as_mapping()
|
||||||
|
.ok_or(anyhow!(
|
||||||
|
"failed to transform to yaml mapping \"{}\"",
|
||||||
|
path.display()
|
||||||
|
))?
|
||||||
|
.to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// save the data to the file
|
||||||
|
/// can set `prefix` string to add some comments
|
||||||
|
pub fn save_yaml<T: Serialize>(path: &PathBuf, data: &T, prefix: Option<&str>) -> Result<()> {
|
||||||
|
let data_str = serde_yaml::to_string(data)?;
|
||||||
|
|
||||||
|
let yaml_str = match prefix {
|
||||||
|
Some(prefix) => format!("{prefix}\n\n{data_str}"),
|
||||||
|
None => data_str,
|
||||||
|
};
|
||||||
|
|
||||||
|
let path_str = path.as_os_str().to_string_lossy().to_string();
|
||||||
|
fs::write(path, yaml_str.as_bytes())
|
||||||
|
.with_context(|| format!("failed to save file \"{path_str}\""))
|
||||||
}
|
}
|
||||||
|
|
||||||
const ALPHABET: [char; 62] = [
|
const ALPHABET: [char; 62] = [
|
||||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i',
|
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i',
|
||||||
'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B',
|
'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B',
|
||||||
'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U',
|
'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U',
|
||||||
'V', 'W', 'X', 'Y', 'Z',
|
'V', 'W', 'X', 'Y', 'Z',
|
||||||
];
|
];
|
||||||
|
|
||||||
/// generate the uid
|
/// generate the uid
|
||||||
pub fn get_uid(prefix: &str) -> String {
|
pub fn get_uid(prefix: &str) -> String {
|
||||||
let id = nanoid!(11, &ALPHABET);
|
let id = nanoid!(11, &ALPHABET);
|
||||||
format!("{prefix}{id}")
|
format!("{prefix}{id}")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// parse the string
|
/// parse the string
|
||||||
/// xxx=123123; => 123123
|
/// xxx=123123; => 123123
|
||||||
pub fn parse_str<T: FromStr>(target: &str, key: &str) -> Option<T> {
|
pub fn parse_str<T: FromStr>(target: &str, key: &str) -> Option<T> {
|
||||||
match target.find(key) {
|
target.split(';').map(str::trim).find_map(|s| {
|
||||||
Some(idx) => {
|
let mut parts = s.splitn(2, '=');
|
||||||
let idx = idx + key.len();
|
match (parts.next(), parts.next()) {
|
||||||
let value = &target[idx..];
|
(Some(k), Some(v)) if k == key => v.parse::<T>().ok(),
|
||||||
match match value.split(';').nth(0) {
|
_ => None,
|
||||||
Some(value) => value.trim().parse(),
|
}
|
||||||
None => value.trim().parse(),
|
})
|
||||||
} {
|
}
|
||||||
Ok(r) => Some(r),
|
|
||||||
Err(_) => None,
|
/// get the last part of the url, if not found, return empty string
|
||||||
}
|
pub fn get_last_part_and_decode(url: &str) -> Option<String> {
|
||||||
}
|
let path = url.split('?').next().unwrap_or(""); // Splits URL and takes the path part
|
||||||
None => None,
|
let segments: Vec<&str> = path.split('/').collect();
|
||||||
}
|
let last_segment = segments.last()?;
|
||||||
|
|
||||||
|
Some(
|
||||||
|
percent_encoding::percent_decode_str(last_segment)
|
||||||
|
.decode_utf8_lossy()
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// open file
|
||||||
|
/// use vscode by default
|
||||||
|
pub fn open_file(app: tauri::AppHandle, path: PathBuf) -> Result<()> {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
let code = "Visual Studio Code";
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
let code = "code";
|
||||||
|
|
||||||
|
let _ = match Program::from_str(code) {
|
||||||
|
Ok(code) => open(&app.shell_scope(), path.to_string_lossy(), Some(code)),
|
||||||
|
Err(err) => {
|
||||||
|
log::error!(target: "app", "Can't find VScode `{err}`");
|
||||||
|
// default open
|
||||||
|
open(&app.shell_scope(), path.to_string_lossy(), None)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! log_if_err {
|
macro_rules! error {
|
||||||
($result: expr) => {
|
($result: expr) => {
|
||||||
if let Err(err) = $result {
|
log::error!(target: "app", "{}", $result);
|
||||||
log::error!("{err}");
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! log_err {
|
||||||
|
($result: expr) => {
|
||||||
|
if let Err(err) = $result {
|
||||||
|
log::error!(target: "app", "{err}");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
($result: expr, $err_str: expr) => {
|
||||||
|
if let Err(_) = $result {
|
||||||
|
log::error!(target: "app", "{}", $err_str);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! trace_err {
|
||||||
|
($result: expr, $err_str: expr) => {
|
||||||
|
if let Err(err) = $result {
|
||||||
|
log::trace!(target: "app", "{}, err {}", $err_str, err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// wrap the anyhow error
|
/// wrap the anyhow error
|
||||||
/// transform the error to String
|
/// transform the error to String
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! wrap_err {
|
macro_rules! wrap_err {
|
||||||
($stat: expr) => {
|
($stat: expr) => {
|
||||||
match $stat {
|
match $stat {
|
||||||
Ok(a) => Ok(a),
|
Ok(a) => Ok(a),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::error!("{}", err.to_string());
|
log::error!(target: "app", "{}", err.to_string());
|
||||||
Err(format!("{}", err.to_string()))
|
Err(format!("{}", err.to_string()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// return the string literal error
|
/// return the string literal error
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! ret_err {
|
macro_rules! ret_err {
|
||||||
($str: expr) => {
|
($str: expr) => {
|
||||||
return Err($str.into())
|
return Err($str.into())
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_value() {
|
fn test_parse_value() {
|
||||||
let test_1 = "upload=111; download=2222; total=3333; expire=444";
|
let test_1 = "upload=111; download=2222; total=3333; expire=444";
|
||||||
let test_2 = "attachment; filename=Clash.yaml";
|
let test_2 = "attachment; filename=Clash.yaml";
|
||||||
|
|
||||||
assert_eq!(parse_str::<usize>(test_1, "upload=").unwrap(), 111);
|
assert_eq!(parse_str::<usize>(test_1, "upload").unwrap(), 111);
|
||||||
assert_eq!(parse_str::<usize>(test_1, "download=").unwrap(), 2222);
|
assert_eq!(parse_str::<usize>(test_1, "download").unwrap(), 2222);
|
||||||
assert_eq!(parse_str::<usize>(test_1, "total=").unwrap(), 3333);
|
assert_eq!(parse_str::<usize>(test_1, "total").unwrap(), 3333);
|
||||||
assert_eq!(parse_str::<usize>(test_1, "expire=").unwrap(), 444);
|
assert_eq!(parse_str::<usize>(test_1, "expire").unwrap(), 444);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_str::<String>(test_2, "filename=").unwrap(),
|
parse_str::<String>(test_2, "filename").unwrap(),
|
||||||
format!("Clash.yaml")
|
format!("Clash.yaml")
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(parse_str::<usize>(test_1, "aaa="), None);
|
assert_eq!(parse_str::<usize>(test_1, "aaa"), None);
|
||||||
assert_eq!(parse_str::<usize>(test_1, "upload1="), None);
|
assert_eq!(parse_str::<usize>(test_1, "upload1"), None);
|
||||||
assert_eq!(parse_str::<usize>(test_1, "expire1="), None);
|
assert_eq!(parse_str::<usize>(test_1, "expire1"), None);
|
||||||
assert_eq!(parse_str::<usize>(test_2, "attachment="), None);
|
assert_eq!(parse_str::<usize>(test_2, "attachment"), None);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,100 +1,330 @@
|
|||||||
use crate::utils::{dirs, tmpl};
|
use crate::config::*;
|
||||||
use chrono::Local;
|
use crate::utils::{dirs, help};
|
||||||
|
use anyhow::Result;
|
||||||
|
use chrono::{Local, TimeZone};
|
||||||
use log::LevelFilter;
|
use log::LevelFilter;
|
||||||
use log4rs::append::console::ConsoleAppender;
|
use log4rs::append::console::ConsoleAppender;
|
||||||
use log4rs::append::file::FileAppender;
|
use log4rs::append::file::FileAppender;
|
||||||
use log4rs::config::{Appender, Config, Root};
|
use log4rs::config::{Appender, Logger, Root};
|
||||||
use log4rs::encode::pattern::PatternEncoder;
|
use log4rs::encode::pattern::PatternEncoder;
|
||||||
use std::fs;
|
use std::fs::{self, DirEntry};
|
||||||
use std::io::Write;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use tauri::PackageInfo;
|
use std::str::FromStr;
|
||||||
|
use tauri::api::process::Command;
|
||||||
|
|
||||||
/// initialize this instance's log file
|
/// initialize this instance's log file
|
||||||
fn init_log(log_dir: &PathBuf) {
|
fn init_log() -> Result<()> {
|
||||||
let local_time = Local::now().format("%Y-%m-%d-%H%M%S").to_string();
|
let log_dir = dirs::app_logs_dir()?;
|
||||||
let log_file = format!("{}.log", local_time);
|
if !log_dir.exists() {
|
||||||
let log_file = log_dir.join(log_file);
|
let _ = fs::create_dir_all(&log_dir);
|
||||||
|
|
||||||
let time_format = "{d(%Y-%m-%d %H:%M:%S)} - {m}{n}";
|
|
||||||
let stdout = ConsoleAppender::builder()
|
|
||||||
.encoder(Box::new(PatternEncoder::new(time_format)))
|
|
||||||
.build();
|
|
||||||
let tofile = FileAppender::builder()
|
|
||||||
.encoder(Box::new(PatternEncoder::new(time_format)))
|
|
||||||
.build(log_file)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let config = Config::builder()
|
|
||||||
.appender(Appender::builder().build("stdout", Box::new(stdout)))
|
|
||||||
.appender(Appender::builder().build("file", Box::new(tofile)))
|
|
||||||
.build(
|
|
||||||
Root::builder()
|
|
||||||
.appenders(["stdout", "file"])
|
|
||||||
.build(LevelFilter::Info),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
log4rs::init_config(config).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Initialize all the files from resources
|
|
||||||
fn init_config(app_dir: &PathBuf) -> std::io::Result<()> {
|
|
||||||
// target path
|
|
||||||
let clash_path = app_dir.join("config.yaml");
|
|
||||||
let verge_path = app_dir.join("verge.yaml");
|
|
||||||
let profile_path = app_dir.join("profiles.yaml");
|
|
||||||
|
|
||||||
if !clash_path.exists() {
|
|
||||||
fs::File::create(clash_path)?.write(tmpl::CLASH_CONFIG)?;
|
|
||||||
}
|
|
||||||
if !verge_path.exists() {
|
|
||||||
fs::File::create(verge_path)?.write(tmpl::VERGE_CONFIG)?;
|
|
||||||
}
|
|
||||||
if !profile_path.exists() {
|
|
||||||
fs::File::create(profile_path)?.write(tmpl::PROFILES_CONFIG)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// initialize app
|
|
||||||
pub fn init_app(package_info: &PackageInfo) {
|
|
||||||
// create app dir
|
|
||||||
let app_dir = dirs::app_home_dir();
|
|
||||||
let log_dir = dirs::app_logs_dir();
|
|
||||||
let profiles_dir = dirs::app_profiles_dir();
|
|
||||||
|
|
||||||
let res_dir = dirs::app_resources_dir(package_info);
|
|
||||||
|
|
||||||
if !app_dir.exists() {
|
|
||||||
fs::create_dir_all(&app_dir).unwrap();
|
|
||||||
}
|
|
||||||
if !log_dir.exists() {
|
|
||||||
fs::create_dir_all(&log_dir).unwrap();
|
|
||||||
}
|
|
||||||
if !profiles_dir.exists() {
|
|
||||||
fs::create_dir_all(&profiles_dir).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
init_log(&log_dir);
|
|
||||||
if let Err(err) = init_config(&app_dir) {
|
|
||||||
log::error!("{err}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// copy the resource file
|
|
||||||
let mmdb_path = app_dir.join("Country.mmdb");
|
|
||||||
let mmdb_tmpl = res_dir.join("Country.mmdb");
|
|
||||||
if !mmdb_path.exists() && mmdb_tmpl.exists() {
|
|
||||||
fs::copy(mmdb_tmpl, mmdb_path).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// copy the wintun.dll
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{
|
|
||||||
let wintun_path = app_dir.join("wintun.dll");
|
|
||||||
let wintun_tmpl = res_dir.join("wintun.dll");
|
|
||||||
if !wintun_path.exists() && wintun_tmpl.exists() {
|
|
||||||
fs::copy(wintun_tmpl, wintun_path).unwrap();
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
let log_level = Config::verge().data().get_log_level();
|
||||||
|
if log_level == LevelFilter::Off {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let local_time = Local::now().format("%Y-%m-%d-%H%M").to_string();
|
||||||
|
let log_file = format!("{}.log", local_time);
|
||||||
|
let log_file = log_dir.join(log_file);
|
||||||
|
|
||||||
|
let log_pattern = match log_level {
|
||||||
|
LevelFilter::Trace => "{d(%Y-%m-%d %H:%M:%S)} {l} [{M}] - {m}{n}",
|
||||||
|
_ => "{d(%Y-%m-%d %H:%M:%S)} {l} - {m}{n}",
|
||||||
|
};
|
||||||
|
|
||||||
|
let encode = Box::new(PatternEncoder::new(log_pattern));
|
||||||
|
|
||||||
|
let stdout = ConsoleAppender::builder().encoder(encode.clone()).build();
|
||||||
|
let tofile = FileAppender::builder().encoder(encode).build(log_file)?;
|
||||||
|
|
||||||
|
let mut logger_builder = Logger::builder();
|
||||||
|
let mut root_builder = Root::builder();
|
||||||
|
|
||||||
|
let log_more = log_level == LevelFilter::Trace || log_level == LevelFilter::Debug;
|
||||||
|
|
||||||
|
#[cfg(feature = "verge-dev")]
|
||||||
|
{
|
||||||
|
logger_builder = logger_builder.appenders(["file", "stdout"]);
|
||||||
|
if log_more {
|
||||||
|
root_builder = root_builder.appenders(["file", "stdout"]);
|
||||||
|
} else {
|
||||||
|
root_builder = root_builder.appenders(["stdout"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "verge-dev"))]
|
||||||
|
{
|
||||||
|
logger_builder = logger_builder.appenders(["file"]);
|
||||||
|
if log_more {
|
||||||
|
root_builder = root_builder.appenders(["file"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (config, _) = log4rs::config::Config::builder()
|
||||||
|
.appender(Appender::builder().build("stdout", Box::new(stdout)))
|
||||||
|
.appender(Appender::builder().build("file", Box::new(tofile)))
|
||||||
|
.logger(logger_builder.additive(false).build("app", log_level))
|
||||||
|
.build_lossy(root_builder.build(log_level));
|
||||||
|
|
||||||
|
log4rs::init_config(config)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 删除log文件
|
||||||
|
pub fn delete_log() -> Result<()> {
|
||||||
|
let log_dir = dirs::app_logs_dir()?;
|
||||||
|
if !log_dir.exists() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let auto_log_clean = {
|
||||||
|
let verge = Config::verge();
|
||||||
|
let verge = verge.data();
|
||||||
|
verge.auto_log_clean.unwrap_or(0)
|
||||||
|
};
|
||||||
|
|
||||||
|
let day = match auto_log_clean {
|
||||||
|
1 => 7,
|
||||||
|
2 => 30,
|
||||||
|
3 => 90,
|
||||||
|
_ => return Ok(()),
|
||||||
|
};
|
||||||
|
|
||||||
|
log::debug!(target: "app", "try to delete log files, day: {day}");
|
||||||
|
|
||||||
|
// %Y-%m-%d to NaiveDateTime
|
||||||
|
let parse_time_str = |s: &str| {
|
||||||
|
let sa: Vec<&str> = s.split('-').collect();
|
||||||
|
if sa.len() != 4 {
|
||||||
|
return Err(anyhow::anyhow!("invalid time str"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let year = i32::from_str(sa[0])?;
|
||||||
|
let month = u32::from_str(sa[1])?;
|
||||||
|
let day = u32::from_str(sa[2])?;
|
||||||
|
let time = chrono::NaiveDate::from_ymd_opt(year, month, day)
|
||||||
|
.ok_or(anyhow::anyhow!("invalid time str"))?
|
||||||
|
.and_hms_opt(0, 0, 0)
|
||||||
|
.ok_or(anyhow::anyhow!("invalid time str"))?;
|
||||||
|
Ok(time)
|
||||||
|
};
|
||||||
|
|
||||||
|
let process_file = |file: DirEntry| -> Result<()> {
|
||||||
|
let file_name = file.file_name();
|
||||||
|
let file_name = file_name.to_str().unwrap_or_default();
|
||||||
|
|
||||||
|
if file_name.ends_with(".log") {
|
||||||
|
let now = Local::now();
|
||||||
|
let created_time = parse_time_str(&file_name[0..file_name.len() - 4])?;
|
||||||
|
let file_time = Local
|
||||||
|
.from_local_datetime(&created_time)
|
||||||
|
.single()
|
||||||
|
.ok_or(anyhow::anyhow!("invalid local datetime"))?;
|
||||||
|
|
||||||
|
let duration = now.signed_duration_since(file_time);
|
||||||
|
if duration.num_days() > day {
|
||||||
|
let file_path = file.path();
|
||||||
|
let _ = fs::remove_file(file_path);
|
||||||
|
log::info!(target: "app", "delete log file: {file_name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
};
|
||||||
|
|
||||||
|
for file in fs::read_dir(&log_dir)?.flatten() {
|
||||||
|
let _ = process_file(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
let service_log_dir = log_dir.join("service");
|
||||||
|
for file in fs::read_dir(&service_log_dir)?.flatten() {
|
||||||
|
let _ = process_file(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize all the config files
|
||||||
|
/// before tauri setup
|
||||||
|
pub fn init_config() -> Result<()> {
|
||||||
|
let _ = dirs::init_portable_flag();
|
||||||
|
let _ = init_log();
|
||||||
|
let _ = delete_log();
|
||||||
|
|
||||||
|
crate::log_err!(dirs::app_home_dir().map(|app_dir| {
|
||||||
|
if !app_dir.exists() {
|
||||||
|
let _ = fs::create_dir_all(&app_dir);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
crate::log_err!(dirs::app_profiles_dir().map(|profiles_dir| {
|
||||||
|
if !profiles_dir.exists() {
|
||||||
|
let _ = fs::create_dir_all(&profiles_dir);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
crate::log_err!(dirs::clash_path().map(|path| {
|
||||||
|
if !path.exists() {
|
||||||
|
help::save_yaml(&path, &IClashTemp::template().0, Some("# Clash Vergeasu"))?;
|
||||||
|
}
|
||||||
|
<Result<()>>::Ok(())
|
||||||
|
}));
|
||||||
|
|
||||||
|
crate::log_err!(dirs::verge_path().map(|path| {
|
||||||
|
if !path.exists() {
|
||||||
|
help::save_yaml(&path, &IVerge::template(), Some("# Clash Verge"))?;
|
||||||
|
}
|
||||||
|
<Result<()>>::Ok(())
|
||||||
|
}));
|
||||||
|
|
||||||
|
crate::log_err!(dirs::profiles_path().map(|path| {
|
||||||
|
if !path.exists() {
|
||||||
|
help::save_yaml(&path, &IProfiles::template(), Some("# Clash Verge"))?;
|
||||||
|
}
|
||||||
|
<Result<()>>::Ok(())
|
||||||
|
}));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// initialize app resources
|
||||||
|
/// after tauri setup
|
||||||
|
pub fn init_resources() -> Result<()> {
|
||||||
|
let app_dir = dirs::app_home_dir()?;
|
||||||
|
let res_dir = dirs::app_resources_dir()?;
|
||||||
|
|
||||||
|
if !app_dir.exists() {
|
||||||
|
let _ = fs::create_dir_all(&app_dir);
|
||||||
|
}
|
||||||
|
if !res_dir.exists() {
|
||||||
|
let _ = fs::create_dir_all(&res_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
let file_list = ["Country.mmdb", "geoip.dat", "geosite.dat"];
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
let file_list = ["Country.mmdb", "geoip.dat", "geosite.dat"];
|
||||||
|
|
||||||
|
// copy the resource file
|
||||||
|
// if the source file is newer than the destination file, copy it over
|
||||||
|
for file in file_list.iter() {
|
||||||
|
let src_path = res_dir.join(file);
|
||||||
|
let dest_path = app_dir.join(file);
|
||||||
|
|
||||||
|
let handle_copy = || {
|
||||||
|
match fs::copy(&src_path, &dest_path) {
|
||||||
|
Ok(_) => log::debug!(target: "app", "resources copied '{file}'"),
|
||||||
|
Err(err) => {
|
||||||
|
log::error!(target: "app", "failed to copy resources '{file}', {err}")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if src_path.exists() && !dest_path.exists() {
|
||||||
|
handle_copy();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let src_modified = fs::metadata(&src_path).and_then(|m| m.modified());
|
||||||
|
let dest_modified = fs::metadata(&dest_path).and_then(|m| m.modified());
|
||||||
|
|
||||||
|
match (src_modified, dest_modified) {
|
||||||
|
(Ok(src_modified), Ok(dest_modified)) => {
|
||||||
|
if src_modified > dest_modified {
|
||||||
|
handle_copy();
|
||||||
|
} else {
|
||||||
|
log::debug!(target: "app", "skipping resource copy '{file}'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
log::debug!(target: "app", "failed to get modified '{file}'");
|
||||||
|
handle_copy();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// initialize url scheme
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub fn init_scheme() -> Result<()> {
|
||||||
|
use tauri::utils::platform::current_exe;
|
||||||
|
use winreg::enums::*;
|
||||||
|
use winreg::RegKey;
|
||||||
|
|
||||||
|
let app_exe = current_exe()?;
|
||||||
|
let app_exe = dunce::canonicalize(app_exe)?;
|
||||||
|
let app_exe = app_exe.to_string_lossy().into_owned();
|
||||||
|
|
||||||
|
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
||||||
|
let (clash, _) = hkcu.create_subkey("Software\\Classes\\Clash")?;
|
||||||
|
clash.set_value("", &"Clash Verge")?;
|
||||||
|
clash.set_value("URL Protocol", &"Clash Verge URL Scheme Protocol")?;
|
||||||
|
let (default_icon, _) = hkcu.create_subkey("Software\\Classes\\Clash\\DefaultIcon")?;
|
||||||
|
default_icon.set_value("", &app_exe)?;
|
||||||
|
let (command, _) = hkcu.create_subkey("Software\\Classes\\Clash\\Shell\\Open\\Command")?;
|
||||||
|
command.set_value("", &format!("{app_exe} \"%1\""))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub fn init_scheme() -> Result<()> {
|
||||||
|
let output = std::process::Command::new("xdg-mime")
|
||||||
|
.arg("default")
|
||||||
|
.arg("clash-verge.desktop")
|
||||||
|
.arg("x-scheme-handler/clash")
|
||||||
|
.output()?;
|
||||||
|
if !output.status.success() {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"failed to set clash scheme, {}",
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
pub fn init_scheme() -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn startup_script() -> Result<()> {
|
||||||
|
let path = {
|
||||||
|
let verge = Config::verge();
|
||||||
|
let verge = verge.latest();
|
||||||
|
verge.startup_script.clone().unwrap_or("".to_string())
|
||||||
|
};
|
||||||
|
|
||||||
|
if !path.is_empty() {
|
||||||
|
let mut shell = "";
|
||||||
|
if path.ends_with(".sh") {
|
||||||
|
shell = "bash";
|
||||||
|
}
|
||||||
|
if path.ends_with(".ps1") {
|
||||||
|
shell = "powershell";
|
||||||
|
}
|
||||||
|
if path.ends_with(".bat") {
|
||||||
|
shell = "powershell";
|
||||||
|
}
|
||||||
|
if shell.is_empty() {
|
||||||
|
return Err(anyhow::anyhow!("unsupported script: {path}"));
|
||||||
|
}
|
||||||
|
let current_dir = PathBuf::from(path.clone());
|
||||||
|
if !current_dir.exists() {
|
||||||
|
return Err(anyhow::anyhow!("script not found: {path}"));
|
||||||
|
}
|
||||||
|
let current_dir = current_dir.parent();
|
||||||
|
match current_dir {
|
||||||
|
Some(dir) => {
|
||||||
|
let _ = Command::new(shell)
|
||||||
|
.current_dir(dir.to_path_buf())
|
||||||
|
.args(&[path])
|
||||||
|
.output()?;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let _ = Command::new(shell).args(&[path]).output()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
pub mod config;
|
|
||||||
pub mod dirs;
|
pub mod dirs;
|
||||||
pub mod help;
|
pub mod help;
|
||||||
pub mod init;
|
pub mod init;
|
||||||
pub mod resolve;
|
pub mod resolve;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
pub mod sysopt;
|
|
||||||
pub mod tmpl;
|
pub mod tmpl;
|
||||||
|
pub mod unix_helper;
|
||||||
|
|||||||
@@ -1,88 +1,266 @@
|
|||||||
use super::{init, server};
|
use crate::config::{IVerge, PrfOption};
|
||||||
use crate::{core::Profiles, log_if_err, states};
|
use crate::{
|
||||||
|
config::{Config, PrfItem},
|
||||||
|
core::*,
|
||||||
|
utils::init,
|
||||||
|
utils::server,
|
||||||
|
};
|
||||||
|
use crate::{log_err, trace_err};
|
||||||
|
use anyhow::Result;
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
|
use serde_yaml::Mapping;
|
||||||
|
use std::net::TcpListener;
|
||||||
|
use tauri::api::notification;
|
||||||
use tauri::{App, AppHandle, Manager};
|
use tauri::{App, AppHandle, Manager};
|
||||||
|
use window_shadows::set_shadow;
|
||||||
|
|
||||||
|
pub static VERSION: OnceCell<String> = OnceCell::new();
|
||||||
|
|
||||||
|
pub fn find_unused_port() -> Result<u16> {
|
||||||
|
match TcpListener::bind("127.0.0.1:0") {
|
||||||
|
Ok(listener) => {
|
||||||
|
let port = listener.local_addr()?.port();
|
||||||
|
Ok(port)
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
let port = Config::verge()
|
||||||
|
.latest()
|
||||||
|
.verge_mixed_port
|
||||||
|
.unwrap_or(Config::clash().data().get_mixed_port());
|
||||||
|
log::warn!(target: "app", "use default port: {}", port);
|
||||||
|
Ok(port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// handle something when start app
|
/// handle something when start app
|
||||||
pub fn resolve_setup(app: &App) {
|
pub fn resolve_setup(app: &mut App) {
|
||||||
// setup a simple http server for singleton
|
#[cfg(target_os = "macos")]
|
||||||
server::embed_server(&app.handle());
|
app.set_activation_policy(tauri::ActivationPolicy::Accessory);
|
||||||
|
let version = app.package_info().version.to_string();
|
||||||
|
handle::Handle::global().init(app.app_handle());
|
||||||
|
VERSION.get_or_init(|| version.clone());
|
||||||
|
|
||||||
// init app config
|
log_err!(init::init_resources());
|
||||||
init::init_app(app.package_info());
|
log_err!(init::init_scheme());
|
||||||
|
log_err!(init::startup_script());
|
||||||
|
// 处理随机端口
|
||||||
|
let enable_random_port = Config::verge().latest().enable_random_port.unwrap_or(false);
|
||||||
|
|
||||||
// init states
|
let mut port = Config::verge()
|
||||||
let clash_state = app.state::<states::ClashState>();
|
.latest()
|
||||||
let verge_state = app.state::<states::VergeState>();
|
.verge_mixed_port
|
||||||
let profiles_state = app.state::<states::ProfilesState>();
|
.unwrap_or(Config::clash().data().get_mixed_port());
|
||||||
|
|
||||||
let mut clash = clash_state.0.lock().unwrap();
|
if enable_random_port {
|
||||||
let mut verge = verge_state.0.lock().unwrap();
|
port = find_unused_port().unwrap_or(
|
||||||
let mut profiles = profiles_state.0.lock().unwrap();
|
Config::verge()
|
||||||
|
.latest()
|
||||||
|
.verge_mixed_port
|
||||||
|
.unwrap_or(Config::clash().data().get_mixed_port()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
log_if_err!(clash.run_sidecar());
|
Config::verge().data().patch_config(IVerge {
|
||||||
|
verge_mixed_port: Some(port),
|
||||||
|
..IVerge::default()
|
||||||
|
});
|
||||||
|
let _ = Config::verge().data().save_file();
|
||||||
|
let mut mapping = Mapping::new();
|
||||||
|
mapping.insert("mixed-port".into(), port.into());
|
||||||
|
Config::clash().data().patch_config(mapping);
|
||||||
|
let _ = Config::clash().data().save_config();
|
||||||
|
|
||||||
*profiles = Profiles::read_file();
|
// 启动核心
|
||||||
|
log::trace!("init config");
|
||||||
|
log_err!(Config::init_config());
|
||||||
|
|
||||||
clash.set_window(app.get_window("main"));
|
log::trace!("launch core");
|
||||||
log_if_err!(clash.activate(&profiles));
|
log_err!(CoreManager::global().init());
|
||||||
log_if_err!(clash.activate_enhanced(&profiles, true, true));
|
|
||||||
|
|
||||||
verge.init_sysproxy(clash.info.port.clone());
|
// setup a simple http server for singleton
|
||||||
|
log::trace!("launch embed server");
|
||||||
|
server::embed_server(app.app_handle());
|
||||||
|
|
||||||
log_if_err!(verge.init_launch());
|
log::trace!("init system tray");
|
||||||
|
log_err!(tray::Tray::update_systray(&app.app_handle()));
|
||||||
|
|
||||||
verge.config.enable_system_proxy.map(|enable| {
|
let silent_start = { Config::verge().data().enable_silent_start };
|
||||||
log_if_err!(app
|
if !silent_start.unwrap_or(false) {
|
||||||
.tray_handle()
|
create_window(&app.app_handle());
|
||||||
.get_item("system_proxy")
|
}
|
||||||
.set_selected(enable));
|
|
||||||
});
|
|
||||||
|
|
||||||
resolve_window(app, verge.config.enable_silent_start.clone());
|
log_err!(sysopt::Sysopt::global().init_launch());
|
||||||
|
log_err!(sysopt::Sysopt::global().init_sysproxy());
|
||||||
|
|
||||||
|
log_err!(handle::Handle::update_systray_part());
|
||||||
|
log_err!(hotkey::Hotkey::global().init(app.app_handle()));
|
||||||
|
log_err!(timer::Timer::global().init());
|
||||||
|
|
||||||
|
let argvs: Vec<String> = std::env::args().collect();
|
||||||
|
if argvs.len() > 1 {
|
||||||
|
tauri::async_runtime::block_on(async {
|
||||||
|
resolve_scheme(argvs[1].to_owned()).await;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// reset system proxy
|
/// reset system proxy
|
||||||
pub fn resolve_reset(app_handle: &AppHandle) {
|
pub fn resolve_reset() {
|
||||||
let verge_state = app_handle.state::<states::VergeState>();
|
log_err!(sysopt::Sysopt::global().reset_sysproxy());
|
||||||
let mut verge = verge_state.0.lock().unwrap();
|
log_err!(CoreManager::global().stop_core());
|
||||||
|
|
||||||
verge.reset_sysproxy();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// customize the window theme
|
/// create main window
|
||||||
fn resolve_window(app: &App, hide: Option<bool>) {
|
pub fn create_window(app_handle: &AppHandle) {
|
||||||
let window = app.get_window("main").unwrap();
|
if let Some(window) = app_handle.get_window("main") {
|
||||||
|
trace_err!(window.unminimize(), "set win unminimize");
|
||||||
// silent start
|
trace_err!(window.show(), "set win visible");
|
||||||
hide.map(|hide| {
|
trace_err!(window.set_focus(), "set win focus");
|
||||||
if hide {
|
return;
|
||||||
window.hide().unwrap();
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
let mut builder = tauri::window::WindowBuilder::new(
|
||||||
{
|
app_handle,
|
||||||
use window_shadows::set_shadow;
|
"main".to_string(),
|
||||||
use window_vibrancy::apply_blur;
|
tauri::WindowUrl::App("index.html".into()),
|
||||||
|
)
|
||||||
|
.title("Clash Verge")
|
||||||
|
.visible(false)
|
||||||
|
.fullscreen(false)
|
||||||
|
.min_inner_size(600.0, 520.0);
|
||||||
|
|
||||||
window.set_decorations(false).unwrap();
|
match Config::verge().latest().window_size_position.clone() {
|
||||||
set_shadow(&window, true).unwrap();
|
Some(size_pos) if size_pos.len() == 4 => {
|
||||||
apply_blur(&window, None).unwrap();
|
let size = (size_pos[0], size_pos[1]);
|
||||||
}
|
let pos = (size_pos[2], size_pos[3]);
|
||||||
|
let w = size.0.clamp(600.0, f64::INFINITY);
|
||||||
|
let h = size.1.clamp(520.0, f64::INFINITY);
|
||||||
|
builder = builder.inner_size(w, h).position(pos.0, pos.1);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
builder = builder.inner_size(800.0, 636.0).center();
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
use tauri::LogicalSize;
|
builder = builder.inner_size(800.0, 642.0).center();
|
||||||
use tauri::Size::Logical;
|
}
|
||||||
window.set_decorations(true).unwrap();
|
|
||||||
window
|
#[cfg(target_os = "linux")]
|
||||||
.set_size(Logical(LogicalSize {
|
{
|
||||||
width: 800.0,
|
builder = builder.inner_size(800.0, 642.0).center();
|
||||||
height: 610.0,
|
}
|
||||||
}))
|
}
|
||||||
.unwrap();
|
};
|
||||||
// use tauri_plugin_vibrancy::MacOSVibrancy;
|
#[cfg(target_os = "windows")]
|
||||||
// #[allow(deprecated)]
|
let window = builder
|
||||||
// window.apply_vibrancy(MacOSVibrancy::AppearanceBased);
|
.decorations(false)
|
||||||
}
|
.additional_browser_args("--enable-features=msWebView2EnableDraggableRegions --disable-features=OverscrollHistoryNavigation,msExperimentalScrolling")
|
||||||
|
.transparent(true)
|
||||||
|
.visible(false)
|
||||||
|
.build();
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
let window = builder
|
||||||
|
.decorations(true)
|
||||||
|
.hidden_title(true)
|
||||||
|
.title_bar_style(tauri::TitleBarStyle::Overlay)
|
||||||
|
.build();
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
let window = builder.decorations(false).transparent(true).build();
|
||||||
|
|
||||||
|
match window {
|
||||||
|
Ok(win) => {
|
||||||
|
let is_maximized = Config::verge()
|
||||||
|
.latest()
|
||||||
|
.window_is_maximized
|
||||||
|
.unwrap_or(false);
|
||||||
|
log::trace!("try to calculate the monitor size");
|
||||||
|
let center = (|| -> Result<bool> {
|
||||||
|
let mut center = false;
|
||||||
|
let monitor = win.current_monitor()?.ok_or(anyhow::anyhow!(""))?;
|
||||||
|
let size = monitor.size();
|
||||||
|
let pos = win.outer_position()?;
|
||||||
|
|
||||||
|
if pos.x < -400
|
||||||
|
|| pos.x > (size.width - 200) as i32
|
||||||
|
|| pos.y < -200
|
||||||
|
|| pos.y > (size.height - 200) as i32
|
||||||
|
{
|
||||||
|
center = true;
|
||||||
|
}
|
||||||
|
Ok(center)
|
||||||
|
})();
|
||||||
|
|
||||||
|
if center.unwrap_or(true) {
|
||||||
|
trace_err!(win.center(), "set win center");
|
||||||
|
}
|
||||||
|
|
||||||
|
trace_err!(set_shadow(&win, true), "set win shadow");
|
||||||
|
if is_maximized {
|
||||||
|
trace_err!(win.maximize(), "set win maximize");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
log::error!("failed to create window");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// save window size and position
|
||||||
|
pub fn save_window_size_position(app_handle: &AppHandle, save_to_file: bool) -> Result<()> {
|
||||||
|
let verge = Config::verge();
|
||||||
|
let mut verge = verge.latest();
|
||||||
|
|
||||||
|
if save_to_file {
|
||||||
|
verge.save_file()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let win = app_handle
|
||||||
|
.get_window("main")
|
||||||
|
.ok_or(anyhow::anyhow!("failed to get window"))?;
|
||||||
|
|
||||||
|
let scale = win.scale_factor()?;
|
||||||
|
let size = win.inner_size()?;
|
||||||
|
let size = size.to_logical::<f64>(scale);
|
||||||
|
let pos = win.outer_position()?;
|
||||||
|
let pos = pos.to_logical::<f64>(scale);
|
||||||
|
let is_maximized = win.is_maximized()?;
|
||||||
|
verge.window_is_maximized = Some(is_maximized);
|
||||||
|
if !is_maximized && size.width >= 600.0 && size.height >= 520.0 {
|
||||||
|
verge.window_size_position = Some(vec![size.width, size.height, pos.x, pos.y]);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn resolve_scheme(param: String) {
|
||||||
|
let url = param
|
||||||
|
.trim_start_matches("clash://install-config/?url=")
|
||||||
|
.trim_start_matches("clash://install-config?url=");
|
||||||
|
let option = PrfOption {
|
||||||
|
user_agent: None,
|
||||||
|
with_proxy: Some(true),
|
||||||
|
self_proxy: None,
|
||||||
|
danger_accept_invalid_certs: None,
|
||||||
|
update_interval: None,
|
||||||
|
};
|
||||||
|
if let Ok(item) = PrfItem::from_url(url, None, None, Some(option)).await {
|
||||||
|
if Config::profiles().data().append_item(item).is_ok() {
|
||||||
|
notification::Notification::new(crate::utils::dirs::APP_ID)
|
||||||
|
.title("Clash Verge")
|
||||||
|
.body("Import profile success")
|
||||||
|
.show()
|
||||||
|
.unwrap();
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
notification::Notification::new(crate::utils::dirs::APP_ID)
|
||||||
|
.title("Clash Verge")
|
||||||
|
.body("Import profile failed")
|
||||||
|
.show()
|
||||||
|
.unwrap();
|
||||||
|
log::error!("failed to parse url: {}", url);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,78 @@
|
|||||||
extern crate warp;
|
extern crate warp;
|
||||||
|
|
||||||
|
use super::resolve;
|
||||||
|
use crate::config::IVerge;
|
||||||
|
use anyhow::{bail, Result};
|
||||||
use port_scanner::local_port_available;
|
use port_scanner::local_port_available;
|
||||||
use tauri::{AppHandle, Manager};
|
use std::convert::Infallible;
|
||||||
|
use tauri::AppHandle;
|
||||||
use warp::Filter;
|
use warp::Filter;
|
||||||
|
|
||||||
#[cfg(not(feature = "verge-dev"))]
|
#[derive(serde::Deserialize, Debug)]
|
||||||
const SERVER_PORT: u16 = 33333;
|
struct QueryParam {
|
||||||
#[cfg(feature = "verge-dev")]
|
param: String,
|
||||||
const SERVER_PORT: u16 = 11233;
|
}
|
||||||
|
|
||||||
/// check whether there is already exists
|
/// check whether there is already exists
|
||||||
pub fn check_singleton() -> Result<(), ()> {
|
pub fn check_singleton() -> Result<()> {
|
||||||
if !local_port_available(SERVER_PORT) {
|
let port = IVerge::get_singleton_port();
|
||||||
tauri::async_runtime::block_on(async {
|
|
||||||
let url = format!("http://127.0.0.1:{}/commands/visible", SERVER_PORT);
|
if !local_port_available(port) {
|
||||||
reqwest::get(url).await.unwrap();
|
tauri::async_runtime::block_on(async {
|
||||||
Err(())
|
let resp = reqwest::get(format!("http://127.0.0.1:{port}/commands/ping"))
|
||||||
})
|
.await?
|
||||||
} else {
|
.text()
|
||||||
Ok(())
|
.await?;
|
||||||
}
|
|
||||||
|
if &resp == "ok" {
|
||||||
|
let argvs: Vec<String> = std::env::args().collect();
|
||||||
|
if argvs.len() > 1 {
|
||||||
|
let param = argvs[1].as_str();
|
||||||
|
reqwest::get(format!(
|
||||||
|
"http://127.0.0.1:{port}/commands/scheme?param={param}"
|
||||||
|
))
|
||||||
|
.await?
|
||||||
|
.text()
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
reqwest::get(format!("http://127.0.0.1:{port}/commands/visible"))
|
||||||
|
.await?
|
||||||
|
.text()
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
bail!("app exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
log::error!("failed to setup singleton listen server");
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The embed server only be used to implement singleton process
|
/// The embed server only be used to implement singleton process
|
||||||
/// maybe it can be used as pac server later
|
/// maybe it can be used as pac server later
|
||||||
pub fn embed_server(app: &AppHandle) {
|
pub fn embed_server(app_handle: AppHandle) {
|
||||||
let window = app.get_window("main").unwrap();
|
let port = IVerge::get_singleton_port();
|
||||||
|
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
let commands = warp::path!("commands" / "visible").map(move || {
|
let ping = warp::path!("commands" / "ping").map(move || "ok");
|
||||||
window.show().unwrap();
|
|
||||||
window.set_focus().unwrap();
|
let visible = warp::path!("commands" / "visible").map(move || {
|
||||||
return format!("ok");
|
resolve::create_window(&app_handle);
|
||||||
|
"ok"
|
||||||
|
});
|
||||||
|
|
||||||
|
let scheme = warp::path!("commands" / "scheme")
|
||||||
|
.and(warp::query::<QueryParam>())
|
||||||
|
.and_then(scheme_handler);
|
||||||
|
|
||||||
|
async fn scheme_handler(query: QueryParam) -> Result<impl warp::Reply, Infallible> {
|
||||||
|
resolve::resolve_scheme(query.param).await;
|
||||||
|
Ok("ok")
|
||||||
|
}
|
||||||
|
let commands = ping.or(visible).or(scheme);
|
||||||
|
warp::serve(commands).run(([127, 0, 0, 1], port)).await;
|
||||||
});
|
});
|
||||||
|
|
||||||
warp::serve(commands)
|
|
||||||
.bind(([127, 0, 0, 1], SERVER_PORT))
|
|
||||||
.await;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,366 +0,0 @@
|
|||||||
use anyhow::Result;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
static DEFAULT_BYPASS: &str = "localhost;127.*;10.*;172.16.*;172.17.*;172.18.*;172.19.*;172.20.*;172.21.*;172.22.*;172.23.*;172.24.*;172.25.*;172.26.*;172.27.*;172.28.*;172.29.*;172.30.*;172.31.*;192.168.*;<local>";
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
static DEFAULT_BYPASS: &str = "localhost,127.0.0.1/8,::1";
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
static DEFAULT_BYPASS: &str =
|
|
||||||
"192.168.0.0/16\n10.0.0.0/8\n172.16.0.0/12\n127.0.0.1\nlocalhost\n*.local\ntimestamp.apple.com\n";
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
static MACOS_SERVICE: &str = "Wi-Fi";
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
|
||||||
pub struct SysProxyConfig {
|
|
||||||
pub enable: bool,
|
|
||||||
pub server: String,
|
|
||||||
pub bypass: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for SysProxyConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
SysProxyConfig {
|
|
||||||
enable: false,
|
|
||||||
server: String::from(""),
|
|
||||||
bypass: String::from(""),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SysProxyConfig {
|
|
||||||
pub fn new(enable: bool, port: String, bypass: Option<String>) -> Self {
|
|
||||||
SysProxyConfig {
|
|
||||||
enable,
|
|
||||||
server: format!("127.0.0.1:{}", port),
|
|
||||||
bypass: bypass.unwrap_or(DEFAULT_BYPASS.into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
impl SysProxyConfig {
|
|
||||||
/// Get the windows system proxy config
|
|
||||||
pub fn get_sys() -> Result<Self> {
|
|
||||||
use winreg::enums::*;
|
|
||||||
use winreg::RegKey;
|
|
||||||
|
|
||||||
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
|
||||||
let cur_var = hkcu.open_subkey_with_flags(
|
|
||||||
"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Internet Settings",
|
|
||||||
KEY_READ,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(SysProxyConfig {
|
|
||||||
enable: cur_var.get_value::<u32, _>("ProxyEnable")? == 1u32,
|
|
||||||
server: cur_var.get_value("ProxyServer")?,
|
|
||||||
bypass: cur_var.get_value("ProxyOverride")?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the windows system proxy config
|
|
||||||
pub fn set_sys(&self) -> Result<()> {
|
|
||||||
use winreg::enums::*;
|
|
||||||
use winreg::RegKey;
|
|
||||||
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
|
||||||
let cur_var = hkcu.open_subkey_with_flags(
|
|
||||||
"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Internet Settings",
|
|
||||||
KEY_SET_VALUE,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let enable: u32 = if self.enable { 1u32 } else { 0u32 };
|
|
||||||
|
|
||||||
cur_var.set_value("ProxyEnable", &enable)?;
|
|
||||||
cur_var.set_value("ProxyServer", &self.server)?;
|
|
||||||
cur_var.set_value("ProxyOverride", &self.bypass)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
impl SysProxyConfig {
|
|
||||||
/// Get the macos system proxy config
|
|
||||||
pub fn get_sys() -> Result<Self> {
|
|
||||||
use std::process::Command;
|
|
||||||
|
|
||||||
let http = macproxy::get_proxy(&["-getwebproxy", MACOS_SERVICE])?;
|
|
||||||
let https = macproxy::get_proxy(&["-getsecurewebproxy", MACOS_SERVICE])?;
|
|
||||||
let sock = macproxy::get_proxy(&["-getsocksfirewallproxy", MACOS_SERVICE])?;
|
|
||||||
|
|
||||||
let mut enable = false;
|
|
||||||
let mut server = "".into();
|
|
||||||
|
|
||||||
if sock.0 == "Yes" {
|
|
||||||
enable = true;
|
|
||||||
server = sock.1;
|
|
||||||
}
|
|
||||||
if https.0 == "Yes" {
|
|
||||||
enable = true;
|
|
||||||
server = https.1;
|
|
||||||
}
|
|
||||||
if http.0 == "Yes" || !enable {
|
|
||||||
enable = http.0 == "Yes";
|
|
||||||
server = http.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
let bypass_output = Command::new("networksetup")
|
|
||||||
.args(["-getproxybypassdomains", MACOS_SERVICE])
|
|
||||||
.output()?;
|
|
||||||
|
|
||||||
// change the format to xxx,xxx
|
|
||||||
let bypass = std::str::from_utf8(&bypass_output.stdout)
|
|
||||||
.unwrap_or(DEFAULT_BYPASS)
|
|
||||||
.to_string()
|
|
||||||
.split('\n')
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(",");
|
|
||||||
|
|
||||||
Ok(SysProxyConfig {
|
|
||||||
enable,
|
|
||||||
server,
|
|
||||||
bypass,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the macos system proxy config
|
|
||||||
pub fn set_sys(&self) -> Result<()> {
|
|
||||||
use std::process::Command;
|
|
||||||
|
|
||||||
let enable = self.enable;
|
|
||||||
let server = self.server.as_str();
|
|
||||||
let bypass = self.bypass.clone();
|
|
||||||
macproxy::set_proxy("-setwebproxy", MACOS_SERVICE, enable, server)?;
|
|
||||||
macproxy::set_proxy("-setsecurewebproxy", MACOS_SERVICE, enable, server)?;
|
|
||||||
macproxy::set_proxy("-setsocksfirewallproxy", MACOS_SERVICE, enable, server)?;
|
|
||||||
|
|
||||||
let domains = bypass.split(",").collect::<Vec<_>>();
|
|
||||||
Command::new("networksetup")
|
|
||||||
.args([["-setproxybypassdomains", MACOS_SERVICE].to_vec(), domains].concat())
|
|
||||||
.status()?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
mod macproxy {
|
|
||||||
use super::*;
|
|
||||||
use anyhow::bail;
|
|
||||||
use std::process::Command;
|
|
||||||
|
|
||||||
/// use networksetup
|
|
||||||
/// get the target proxy config
|
|
||||||
pub(super) fn get_proxy(args: &[&str; 2]) -> Result<(String, String)> {
|
|
||||||
let output = Command::new("networksetup").args(args).output()?;
|
|
||||||
|
|
||||||
let stdout = std::str::from_utf8(&output.stdout)?;
|
|
||||||
let enable = parse(stdout, "Enabled:");
|
|
||||||
let server = parse(stdout, "Server:");
|
|
||||||
let port = parse(stdout, "Port:");
|
|
||||||
let server = format!("{server}:{port}");
|
|
||||||
Ok((enable.into(), server))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// use networksetup
|
|
||||||
/// set the target proxy config
|
|
||||||
pub(super) fn set_proxy(
|
|
||||||
target: &str, // like: -setwebproxy
|
|
||||||
device: &str,
|
|
||||||
enable: bool,
|
|
||||||
server: &str,
|
|
||||||
) -> Result<()> {
|
|
||||||
let mut split = server.split(":");
|
|
||||||
let host = split.next();
|
|
||||||
let port = split.next();
|
|
||||||
|
|
||||||
// can not parse the field
|
|
||||||
if host.is_none() || port.is_none() {
|
|
||||||
bail!("failed to parse the server into host:port");
|
|
||||||
}
|
|
||||||
|
|
||||||
let args = vec![target, device, host.unwrap(), port.unwrap()];
|
|
||||||
Command::new("networksetup").args(&args).status()?;
|
|
||||||
|
|
||||||
let target_state = String::from(target) + "state";
|
|
||||||
let enable = if enable { "on" } else { "off" };
|
|
||||||
let args = vec![target_state.as_str(), device, enable];
|
|
||||||
Command::new("networksetup").args(&args).status()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// parse the networksetup output
|
|
||||||
fn parse<'a>(target: &'a str, key: &'a str) -> &'a str {
|
|
||||||
match target.find(key) {
|
|
||||||
Some(idx) => {
|
|
||||||
let idx = idx + key.len();
|
|
||||||
let value = &target[idx..];
|
|
||||||
let value = match value.find("\n") {
|
|
||||||
Some(end) => &value[..end],
|
|
||||||
None => value,
|
|
||||||
};
|
|
||||||
value.trim()
|
|
||||||
}
|
|
||||||
None => "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_get() {
|
|
||||||
use std::process::Command;
|
|
||||||
|
|
||||||
let output = Command::new("networksetup")
|
|
||||||
.args(["-getwebproxy", "Wi-Fi"])
|
|
||||||
.output();
|
|
||||||
|
|
||||||
let output = output.unwrap();
|
|
||||||
let stdout = std::str::from_utf8(&output.stdout).unwrap();
|
|
||||||
let enable = parse(stdout, "Enabled:");
|
|
||||||
let server = parse(stdout, "Server:");
|
|
||||||
let port = parse(stdout, "Port:");
|
|
||||||
|
|
||||||
println!("enable: {}, server: {}, port: {}", enable, server, port);
|
|
||||||
|
|
||||||
dbg!(SysProxyConfig::get_sys().unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_set() {
|
|
||||||
let sysproxy = SysProxyConfig::new(true, "7890".into(), None);
|
|
||||||
dbg!(sysproxy.set_sys().unwrap());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
///
|
|
||||||
/// Linux Desktop System Proxy Supports
|
|
||||||
/// by using `gsettings`
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
impl SysProxyConfig {
|
|
||||||
/// Get the system proxy config [http/https/socks]
|
|
||||||
pub fn get_sys() -> Result<Self> {
|
|
||||||
use std::process::Command;
|
|
||||||
|
|
||||||
let schema = "org.gnome.system.proxy";
|
|
||||||
|
|
||||||
// get enable
|
|
||||||
let mode = Command::new("gsettings")
|
|
||||||
.args(["get", schema, "mode"])
|
|
||||||
.output()?;
|
|
||||||
let mode = std::str::from_utf8(&mode.stdout)?;
|
|
||||||
let enable = mode == "manual";
|
|
||||||
|
|
||||||
// get bypass
|
|
||||||
// Todo: parse the ignore-hosts
|
|
||||||
// ['aaa', 'bbb'] -> aaa,bbb
|
|
||||||
let ignore = Command::new("gsettings")
|
|
||||||
.args(["get", schema, "ignore-hosts"])
|
|
||||||
.output()?;
|
|
||||||
let ignore = std::str::from_utf8(&ignore.stdout)?;
|
|
||||||
let bypass = ignore.to_string();
|
|
||||||
|
|
||||||
let http = Self::get_proxy("http")?;
|
|
||||||
let https = Self::get_proxy("https")?;
|
|
||||||
let socks = Self::get_proxy("socks")?;
|
|
||||||
|
|
||||||
let mut server = "".into();
|
|
||||||
|
|
||||||
if socks.len() > 0 {
|
|
||||||
server = socks;
|
|
||||||
}
|
|
||||||
if https.len() > 0 {
|
|
||||||
server = https;
|
|
||||||
}
|
|
||||||
if http.len() > 0 {
|
|
||||||
server = http;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(SysProxyConfig {
|
|
||||||
enable,
|
|
||||||
server,
|
|
||||||
bypass,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the system proxy config [http/https/socks]
|
|
||||||
pub fn set_sys(&self) -> Result<()> {
|
|
||||||
use anyhow::bail;
|
|
||||||
use std::process::Command;
|
|
||||||
|
|
||||||
let enable = self.enable;
|
|
||||||
let server = self.server.as_str();
|
|
||||||
let bypass = self.bypass.clone();
|
|
||||||
let schema = "org.gnome.system.proxy";
|
|
||||||
|
|
||||||
if enable {
|
|
||||||
let mut split = server.split(":");
|
|
||||||
let host = split.next();
|
|
||||||
let port = split.next();
|
|
||||||
|
|
||||||
if port.is_none() {
|
|
||||||
bail!("failed to parse the port");
|
|
||||||
}
|
|
||||||
|
|
||||||
let host = format!("'{}'", host.unwrap_or("127.0.0.1"));
|
|
||||||
let host = host.as_str();
|
|
||||||
let port = port.unwrap();
|
|
||||||
|
|
||||||
let http = format!("{schema}.http");
|
|
||||||
Command::new("gsettings")
|
|
||||||
.args(["set", http.as_str(), "host", host])
|
|
||||||
.status()?;
|
|
||||||
Command::new("gsettings")
|
|
||||||
.args(["set", http.as_str(), "port", port])
|
|
||||||
.status()?;
|
|
||||||
|
|
||||||
let https = format!("{schema}.https");
|
|
||||||
Command::new("gsettings")
|
|
||||||
.args(["set", https.as_str(), "host", host])
|
|
||||||
.status()?;
|
|
||||||
Command::new("gsettings")
|
|
||||||
.args(["set", https.as_str(), "port", port])
|
|
||||||
.status()?;
|
|
||||||
|
|
||||||
let socks = format!("{schema}.socks");
|
|
||||||
Command::new("gsettings")
|
|
||||||
.args(["set", socks.as_str(), "host", host])
|
|
||||||
.status()?;
|
|
||||||
Command::new("gsettings")
|
|
||||||
.args(["set", socks.as_str(), "port", port])
|
|
||||||
.status()?;
|
|
||||||
|
|
||||||
// set bypass
|
|
||||||
// Todo: parse the ignore-hosts
|
|
||||||
// aaa,bbb,cccc -> ['aaa', 'bbb', 'ccc']
|
|
||||||
Command::new("gsettings")
|
|
||||||
.args(["set", schema, "ignore-hosts", bypass.as_str()]) // todo
|
|
||||||
.status()?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mode = if enable { "'manual'" } else { "'none'" };
|
|
||||||
Command::new("gsettings")
|
|
||||||
.args(["set", schema, "mode", mode])
|
|
||||||
.status()?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// help function
|
|
||||||
fn get_proxy(typ: &str) -> Result<String> {
|
|
||||||
use std::process::Command;
|
|
||||||
|
|
||||||
let schema = format!("org.gnome.system.proxy.{typ}");
|
|
||||||
let schema = schema.as_str();
|
|
||||||
|
|
||||||
let host = Command::new("gsettings")
|
|
||||||
.args(["get", schema, "host"])
|
|
||||||
.output()?;
|
|
||||||
let host = std::str::from_utf8(&host.stdout)?;
|
|
||||||
|
|
||||||
let port = Command::new("gsettings")
|
|
||||||
.args(["get", schema, "port"])
|
|
||||||
.output()?;
|
|
||||||
let port = std::str::from_utf8(&port.stdout)?;
|
|
||||||
|
|
||||||
Ok(format!("{host}:{port}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,70 +1,43 @@
|
|||||||
///! Some config file template
|
//! Some config file template
|
||||||
|
|
||||||
/// template for clash core `config.yaml`
|
|
||||||
pub const CLASH_CONFIG: &[u8] = br#"# Default Config For Clash Core
|
|
||||||
|
|
||||||
mixed-port: 7890
|
|
||||||
log-level: info
|
|
||||||
allow-lan: false
|
|
||||||
external-controller: 127.0.0.1:9090
|
|
||||||
mode: rule
|
|
||||||
secret: ""
|
|
||||||
"#;
|
|
||||||
|
|
||||||
/// template for `profiles.yaml`
|
|
||||||
pub const PROFILES_CONFIG: &[u8] = b"# Profiles Config for Clash Verge
|
|
||||||
|
|
||||||
current: ~
|
|
||||||
items: ~
|
|
||||||
";
|
|
||||||
|
|
||||||
/// template for `verge.yaml`
|
|
||||||
pub const VERGE_CONFIG: &[u8] = b"# Defaulf Config For Clash Verge
|
|
||||||
|
|
||||||
language: en
|
|
||||||
theme_mode: light
|
|
||||||
theme_blur: false
|
|
||||||
traffic_graph: true
|
|
||||||
enable_self_startup: false
|
|
||||||
enable_system_proxy: false
|
|
||||||
enable_proxy_guard: false
|
|
||||||
proxy_guard_duration: 10
|
|
||||||
system_proxy_bypass: localhost;127.*;10.*;192.168.*;<local>
|
|
||||||
";
|
|
||||||
|
|
||||||
/// template for new a profile item
|
/// template for new a profile item
|
||||||
pub const ITEM_LOCAL: &str = "# Profile Template for clash verge
|
pub const ITEM_LOCAL: &str = "# Profile Template for Clash Verge
|
||||||
|
|
||||||
proxies:
|
proxies: []
|
||||||
|
|
||||||
proxy-groups:
|
proxy-groups: []
|
||||||
|
|
||||||
rules:
|
rules: []
|
||||||
";
|
";
|
||||||
|
|
||||||
/// enhanced profile
|
/// enhanced profile
|
||||||
pub const ITEM_MERGE: &str = "# Merge Template for clash verge
|
pub const ITEM_MERGE: &str = "# Profile Enhancement Merge Template for Clash Verge
|
||||||
# The `Merge` format used to enhance profile
|
|
||||||
|
|
||||||
prepend-rules:
|
prepend-rules: []
|
||||||
|
|
||||||
prepend-proxies:
|
prepend-rule-providers: {}
|
||||||
|
|
||||||
prepend-proxy-groups:
|
prepend-proxies: []
|
||||||
|
|
||||||
append-rules:
|
prepend-proxy-providers: {}
|
||||||
|
|
||||||
append-proxies:
|
prepend-proxy-groups: []
|
||||||
|
|
||||||
append-proxy-groups:
|
append-rules: []
|
||||||
|
|
||||||
|
append-rule-providers: {}
|
||||||
|
|
||||||
|
append-proxies: []
|
||||||
|
|
||||||
|
append-proxy-providers: {}
|
||||||
|
|
||||||
|
append-proxy-groups: []
|
||||||
";
|
";
|
||||||
|
|
||||||
/// enhanced profile
|
/// enhanced profile
|
||||||
pub const ITEM_SCRIPT: &str = "// Should define the `main` function
|
pub const ITEM_SCRIPT: &str = "// Define main function (script entry)
|
||||||
// The argument to this function is the clash config
|
|
||||||
// or the result of the previous handler
|
function main(config) {
|
||||||
// so you should return the config after processing
|
return config;
|
||||||
function main(params) {
|
|
||||||
return params;
|
|
||||||
}
|
}
|
||||||
";
|
";
|
||||||
|
|||||||
14
src-tauri/src/utils/unix_helper.rs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub fn linux_elevator() -> &'static str {
|
||||||
|
use std::process::Command;
|
||||||
|
match Command::new("which").arg("pkexec").output() {
|
||||||
|
Ok(output) => {
|
||||||
|
if output.stdout.is_empty() {
|
||||||
|
"sudo"
|
||||||
|
} else {
|
||||||
|
"pkexec"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => "sudo",
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,62 +1,41 @@
|
|||||||
{
|
{
|
||||||
|
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "Clash Verge",
|
"productName": "Clash Verge",
|
||||||
"version": "0.0.27"
|
"version": "1.6.1"
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"distDir": "../dist",
|
"distDir": "../dist",
|
||||||
"devPath": "http://localhost:3000/",
|
"devPath": "http://localhost:3000/",
|
||||||
"beforeDevCommand": "yarn run web:dev",
|
"beforeDevCommand": "pnpm run web:dev",
|
||||||
"beforeBuildCommand": "yarn run web:build"
|
"beforeBuildCommand": "pnpm run web:build"
|
||||||
},
|
},
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"systemTray": {
|
|
||||||
"iconPath": "icons/icon.png",
|
|
||||||
"iconAsTemplate": true
|
|
||||||
},
|
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"active": true,
|
"active": true,
|
||||||
"targets": "all",
|
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
|
||||||
"identifier": "top.gydi.clashverge",
|
|
||||||
"icon": [
|
"icon": [
|
||||||
"icons/32x32.png",
|
"icons/32x32.png",
|
||||||
"icons/128x128.png",
|
"icons/128x128.png",
|
||||||
"icons/128x128@2x.png",
|
"icons/128x128@2x.png",
|
||||||
"icons/icon.icns",
|
"icons/icon-new.icns",
|
||||||
"icons/icon.ico"
|
"icons/icon.ico"
|
||||||
],
|
],
|
||||||
"resources": ["resources"],
|
"resources": ["resources"],
|
||||||
"externalBin": ["sidecar/clash"],
|
"externalBin": ["sidecar/clash-meta", "sidecar/clash-meta-alpha"],
|
||||||
"copyright": "© 2022 zzzgydi All Rights Reserved",
|
"copyright": "© 2022 zzzgydi All Rights Reserved",
|
||||||
"category": "DeveloperTool",
|
"category": "DeveloperTool",
|
||||||
"shortDescription": "A Clash GUI based on tauri.",
|
"shortDescription": "A Clash Meta GUI based on tauri.",
|
||||||
"longDescription": "A Clash GUI based on tauri.",
|
"longDescription": "A Clash Meta GUI based on tauri."
|
||||||
"deb": {
|
|
||||||
"depends": [],
|
|
||||||
"useBootstrapper": false
|
|
||||||
},
|
|
||||||
"macOS": {
|
|
||||||
"frameworks": [],
|
|
||||||
"minimumSystemVersion": "",
|
|
||||||
"useBootstrapper": false,
|
|
||||||
"exceptionDomain": "",
|
|
||||||
"signingIdentity": null,
|
|
||||||
"entitlements": null
|
|
||||||
},
|
|
||||||
"windows": {
|
|
||||||
"certificateThumbprint": null,
|
|
||||||
"digestAlgorithm": "sha256",
|
|
||||||
"timestampUrl": ""
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"updater": {
|
"updater": {
|
||||||
"active": true,
|
"active": true,
|
||||||
"endpoints": [
|
"endpoints": [
|
||||||
"https://github.com/zzzgydi/clash-verge/releases/download/updater/update.json",
|
"https://mirror.ghproxy.com/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json",
|
||||||
"https://hub.fastgit.xyz/zzzgydi/clash-verge/releases/download/updater/update-proxy.json"
|
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update.json"
|
||||||
],
|
],
|
||||||
"dialog": false,
|
"dialog": false,
|
||||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDExNUFBNTBBN0FDNEFBRTUKUldUbHFzUjZDcVZhRVRJM25NS3NkSFlFVElxUkNZMzZ6bHUwRVJjb2F3alJXVzRaeDdSaTA2YWYK"
|
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK"
|
||||||
},
|
},
|
||||||
"allowlist": {
|
"allowlist": {
|
||||||
"shell": {
|
"shell": {
|
||||||
@@ -67,23 +46,36 @@
|
|||||||
},
|
},
|
||||||
"process": {
|
"process": {
|
||||||
"all": true
|
"all": true
|
||||||
|
},
|
||||||
|
"globalShortcut": {
|
||||||
|
"all": true
|
||||||
|
},
|
||||||
|
"clipboard": {
|
||||||
|
"all": true
|
||||||
|
},
|
||||||
|
"notification": {
|
||||||
|
"all": true
|
||||||
|
},
|
||||||
|
"dialog": {
|
||||||
|
"all": false,
|
||||||
|
"open": true
|
||||||
|
},
|
||||||
|
"protocol": {
|
||||||
|
"asset": true,
|
||||||
|
"assetScope": ["$APPDATA/**", "$RESOURCE/../**"]
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"all": true
|
||||||
|
},
|
||||||
|
"fs": {
|
||||||
|
"exists": true,
|
||||||
|
"readFile": true,
|
||||||
|
"scope": ["$APPDATA/**", "$RESOURCE/../**", "**"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"windows": [
|
"windows": [],
|
||||||
{
|
|
||||||
"title": "Clash Verge",
|
|
||||||
"width": 800,
|
|
||||||
"height": 600,
|
|
||||||
"resizable": true,
|
|
||||||
"fullscreen": false,
|
|
||||||
"decorations": false,
|
|
||||||
"transparent": true,
|
|
||||||
"minWidth": 600,
|
|
||||||
"minHeight": 520
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"security": {
|
"security": {
|
||||||
"csp": "script-src 'unsafe-eval' 'self'; default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; img-src data: 'self';"
|
"csp": "script-src 'unsafe-eval' 'self'; default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; img-src asset: http: https: data: 'self';"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
16
src-tauri/tauri.linux.conf.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||||
|
"tauri": {
|
||||||
|
"systemTray": {
|
||||||
|
"iconPath": "icons/tray-icon.png"
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
|
||||||
|
"targets": ["deb", "appimage", "updater"],
|
||||||
|
"deb": {
|
||||||
|
"depends": ["openssl"],
|
||||||
|
"desktopTemplate": "./template/clash-verge.desktop"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src-tauri/tauri.macos.conf.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||||
|
"tauri": {
|
||||||
|
"systemTray": {
|
||||||
|
"iconPath": "icons/mac-tray-icon.png",
|
||||||
|
"iconAsTemplate": true
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
|
||||||
|
"targets": ["app", "dmg", "updater"],
|
||||||
|
"macOS": {
|
||||||
|
"frameworks": [],
|
||||||
|
"minimumSystemVersion": "10.15",
|
||||||
|
"exceptionDomain": "",
|
||||||
|
"signingIdentity": null,
|
||||||
|
"entitlements": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src-tauri/tauri.windows.conf.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||||
|
"tauri": {
|
||||||
|
"systemTray": {
|
||||||
|
"iconPath": "icons/tray-icon.png"
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
|
||||||
|
"targets": ["nsis", "updater"],
|
||||||
|
"windows": {
|
||||||
|
"certificateThumbprint": null,
|
||||||
|
"digestAlgorithm": "sha256",
|
||||||
|
"timestampUrl": "",
|
||||||
|
"webviewInstallMode": {
|
||||||
|
"type": "embedBootstrapper",
|
||||||
|
"silent": true
|
||||||
|
},
|
||||||
|
"nsis": {
|
||||||
|
"displayLanguageSelector": true,
|
||||||
|
"installerIcon": "icons/icon.ico",
|
||||||
|
"languages": ["SimpChinese", "English"],
|
||||||
|
"license": "../LICENSE",
|
||||||
|
"installMode": "perMachine",
|
||||||
|
"template": "./template/installer.nsi"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src-tauri/template/clash-verge.desktop
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Categories={{{categories}}}
|
||||||
|
Comment={{{comment}}}
|
||||||
|
Exec={{{exec}}} %u
|
||||||
|
Icon={{{icon}}}
|
||||||
|
Name={{{name}}}
|
||||||
|
Terminal=false
|
||||||
|
Type=Application
|
||||||
|
MimeType=x-scheme-handler/clash;
|
||||||
852
src-tauri/template/installer.nsi
Normal file
@@ -0,0 +1,852 @@
|
|||||||
|
; This file is copied from https://github.com/tauri-apps/tauri/blob/tauri-v1.5/tooling/bundler/src/bundle/windows/templates/installer.nsi
|
||||||
|
; and edit to fit the needs of the project. the latest tauri 2.x has a different base nsi script.
|
||||||
|
|
||||||
|
Unicode true
|
||||||
|
; Set the compression algorithm. Default is LZMA.
|
||||||
|
!if "{{compression}}" == ""
|
||||||
|
SetCompressor /SOLID lzma
|
||||||
|
!else
|
||||||
|
SetCompressor /SOLID "{{compression}}"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!include MUI2.nsh
|
||||||
|
!include FileFunc.nsh
|
||||||
|
!include x64.nsh
|
||||||
|
!include WordFunc.nsh
|
||||||
|
!include "LogicLib.nsh"
|
||||||
|
!include "StrFunc.nsh"
|
||||||
|
!addplugindir "$%AppData%\Local\NSIS\"
|
||||||
|
${StrCase}
|
||||||
|
${StrLoc}
|
||||||
|
|
||||||
|
!define MANUFACTURER "{{manufacturer}}"
|
||||||
|
!define PRODUCTNAME "{{product_name}}"
|
||||||
|
!define VERSION "{{version}}"
|
||||||
|
!define VERSIONWITHBUILD "{{version_with_build}}"
|
||||||
|
!define SHORTDESCRIPTION "{{short_description}}"
|
||||||
|
!define INSTALLMODE "{{install_mode}}"
|
||||||
|
!define LICENSE "{{license}}"
|
||||||
|
!define INSTALLERICON "{{installer_icon}}"
|
||||||
|
!define SIDEBARIMAGE "{{sidebar_image}}"
|
||||||
|
!define HEADERIMAGE "{{header_image}}"
|
||||||
|
!define MAINBINARYNAME "{{main_binary_name}}"
|
||||||
|
!define MAINBINARYSRCPATH "{{main_binary_path}}"
|
||||||
|
!define BUNDLEID "{{bundle_id}}"
|
||||||
|
!define COPYRIGHT "{{copyright}}"
|
||||||
|
!define OUTFILE "{{out_file}}"
|
||||||
|
!define ARCH "{{arch}}"
|
||||||
|
!define PLUGINSPATH "{{additional_plugins_path}}"
|
||||||
|
!define ALLOWDOWNGRADES "{{allow_downgrades}}"
|
||||||
|
!define DISPLAYLANGUAGESELECTOR "{{display_language_selector}}"
|
||||||
|
!define INSTALLWEBVIEW2MODE "{{install_webview2_mode}}"
|
||||||
|
!define WEBVIEW2INSTALLERARGS "{{webview2_installer_args}}"
|
||||||
|
!define WEBVIEW2BOOTSTRAPPERPATH "{{webview2_bootstrapper_path}}"
|
||||||
|
!define WEBVIEW2INSTALLERPATH "{{webview2_installer_path}}"
|
||||||
|
!define UNINSTKEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCTNAME}"
|
||||||
|
!define MANUPRODUCTKEY "Software\${MANUFACTURER}\${PRODUCTNAME}"
|
||||||
|
!define UNINSTALLERSIGNCOMMAND "{{uninstaller_sign_cmd}}"
|
||||||
|
!define ESTIMATEDSIZE "{{estimated_size}}"
|
||||||
|
|
||||||
|
Name "${PRODUCTNAME}"
|
||||||
|
BrandingText "${COPYRIGHT}"
|
||||||
|
OutFile "${OUTFILE}"
|
||||||
|
|
||||||
|
VIProductVersion "${VERSIONWITHBUILD}"
|
||||||
|
VIAddVersionKey "ProductName" "${PRODUCTNAME}"
|
||||||
|
VIAddVersionKey "FileDescription" "${SHORTDESCRIPTION}"
|
||||||
|
VIAddVersionKey "LegalCopyright" "${COPYRIGHT}"
|
||||||
|
VIAddVersionKey "FileVersion" "${VERSION}"
|
||||||
|
VIAddVersionKey "ProductVersion" "${VERSION}"
|
||||||
|
|
||||||
|
; Plugins path, currently exists for linux only
|
||||||
|
!if "${PLUGINSPATH}" != ""
|
||||||
|
!addplugindir "${PLUGINSPATH}"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!if "${UNINSTALLERSIGNCOMMAND}" != ""
|
||||||
|
!uninstfinalize '${UNINSTALLERSIGNCOMMAND}'
|
||||||
|
!endif
|
||||||
|
|
||||||
|
; Handle install mode, `perUser`, `perMachine` or `both`
|
||||||
|
!if "${INSTALLMODE}" == "perMachine"
|
||||||
|
RequestExecutionLevel highest
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!if "${INSTALLMODE}" == "currentUser"
|
||||||
|
RequestExecutionLevel user
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!if "${INSTALLMODE}" == "both"
|
||||||
|
!define MULTIUSER_MUI
|
||||||
|
!define MULTIUSER_INSTALLMODE_INSTDIR "${PRODUCTNAME}"
|
||||||
|
!define MULTIUSER_INSTALLMODE_COMMANDLINE
|
||||||
|
!if "${ARCH}" == "x64"
|
||||||
|
!define MULTIUSER_USE_PROGRAMFILES64
|
||||||
|
!else if "${ARCH}" == "arm64"
|
||||||
|
!define MULTIUSER_USE_PROGRAMFILES64
|
||||||
|
!endif
|
||||||
|
!define MULTIUSER_INSTALLMODE_DEFAULT_REGISTRY_KEY "${UNINSTKEY}"
|
||||||
|
!define MULTIUSER_INSTALLMODE_DEFAULT_REGISTRY_VALUENAME "CurrentUser"
|
||||||
|
!define MULTIUSER_INSTALLMODEPAGE_SHOWUSERNAME
|
||||||
|
!define MULTIUSER_INSTALLMODE_FUNCTION RestorePreviousInstallLocation
|
||||||
|
!define MULTIUSER_EXECUTIONLEVEL Highest
|
||||||
|
!include MultiUser.nsh
|
||||||
|
!endif
|
||||||
|
|
||||||
|
; installer icon
|
||||||
|
!if "${INSTALLERICON}" != ""
|
||||||
|
!define MUI_ICON "${INSTALLERICON}"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
; installer sidebar image
|
||||||
|
!if "${SIDEBARIMAGE}" != ""
|
||||||
|
!define MUI_WELCOMEFINISHPAGE_BITMAP "${SIDEBARIMAGE}"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
; installer header image
|
||||||
|
!if "${HEADERIMAGE}" != ""
|
||||||
|
!define MUI_HEADERIMAGE
|
||||||
|
!define MUI_HEADERIMAGE_BITMAP "${HEADERIMAGE}"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
; Define registry key to store installer language
|
||||||
|
!define MUI_LANGDLL_REGISTRY_ROOT "HKCU"
|
||||||
|
!define MUI_LANGDLL_REGISTRY_KEY "${MANUPRODUCTKEY}"
|
||||||
|
!define MUI_LANGDLL_REGISTRY_VALUENAME "Installer Language"
|
||||||
|
|
||||||
|
; Installer pages, must be ordered as they appear
|
||||||
|
; 1. Welcome Page
|
||||||
|
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive
|
||||||
|
!insertmacro MUI_PAGE_WELCOME
|
||||||
|
|
||||||
|
; 2. License Page (if defined)
|
||||||
|
!if "${LICENSE}" != ""
|
||||||
|
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive
|
||||||
|
!insertmacro MUI_PAGE_LICENSE "${LICENSE}"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
; 3. Install mode (if it is set to `both`)
|
||||||
|
!if "${INSTALLMODE}" == "both"
|
||||||
|
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive
|
||||||
|
!insertmacro MULTIUSER_PAGE_INSTALLMODE
|
||||||
|
!endif
|
||||||
|
|
||||||
|
|
||||||
|
; 4. Custom page to ask user if he wants to reinstall/uninstall
|
||||||
|
; only if a previous installtion was detected
|
||||||
|
Var ReinstallPageCheck
|
||||||
|
Page custom PageReinstall PageLeaveReinstall
|
||||||
|
Function PageReinstall
|
||||||
|
; Uninstall previous WiX installation if exists.
|
||||||
|
;
|
||||||
|
; A WiX installer stores the isntallation info in registry
|
||||||
|
; using a UUID and so we have to loop through all keys under
|
||||||
|
; `HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall`
|
||||||
|
; and check if `DisplayName` and `Publisher` keys match ${PRODUCTNAME} and ${MANUFACTURER}
|
||||||
|
;
|
||||||
|
; This has a potentional issue that there maybe another installation that matches
|
||||||
|
; our ${PRODUCTNAME} and ${MANUFACTURER} but wasn't installed by our WiX installer,
|
||||||
|
; however, this should be fine since the user will have to confirm the uninstallation
|
||||||
|
; and they can chose to abort it if doesn't make sense.
|
||||||
|
StrCpy $0 0
|
||||||
|
|
||||||
|
wix_loop:
|
||||||
|
EnumRegKey $1 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" $0
|
||||||
|
StrCmp $1 "" wix_done ; Exit loop if there is no more keys to loop on
|
||||||
|
IntOp $0 $0 + 1
|
||||||
|
ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$1" "DisplayName"
|
||||||
|
ReadRegStr $R1 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$1" "Publisher"
|
||||||
|
StrCmp "$R0$R1" "${PRODUCTNAME}${MANUFACTURER}" 0 wix_loop
|
||||||
|
ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$1" "UninstallString"
|
||||||
|
${StrCase} $R1 $R0 "L"
|
||||||
|
${StrLoc} $R0 $R1 "msiexec" ">"
|
||||||
|
StrCmp $R0 0 0 wix_done
|
||||||
|
StrCpy $R7 "wix"
|
||||||
|
StrCpy $R6 "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$1"
|
||||||
|
Goto compare_version
|
||||||
|
wix_done:
|
||||||
|
|
||||||
|
; Check if there is an existing installation, if not, abort the reinstall page
|
||||||
|
ReadRegStr $R0 SHCTX "${UNINSTKEY}" ""
|
||||||
|
ReadRegStr $R1 SHCTX "${UNINSTKEY}" "UninstallString"
|
||||||
|
${IfThen} "$R0$R1" == "" ${|} Abort ${|}
|
||||||
|
|
||||||
|
; Compare this installar version with the existing installation
|
||||||
|
; and modify the messages presented to the user accordingly
|
||||||
|
compare_version:
|
||||||
|
StrCpy $R4 "$(older)"
|
||||||
|
${If} $R7 == "wix"
|
||||||
|
ReadRegStr $R0 HKLM "$R6" "DisplayVersion"
|
||||||
|
${Else}
|
||||||
|
ReadRegStr $R0 SHCTX "${UNINSTKEY}" "DisplayVersion"
|
||||||
|
${EndIf}
|
||||||
|
${IfThen} $R0 == "" ${|} StrCpy $R4 "$(unknown)" ${|}
|
||||||
|
|
||||||
|
nsis_tauri_utils::SemverCompare "${VERSION}" $R0
|
||||||
|
Pop $R0
|
||||||
|
; Reinstalling the same version
|
||||||
|
${If} $R0 == 0
|
||||||
|
StrCpy $R1 "$(alreadyInstalledLong)"
|
||||||
|
StrCpy $R2 "$(addOrReinstall)"
|
||||||
|
StrCpy $R3 "$(uninstallApp)"
|
||||||
|
!insertmacro MUI_HEADER_TEXT "$(alreadyInstalled)" "$(chooseMaintenanceOption)"
|
||||||
|
StrCpy $R5 "2"
|
||||||
|
; Upgrading
|
||||||
|
${ElseIf} $R0 == 1
|
||||||
|
StrCpy $R1 "$(olderOrUnknownVersionInstalled)"
|
||||||
|
StrCpy $R2 "$(uninstallBeforeInstalling)"
|
||||||
|
StrCpy $R3 "$(dontUninstall)"
|
||||||
|
!insertmacro MUI_HEADER_TEXT "$(alreadyInstalled)" "$(choowHowToInstall)"
|
||||||
|
StrCpy $R5 "1"
|
||||||
|
; Downgrading
|
||||||
|
${ElseIf} $R0 == -1
|
||||||
|
StrCpy $R1 "$(newerVersionInstalled)"
|
||||||
|
StrCpy $R2 "$(uninstallBeforeInstalling)"
|
||||||
|
!if "${ALLOWDOWNGRADES}" == "true"
|
||||||
|
StrCpy $R3 "$(dontUninstall)"
|
||||||
|
!else
|
||||||
|
StrCpy $R3 "$(dontUninstallDowngrade)"
|
||||||
|
!endif
|
||||||
|
!insertmacro MUI_HEADER_TEXT "$(alreadyInstalled)" "$(choowHowToInstall)"
|
||||||
|
StrCpy $R5 "1"
|
||||||
|
${Else}
|
||||||
|
Abort
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
Call SkipIfPassive
|
||||||
|
|
||||||
|
nsDialogs::Create 1018
|
||||||
|
Pop $R4
|
||||||
|
${IfThen} $(^RTL) == 1 ${|} nsDialogs::SetRTL $(^RTL) ${|}
|
||||||
|
|
||||||
|
${NSD_CreateLabel} 0 0 100% 24u $R1
|
||||||
|
Pop $R1
|
||||||
|
|
||||||
|
${NSD_CreateRadioButton} 30u 50u -30u 8u $R2
|
||||||
|
Pop $R2
|
||||||
|
${NSD_OnClick} $R2 PageReinstallUpdateSelection
|
||||||
|
|
||||||
|
${NSD_CreateRadioButton} 30u 70u -30u 8u $R3
|
||||||
|
Pop $R3
|
||||||
|
; disable this radio button if downgrading and downgrades are disabled
|
||||||
|
!if "${ALLOWDOWNGRADES}" == "false"
|
||||||
|
${IfThen} $R0 == -1 ${|} EnableWindow $R3 0 ${|}
|
||||||
|
!endif
|
||||||
|
${NSD_OnClick} $R3 PageReinstallUpdateSelection
|
||||||
|
|
||||||
|
; Check the first radio button if this the first time
|
||||||
|
; we enter this page or if the second button wasn't
|
||||||
|
; selected the last time we were on this page
|
||||||
|
${If} $ReinstallPageCheck != 2
|
||||||
|
SendMessage $R2 ${BM_SETCHECK} ${BST_CHECKED} 0
|
||||||
|
${Else}
|
||||||
|
SendMessage $R3 ${BM_SETCHECK} ${BST_CHECKED} 0
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
${NSD_SetFocus} $R2
|
||||||
|
nsDialogs::Show
|
||||||
|
FunctionEnd
|
||||||
|
Function PageReinstallUpdateSelection
|
||||||
|
${NSD_GetState} $R2 $R1
|
||||||
|
${If} $R1 == ${BST_CHECKED}
|
||||||
|
StrCpy $ReinstallPageCheck 1
|
||||||
|
${Else}
|
||||||
|
StrCpy $ReinstallPageCheck 2
|
||||||
|
${EndIf}
|
||||||
|
FunctionEnd
|
||||||
|
Function PageLeaveReinstall
|
||||||
|
${NSD_GetState} $R2 $R1
|
||||||
|
|
||||||
|
; $R5 holds whether we are reinstalling the same version or not
|
||||||
|
; $R5 == "1" -> different versions
|
||||||
|
; $R5 == "2" -> same version
|
||||||
|
;
|
||||||
|
; $R1 holds the radio buttons state. its meaning is dependant on the context
|
||||||
|
StrCmp $R5 "1" 0 +2 ; Existing install is not the same version?
|
||||||
|
StrCmp $R1 "1" reinst_uninstall reinst_done ; $R1 == "1", then user chose to uninstall existing version, otherwise skip uninstalling
|
||||||
|
StrCmp $R1 "1" reinst_done ; Same version? skip uninstalling
|
||||||
|
|
||||||
|
reinst_uninstall:
|
||||||
|
HideWindow
|
||||||
|
ClearErrors
|
||||||
|
|
||||||
|
${If} $R7 == "wix"
|
||||||
|
ReadRegStr $R1 HKLM "$R6" "UninstallString"
|
||||||
|
ExecWait '$R1' $0
|
||||||
|
${Else}
|
||||||
|
ReadRegStr $4 SHCTX "${MANUPRODUCTKEY}" ""
|
||||||
|
ReadRegStr $R1 SHCTX "${UNINSTKEY}" "UninstallString"
|
||||||
|
ExecWait '$R1 /P _?=$4' $0
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
BringToFront
|
||||||
|
|
||||||
|
${IfThen} ${Errors} ${|} StrCpy $0 2 ${|} ; ExecWait failed, set fake exit code
|
||||||
|
|
||||||
|
${If} $0 <> 0
|
||||||
|
${OrIf} ${FileExists} "$INSTDIR\${MAINBINARYNAME}.exe"
|
||||||
|
${If} $0 = 1 ; User aborted uninstaller?
|
||||||
|
StrCmp $R5 "2" 0 +2 ; Is the existing install the same version?
|
||||||
|
Quit ; ...yes, already installed, we are done
|
||||||
|
Abort
|
||||||
|
${EndIf}
|
||||||
|
MessageBox MB_ICONEXCLAMATION "$(unableToUninstall)"
|
||||||
|
Abort
|
||||||
|
${Else}
|
||||||
|
StrCpy $0 $R1 1
|
||||||
|
${IfThen} $0 == '"' ${|} StrCpy $R1 $R1 -1 1 ${|} ; Strip quotes from UninstallString
|
||||||
|
Delete $R1
|
||||||
|
RMDir $INSTDIR
|
||||||
|
${EndIf}
|
||||||
|
reinst_done:
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
; 5. Choose install directoy page
|
||||||
|
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive
|
||||||
|
!insertmacro MUI_PAGE_DIRECTORY
|
||||||
|
|
||||||
|
; 6. Start menu shortcut page
|
||||||
|
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive
|
||||||
|
Var AppStartMenuFolder
|
||||||
|
!insertmacro MUI_PAGE_STARTMENU Application $AppStartMenuFolder
|
||||||
|
|
||||||
|
; 7. Installation page
|
||||||
|
!insertmacro MUI_PAGE_INSTFILES
|
||||||
|
|
||||||
|
; 8. Finish page
|
||||||
|
;
|
||||||
|
; Don't auto jump to finish page after installation page,
|
||||||
|
; because the installation page has useful info that can be used debug any issues with the installer.
|
||||||
|
!define MUI_FINISHPAGE_NOAUTOCLOSE
|
||||||
|
; Use show readme button in the finish page as a button create a desktop shortcut
|
||||||
|
!define MUI_FINISHPAGE_SHOWREADME
|
||||||
|
!define MUI_FINISHPAGE_SHOWREADME_TEXT "$(createDesktop)"
|
||||||
|
!define MUI_FINISHPAGE_SHOWREADME_FUNCTION CreateDesktopShortcut
|
||||||
|
; Show run app after installation.
|
||||||
|
!define MUI_FINISHPAGE_RUN "$INSTDIR\${MAINBINARYNAME}.exe"
|
||||||
|
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive
|
||||||
|
!insertmacro MUI_PAGE_FINISH
|
||||||
|
|
||||||
|
; Uninstaller Pages
|
||||||
|
; 1. Confirm uninstall page
|
||||||
|
Var DeleteAppDataCheckbox
|
||||||
|
Var DeleteAppDataCheckboxState
|
||||||
|
!define /ifndef WS_EX_LAYOUTRTL 0x00400000
|
||||||
|
!define MUI_PAGE_CUSTOMFUNCTION_SHOW un.ConfirmShow
|
||||||
|
Function un.ConfirmShow
|
||||||
|
FindWindow $1 "#32770" "" $HWNDPARENT ; Find inner dialog
|
||||||
|
${If} $(^RTL) == 1
|
||||||
|
System::Call 'USER32::CreateWindowEx(i${__NSD_CheckBox_EXSTYLE}|${WS_EX_LAYOUTRTL},t"${__NSD_CheckBox_CLASS}",t "$(deleteAppData)",i${__NSD_CheckBox_STYLE},i 50,i 100,i 400, i 25,i$1,i0,i0,i0)i.s'
|
||||||
|
${Else}
|
||||||
|
System::Call 'USER32::CreateWindowEx(i${__NSD_CheckBox_EXSTYLE},t"${__NSD_CheckBox_CLASS}",t "$(deleteAppData)",i${__NSD_CheckBox_STYLE},i 0,i 100,i 400, i 25,i$1,i0,i0,i0)i.s'
|
||||||
|
${EndIf}
|
||||||
|
Pop $DeleteAppDataCheckbox
|
||||||
|
SendMessage $HWNDPARENT ${WM_GETFONT} 0 0 $1
|
||||||
|
SendMessage $DeleteAppDataCheckbox ${WM_SETFONT} $1 1
|
||||||
|
FunctionEnd
|
||||||
|
!define MUI_PAGE_CUSTOMFUNCTION_LEAVE un.ConfirmLeave
|
||||||
|
Function un.ConfirmLeave
|
||||||
|
SendMessage $DeleteAppDataCheckbox ${BM_GETCHECK} 0 0 $DeleteAppDataCheckboxState
|
||||||
|
FunctionEnd
|
||||||
|
!insertmacro MUI_UNPAGE_CONFIRM
|
||||||
|
|
||||||
|
; 2. Uninstalling Page
|
||||||
|
!insertmacro MUI_UNPAGE_INSTFILES
|
||||||
|
|
||||||
|
;Languages
|
||||||
|
{{#each languages}}
|
||||||
|
!insertmacro MUI_LANGUAGE "{{this}}"
|
||||||
|
{{/each}}
|
||||||
|
!insertmacro MUI_RESERVEFILE_LANGDLL
|
||||||
|
{{#each language_files}}
|
||||||
|
!include "{{this}}"
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
|
!macro SetContext
|
||||||
|
!if "${INSTALLMODE}" == "currentUser"
|
||||||
|
SetShellVarContext current
|
||||||
|
!else if "${INSTALLMODE}" == "perMachine"
|
||||||
|
SetShellVarContext all
|
||||||
|
!endif
|
||||||
|
|
||||||
|
${If} ${RunningX64}
|
||||||
|
!if "${ARCH}" == "x64"
|
||||||
|
SetRegView 64
|
||||||
|
!else if "${ARCH}" == "arm64"
|
||||||
|
SetRegView 64
|
||||||
|
!else
|
||||||
|
SetRegView 32
|
||||||
|
!endif
|
||||||
|
${EndIf}
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
Var PassiveMode
|
||||||
|
Function .onInit
|
||||||
|
${GetOptions} $CMDLINE "/P" $PassiveMode
|
||||||
|
IfErrors +2 0
|
||||||
|
StrCpy $PassiveMode 1
|
||||||
|
|
||||||
|
!if "${DISPLAYLANGUAGESELECTOR}" == "true"
|
||||||
|
!insertmacro MUI_LANGDLL_DISPLAY
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!insertmacro SetContext
|
||||||
|
|
||||||
|
${If} $INSTDIR == ""
|
||||||
|
; Set default install location
|
||||||
|
!if "${INSTALLMODE}" == "perMachine"
|
||||||
|
${If} ${RunningX64}
|
||||||
|
!if "${ARCH}" == "x64"
|
||||||
|
StrCpy $INSTDIR "$PROGRAMFILES64\${PRODUCTNAME}"
|
||||||
|
!else if "${ARCH}" == "arm64"
|
||||||
|
StrCpy $INSTDIR "$PROGRAMFILES64\${PRODUCTNAME}"
|
||||||
|
!else
|
||||||
|
StrCpy $INSTDIR "$PROGRAMFILES\${PRODUCTNAME}"
|
||||||
|
!endif
|
||||||
|
${Else}
|
||||||
|
StrCpy $INSTDIR "$PROGRAMFILES\${PRODUCTNAME}"
|
||||||
|
${EndIf}
|
||||||
|
!else if "${INSTALLMODE}" == "currentUser"
|
||||||
|
StrCpy $INSTDIR "$LOCALAPPDATA\${PRODUCTNAME}"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
Call RestorePreviousInstallLocation
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
|
||||||
|
!if "${INSTALLMODE}" == "both"
|
||||||
|
!insertmacro MULTIUSER_INIT
|
||||||
|
!endif
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
!macro CheckAllVergeProcesses
|
||||||
|
; Check if Clash Verge.exe is running
|
||||||
|
nsis_tauri_utils::FindProcess "Clash Verge.exe"
|
||||||
|
${If} $R0 != 0
|
||||||
|
; Kill the process
|
||||||
|
DetailPrint "Kill Clash Verge.exe..."
|
||||||
|
!if "${INSTALLMODE}" == "currentUser"
|
||||||
|
nsis_tauri_utils::KillProcessCurrentUser "Clash Verge.exe"
|
||||||
|
!else
|
||||||
|
nsis_tauri_utils::KillProcess "Clash Verge.exe"
|
||||||
|
!endif
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
|
||||||
|
; Check if clash-verge-service.exe is running
|
||||||
|
nsis_tauri_utils::FindProcess "clash-verge-service.exe"
|
||||||
|
${If} $R0 != 0
|
||||||
|
; Kill the process
|
||||||
|
DetailPrint "Kill clash-verge-service.exe..."
|
||||||
|
!if "${INSTALLMODE}" == "currentUser"
|
||||||
|
nsis_tauri_utils::KillProcessCurrentUser "clash-verge-service.exe"
|
||||||
|
!else
|
||||||
|
nsis_tauri_utils::KillProcess "clash-verge-service.exe"
|
||||||
|
!endif
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
|
||||||
|
; Check if clash-meta-alpha.exe is running
|
||||||
|
nsis_tauri_utils::FindProcess "clash-meta-alpha.exe"
|
||||||
|
${If} $R0 != 0
|
||||||
|
; Kill the process
|
||||||
|
DetailPrint "Kill clash-meta-alpha.exe..."
|
||||||
|
!if "${INSTALLMODE}" == "currentUser"
|
||||||
|
nsis_tauri_utils::KillProcessCurrentUser "clash-meta-alpha.exe"
|
||||||
|
!else
|
||||||
|
nsis_tauri_utils::KillProcess "clash-meta-alpha.exe"
|
||||||
|
!endif
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
; Check if clash-meta.exe is running
|
||||||
|
nsis_tauri_utils::FindProcess "clash-meta.exe"
|
||||||
|
${If} $R0 != 0
|
||||||
|
; Kill the process
|
||||||
|
DetailPrint "Kill clash-meta.exe..."
|
||||||
|
!if "${INSTALLMODE}" == "currentUser"
|
||||||
|
nsis_tauri_utils::KillProcessCurrentUser "clash-meta.exe"
|
||||||
|
!else
|
||||||
|
nsis_tauri_utils::KillProcess "clash-meta.exe"
|
||||||
|
!endif
|
||||||
|
${EndIf}
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro StartVergeService
|
||||||
|
; Check if the service exists
|
||||||
|
SimpleSC::ExistsService "clash_verge_service"
|
||||||
|
Pop $0 ; 0:service exists;other: service not exists
|
||||||
|
; Service exists
|
||||||
|
${If} $0 == 0
|
||||||
|
Push $0
|
||||||
|
; Check if the service is running
|
||||||
|
SimpleSC::ServiceIsRunning "clash_verge_service"
|
||||||
|
Pop $0 ; returns an errorcode (<>0) otherwise success (0)
|
||||||
|
Pop $1 ; returns 1 (service is running) - returns 0 (service is not running)
|
||||||
|
${If} $0 == 0
|
||||||
|
Push $0
|
||||||
|
${If} $1 == 0
|
||||||
|
DetailPrint "Restart Clash Verge Service..."
|
||||||
|
SimpleSC::StartService "clash_verge_service" "" 30
|
||||||
|
${EndIf}
|
||||||
|
${ElseIf} $0 != 0
|
||||||
|
Push $0
|
||||||
|
SimpleSC::GetErrorMessage
|
||||||
|
Pop $0
|
||||||
|
MessageBox MB_OK|MB_ICONSTOP "Check Service Status Error ($0)"
|
||||||
|
${EndIf}
|
||||||
|
${EndIf}
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro RemoveVergeService
|
||||||
|
; Check if the service exists
|
||||||
|
SimpleSC::ExistsService "clash_verge_service"
|
||||||
|
Pop $0 ; 0:service exists;other: service not exists
|
||||||
|
; Service exists
|
||||||
|
${If} $0 == 0
|
||||||
|
Push $0
|
||||||
|
; Check if the service is running
|
||||||
|
SimpleSC::ServiceIsRunning "clash_verge_service"
|
||||||
|
Pop $0 ; returns an errorcode (<>0) otherwise success (0)
|
||||||
|
Pop $1 ; returns 1 (service is running) - returns 0 (service is not running)
|
||||||
|
${If} $0 == 0
|
||||||
|
Push $0
|
||||||
|
${If} $1 == 1
|
||||||
|
DetailPrint "Stop Clash Verge Service..."
|
||||||
|
SimpleSC::StopService "clash_verge_service" 1 30
|
||||||
|
Pop $0 ; returns an errorcode (<>0) otherwise success (0)
|
||||||
|
${If} $0 == 0
|
||||||
|
DetailPrint "Removing Clash Verge Service..."
|
||||||
|
SimpleSC::RemoveService "clash_verge_service"
|
||||||
|
${ElseIf} $0 != 0
|
||||||
|
Push $0
|
||||||
|
SimpleSC::GetErrorMessage
|
||||||
|
Pop $0
|
||||||
|
MessageBox MB_OK|MB_ICONSTOP "Clash Verge Service Stop Error ($0)"
|
||||||
|
${EndIf}
|
||||||
|
${ElseIf} $1 == 0
|
||||||
|
DetailPrint "Removing Clash Verge Service..."
|
||||||
|
SimpleSC::RemoveService "clash_verge_service"
|
||||||
|
${EndIf}
|
||||||
|
${ElseIf} $0 != 0
|
||||||
|
Push $0
|
||||||
|
SimpleSC::GetErrorMessage
|
||||||
|
Pop $0
|
||||||
|
MessageBox MB_OK|MB_ICONSTOP "Check Service Status Error ($0)"
|
||||||
|
${EndIf}
|
||||||
|
${EndIf}
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
Section EarlyChecks
|
||||||
|
; Abort silent installer if downgrades is disabled
|
||||||
|
!if "${ALLOWDOWNGRADES}" == "false"
|
||||||
|
IfSilent 0 silent_downgrades_done
|
||||||
|
; If downgrading
|
||||||
|
${If} $R0 == -1
|
||||||
|
System::Call 'kernel32::AttachConsole(i -1)i.r0'
|
||||||
|
${If} $0 != 0
|
||||||
|
System::Call 'kernel32::GetStdHandle(i -11)i.r0'
|
||||||
|
System::call 'kernel32::SetConsoleTextAttribute(i r0, i 0x0004)' ; set red color
|
||||||
|
FileWrite $0 "$(silentDowngrades)"
|
||||||
|
${EndIf}
|
||||||
|
Abort
|
||||||
|
${EndIf}
|
||||||
|
silent_downgrades_done:
|
||||||
|
!endif
|
||||||
|
|
||||||
|
SectionEnd
|
||||||
|
|
||||||
|
Section WebView2
|
||||||
|
; Check if Webview2 is already installed and skip this section
|
||||||
|
${If} ${RunningX64}
|
||||||
|
ReadRegStr $4 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
|
||||||
|
${Else}
|
||||||
|
ReadRegStr $4 HKLM "SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
|
||||||
|
${EndIf}
|
||||||
|
ReadRegStr $5 HKCU "SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
|
||||||
|
|
||||||
|
StrCmp $4 "" 0 webview2_done
|
||||||
|
StrCmp $5 "" 0 webview2_done
|
||||||
|
|
||||||
|
; Webview2 install modes
|
||||||
|
!if "${INSTALLWEBVIEW2MODE}" == "downloadBootstrapper"
|
||||||
|
Delete "$TEMP\MicrosoftEdgeWebview2Setup.exe"
|
||||||
|
DetailPrint "$(webview2Downloading)"
|
||||||
|
nsis_tauri_utils::download "https://go.microsoft.com/fwlink/p/?LinkId=2124703" "$TEMP\MicrosoftEdgeWebview2Setup.exe"
|
||||||
|
Pop $0
|
||||||
|
${If} $0 == 0
|
||||||
|
DetailPrint "$(webview2DownloadSuccess)"
|
||||||
|
${Else}
|
||||||
|
DetailPrint "$(webview2DownloadError)"
|
||||||
|
Abort "$(webview2AbortError)"
|
||||||
|
${EndIf}
|
||||||
|
StrCpy $6 "$TEMP\MicrosoftEdgeWebview2Setup.exe"
|
||||||
|
Goto install_webview2
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!if "${INSTALLWEBVIEW2MODE}" == "embedBootstrapper"
|
||||||
|
Delete "$TEMP\MicrosoftEdgeWebview2Setup.exe"
|
||||||
|
File "/oname=$TEMP\MicrosoftEdgeWebview2Setup.exe" "${WEBVIEW2BOOTSTRAPPERPATH}"
|
||||||
|
DetailPrint "$(installingWebview2)"
|
||||||
|
StrCpy $6 "$TEMP\MicrosoftEdgeWebview2Setup.exe"
|
||||||
|
Goto install_webview2
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!if "${INSTALLWEBVIEW2MODE}" == "offlineInstaller"
|
||||||
|
Delete "$TEMP\MicrosoftEdgeWebView2RuntimeInstaller.exe"
|
||||||
|
File "/oname=$TEMP\MicrosoftEdgeWebView2RuntimeInstaller.exe" "${WEBVIEW2INSTALLERPATH}"
|
||||||
|
DetailPrint "$(installingWebview2)"
|
||||||
|
StrCpy $6 "$TEMP\MicrosoftEdgeWebView2RuntimeInstaller.exe"
|
||||||
|
Goto install_webview2
|
||||||
|
!endif
|
||||||
|
|
||||||
|
Goto webview2_done
|
||||||
|
|
||||||
|
install_webview2:
|
||||||
|
DetailPrint "$(installingWebview2)"
|
||||||
|
; $6 holds the path to the webview2 installer
|
||||||
|
ExecWait "$6 ${WEBVIEW2INSTALLERARGS} /install" $1
|
||||||
|
${If} $1 == 0
|
||||||
|
DetailPrint "$(webview2InstallSuccess)"
|
||||||
|
${Else}
|
||||||
|
DetailPrint "$(webview2InstallError)"
|
||||||
|
Abort "$(webview2AbortError)"
|
||||||
|
${EndIf}
|
||||||
|
webview2_done:
|
||||||
|
SectionEnd
|
||||||
|
|
||||||
|
!macro CheckIfAppIsRunning
|
||||||
|
!if "${INSTALLMODE}" == "currentUser"
|
||||||
|
nsis_tauri_utils::FindProcessCurrentUser "${MAINBINARYNAME}.exe"
|
||||||
|
!else
|
||||||
|
nsis_tauri_utils::FindProcess "${MAINBINARYNAME}.exe"
|
||||||
|
!endif
|
||||||
|
Pop $R0
|
||||||
|
${If} $R0 = 0
|
||||||
|
IfSilent kill 0
|
||||||
|
${IfThen} $PassiveMode != 1 ${|} MessageBox MB_OKCANCEL "$(appRunningOkKill)" IDOK kill IDCANCEL cancel ${|}
|
||||||
|
kill:
|
||||||
|
!if "${INSTALLMODE}" == "currentUser"
|
||||||
|
nsis_tauri_utils::KillProcessCurrentUser "${MAINBINARYNAME}.exe"
|
||||||
|
!else
|
||||||
|
nsis_tauri_utils::KillProcess "${MAINBINARYNAME}.exe"
|
||||||
|
!endif
|
||||||
|
Pop $R0
|
||||||
|
Sleep 500
|
||||||
|
${If} $R0 = 0
|
||||||
|
Goto app_check_done
|
||||||
|
${Else}
|
||||||
|
IfSilent silent ui
|
||||||
|
silent:
|
||||||
|
System::Call 'kernel32::AttachConsole(i -1)i.r0'
|
||||||
|
${If} $0 != 0
|
||||||
|
System::Call 'kernel32::GetStdHandle(i -11)i.r0'
|
||||||
|
System::call 'kernel32::SetConsoleTextAttribute(i r0, i 0x0004)' ; set red color
|
||||||
|
FileWrite $0 "$(appRunning)$\n"
|
||||||
|
${EndIf}
|
||||||
|
Abort
|
||||||
|
ui:
|
||||||
|
Abort "$(failedToKillApp)"
|
||||||
|
${EndIf}
|
||||||
|
cancel:
|
||||||
|
Abort "$(appRunning)"
|
||||||
|
${EndIf}
|
||||||
|
app_check_done:
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
Section Install
|
||||||
|
SetOutPath $INSTDIR
|
||||||
|
|
||||||
|
!insertmacro CheckIfAppIsRunning
|
||||||
|
!insertmacro CheckAllVergeProcesses
|
||||||
|
; Copy main executable
|
||||||
|
File "${MAINBINARYSRCPATH}"
|
||||||
|
|
||||||
|
; Copy resources
|
||||||
|
{{#each resources_dirs}}
|
||||||
|
CreateDirectory "$INSTDIR\\{{this}}"
|
||||||
|
{{/each}}
|
||||||
|
{{#each resources}}
|
||||||
|
File /a "/oname={{this.[1]}}" "{{@key}}"
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
|
; Copy external binaries
|
||||||
|
{{#each binaries}}
|
||||||
|
File /a "/oname={{this}}" "{{@key}}"
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
|
!insertmacro StartVergeService
|
||||||
|
|
||||||
|
; Create uninstaller
|
||||||
|
WriteUninstaller "$INSTDIR\uninstall.exe"
|
||||||
|
|
||||||
|
; Save $INSTDIR in registry for future installations
|
||||||
|
WriteRegStr SHCTX "${MANUPRODUCTKEY}" "" $INSTDIR
|
||||||
|
|
||||||
|
!if "${INSTALLMODE}" == "both"
|
||||||
|
; Save install mode to be selected by default for the next installation such as updating
|
||||||
|
; or when uninstalling
|
||||||
|
WriteRegStr SHCTX "${UNINSTKEY}" $MultiUser.InstallMode 1
|
||||||
|
!endif
|
||||||
|
|
||||||
|
; Registry information for add/remove programs
|
||||||
|
WriteRegStr SHCTX "${UNINSTKEY}" "DisplayName" "${PRODUCTNAME}"
|
||||||
|
WriteRegStr SHCTX "${UNINSTKEY}" "DisplayIcon" "$\"$INSTDIR\${MAINBINARYNAME}.exe$\""
|
||||||
|
WriteRegStr SHCTX "${UNINSTKEY}" "DisplayVersion" "${VERSION}"
|
||||||
|
WriteRegStr SHCTX "${UNINSTKEY}" "Publisher" "${MANUFACTURER}"
|
||||||
|
WriteRegStr SHCTX "${UNINSTKEY}" "InstallLocation" "$\"$INSTDIR$\""
|
||||||
|
WriteRegStr SHCTX "${UNINSTKEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
|
||||||
|
WriteRegDWORD SHCTX "${UNINSTKEY}" "NoModify" "1"
|
||||||
|
WriteRegDWORD SHCTX "${UNINSTKEY}" "NoRepair" "1"
|
||||||
|
WriteRegDWORD SHCTX "${UNINSTKEY}" "EstimatedSize" "${ESTIMATEDSIZE}"
|
||||||
|
|
||||||
|
; Create start menu shortcut (GUI)
|
||||||
|
!insertmacro MUI_STARTMENU_WRITE_BEGIN Application
|
||||||
|
Call CreateStartMenuShortcut
|
||||||
|
!insertmacro MUI_STARTMENU_WRITE_END
|
||||||
|
|
||||||
|
; Create shortcuts for silent and passive installers, which
|
||||||
|
; can be disabled by passing `/NS` flag
|
||||||
|
; GUI installer has buttons for users to control creating them
|
||||||
|
IfSilent check_ns_flag 0
|
||||||
|
${IfThen} $PassiveMode == 1 ${|} Goto check_ns_flag ${|}
|
||||||
|
Goto shortcuts_done
|
||||||
|
check_ns_flag:
|
||||||
|
${GetOptions} $CMDLINE "/NS" $R0
|
||||||
|
IfErrors 0 shortcuts_done
|
||||||
|
Call CreateDesktopShortcut
|
||||||
|
Call CreateStartMenuShortcut
|
||||||
|
shortcuts_done:
|
||||||
|
|
||||||
|
; Auto close this page for passive mode
|
||||||
|
${IfThen} $PassiveMode == 1 ${|} SetAutoClose true ${|}
|
||||||
|
SectionEnd
|
||||||
|
|
||||||
|
Function .onInstSuccess
|
||||||
|
; Check for `/R` flag only in silent and passive installers because
|
||||||
|
; GUI installer has a toggle for the user to (re)start the app
|
||||||
|
IfSilent check_r_flag 0
|
||||||
|
${IfThen} $PassiveMode == 1 ${|} Goto check_r_flag ${|}
|
||||||
|
Goto run_done
|
||||||
|
check_r_flag:
|
||||||
|
${GetOptions} $CMDLINE "/R" $R0
|
||||||
|
IfErrors run_done 0
|
||||||
|
Exec '"$INSTDIR\${MAINBINARYNAME}.exe"'
|
||||||
|
run_done:
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
Function un.onInit
|
||||||
|
!insertmacro SetContext
|
||||||
|
|
||||||
|
!if "${INSTALLMODE}" == "both"
|
||||||
|
!insertmacro MULTIUSER_UNINIT
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!insertmacro MUI_UNGETLANGUAGE
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
Function un.isDirectoryEmpty
|
||||||
|
Exch $0
|
||||||
|
Push $1
|
||||||
|
Push $2
|
||||||
|
StrCpy $2 0
|
||||||
|
FindFirst $1 $2 "$0\*.*"
|
||||||
|
loop:
|
||||||
|
StrCmp $2 "" done
|
||||||
|
StrCmp $2 "." next
|
||||||
|
StrCmp $2 ".." next
|
||||||
|
StrCpy $0 0
|
||||||
|
goto done
|
||||||
|
next:
|
||||||
|
FindNext $1 $2
|
||||||
|
goto loop
|
||||||
|
done:
|
||||||
|
FindClose $1
|
||||||
|
StrCmp $2 "" 0 +2
|
||||||
|
StrCpy $0 1
|
||||||
|
Pop $2
|
||||||
|
Pop $1
|
||||||
|
Exch $0
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
Section Uninstall
|
||||||
|
!insertmacro CheckIfAppIsRunning
|
||||||
|
!insertmacro CheckAllVergeProcesses
|
||||||
|
!insertmacro RemoveVergeService
|
||||||
|
; Delete the app directory and its content from disk
|
||||||
|
; Copy main executable
|
||||||
|
Delete "$INSTDIR\${MAINBINARYNAME}.exe"
|
||||||
|
|
||||||
|
; Delete resources
|
||||||
|
{{#each resources}}
|
||||||
|
Delete "$INSTDIR\\{{this.[1]}}"
|
||||||
|
{{/each}}
|
||||||
|
Delete "$INSTDIR\resources"
|
||||||
|
; Delete external binaries
|
||||||
|
{{#each binaries}}
|
||||||
|
Delete "$INSTDIR\\{{this}}"
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
|
; Delete uninstaller
|
||||||
|
Delete "$INSTDIR\uninstall.exe"
|
||||||
|
|
||||||
|
; Remove InstallDir
|
||||||
|
Push "$INSTDIR"
|
||||||
|
Call un.isDirectoryEmpty
|
||||||
|
Pop $0
|
||||||
|
${If} $0 == 1
|
||||||
|
RMDir /R /REBOOTOK "$INSTDIR"
|
||||||
|
${Else}
|
||||||
|
MessageBox MB_OK "Install Directory is not Empty, Please remove it manually."
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
; Remove start menu shortcut
|
||||||
|
!insertmacro MUI_STARTMENU_GETFOLDER Application $AppStartMenuFolder
|
||||||
|
Delete "$SMPROGRAMS\$AppStartMenuFolder\${MAINBINARYNAME}.lnk"
|
||||||
|
RMDir "$SMPROGRAMS\$AppStartMenuFolder"
|
||||||
|
|
||||||
|
; Remove desktop shortcuts
|
||||||
|
Delete "$DESKTOP\${MAINBINARYNAME}.lnk"
|
||||||
|
|
||||||
|
; Remove registry information for add/remove programs
|
||||||
|
!if "${INSTALLMODE}" == "both"
|
||||||
|
DeleteRegKey SHCTX "${UNINSTKEY}"
|
||||||
|
!else if "${INSTALLMODE}" == "perMachine"
|
||||||
|
DeleteRegKey HKLM "${UNINSTKEY}"
|
||||||
|
!else
|
||||||
|
DeleteRegKey HKCU "${UNINSTKEY}"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
DeleteRegValue HKCU "${MANUPRODUCTKEY}" "Installer Language"
|
||||||
|
|
||||||
|
; Delete app data
|
||||||
|
${If} $DeleteAppDataCheckboxState == 1
|
||||||
|
SetShellVarContext current
|
||||||
|
RmDir /r "$APPDATA\${BUNDLEID}"
|
||||||
|
RmDir /r "$LOCALAPPDATA\${BUNDLEID}"
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
${GetOptions} $CMDLINE "/P" $R0
|
||||||
|
IfErrors +2 0
|
||||||
|
SetAutoClose true
|
||||||
|
SectionEnd
|
||||||
|
|
||||||
|
Function RestorePreviousInstallLocation
|
||||||
|
ReadRegStr $4 SHCTX "${MANUPRODUCTKEY}" ""
|
||||||
|
StrCmp $4 "" +2 0
|
||||||
|
StrCpy $INSTDIR $4
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
Function SkipIfPassive
|
||||||
|
${IfThen} $PassiveMode == 1 ${|} Abort ${|}
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
Function CreateDesktopShortcut
|
||||||
|
CreateShortcut "$DESKTOP\${MAINBINARYNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe"
|
||||||
|
ApplicationID::Set "$DESKTOP\${MAINBINARYNAME}.lnk" "${BUNDLEID}"
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
Function CreateStartMenuShortcut
|
||||||
|
CreateDirectory "$SMPROGRAMS\$AppStartMenuFolder"
|
||||||
|
CreateShortcut "$SMPROGRAMS\$AppStartMenuFolder\${MAINBINARYNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe"
|
||||||
|
ApplicationID::Set "$SMPROGRAMS\$AppStartMenuFolder\${MAINBINARYNAME}.lnk" "${BUNDLEID}"
|
||||||
|
FunctionEnd
|
||||||
BIN
src/assets/fonts/Twemoji.Mozilla.ttf
Normal file
10
src/assets/image/itemicon/connections.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="36" height="36" rx="18" fill="url(#paint0_linear_971_118)"/>
|
||||||
|
<path d="M17.9917 9.66675C13.3917 9.66675 9.66669 13.4001 9.66669 18.0001C9.66669 22.6001 13.3917 26.3334 17.9917 26.3334C22.6 26.3334 26.3334 22.6001 26.3334 18.0001C26.3334 13.4001 22.6 9.66675 17.9917 9.66675ZM23.7667 14.6667H21.3084C21.0417 13.6251 20.6584 12.6251 20.1584 11.7001C21.6917 12.2251 22.9667 13.2917 23.7667 14.6667ZM18 11.3667C18.6917 12.3667 19.2334 13.4751 19.5917 14.6667H16.4084C16.7667 13.4751 17.3084 12.3667 18 11.3667ZM11.55 19.6667C11.4167 19.1334 11.3334 18.5751 11.3334 18.0001C11.3334 17.4251 11.4167 16.8667 11.55 16.3334H14.3667C14.3 16.8834 14.25 17.4334 14.25 18.0001C14.25 18.5667 14.3 19.1167 14.3667 19.6667H11.55ZM12.2334 21.3334H14.6917C14.9584 22.3751 15.3417 23.3751 15.8417 24.3001C14.3084 23.7751 13.0334 22.7167 12.2334 21.3334ZM14.6917 14.6667H12.2334C13.0334 13.2834 14.3084 12.2251 15.8417 11.7001C15.3417 12.6251 14.9584 13.6251 14.6917 14.6667ZM18 24.6334C17.3084 23.6334 16.7667 22.5251 16.4084 21.3334H19.5917C19.2334 22.5251 18.6917 23.6334 18 24.6334ZM19.95 19.6667H16.05C15.975 19.1167 15.9167 18.5667 15.9167 18.0001C15.9167 17.4334 15.975 16.8751 16.05 16.3334H19.95C20.025 16.8751 20.0834 17.4334 20.0834 18.0001C20.0834 18.5667 20.025 19.1167 19.95 19.6667ZM20.1584 24.3001C20.6584 23.3751 21.0417 22.3751 21.3084 21.3334H23.7667C22.9667 22.7084 21.6917 23.7751 20.1584 24.3001ZM21.6334 19.6667C21.7 19.1167 21.75 18.5667 21.75 18.0001C21.75 17.4334 21.7 16.8834 21.6334 16.3334H24.45C24.5834 16.8667 24.6667 17.4251 24.6667 18.0001C24.6667 18.5751 24.5834 19.1334 24.45 19.6667H21.6334Z" fill="white"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear_971_118" x1="31" y1="27.5" x2="6.5" y2="7" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#009038"/>
|
||||||
|
<stop offset="1" stop-color="#1CA350"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
10
src/assets/image/itemicon/logs.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="36" height="36" rx="18" fill="url(#paint0_linear_971_127)"/>
|
||||||
|
<path d="M18.8334 22.1667H12.1667C11.7084 22.1667 11.3334 22.5417 11.3334 23.0001C11.3334 23.4584 11.7084 23.8334 12.1667 23.8334H18.8334C19.2917 23.8334 19.6667 23.4584 19.6667 23.0001C19.6667 22.5417 19.2917 22.1667 18.8334 22.1667ZM23.8334 15.5001H12.1667C11.7084 15.5001 11.3334 15.8751 11.3334 16.3334C11.3334 16.7917 11.7084 17.1667 12.1667 17.1667H23.8334C24.2917 17.1667 24.6667 16.7917 24.6667 16.3334C24.6667 15.8751 24.2917 15.5001 23.8334 15.5001ZM12.1667 20.5001H23.8334C24.2917 20.5001 24.6667 20.1251 24.6667 19.6667C24.6667 19.2084 24.2917 18.8334 23.8334 18.8334H12.1667C11.7084 18.8334 11.3334 19.2084 11.3334 19.6667C11.3334 20.1251 11.7084 20.5001 12.1667 20.5001ZM11.3334 13.0001C11.3334 13.4584 11.7084 13.8334 12.1667 13.8334H23.8334C24.2917 13.8334 24.6667 13.4584 24.6667 13.0001C24.6667 12.5417 24.2917 12.1667 23.8334 12.1667H12.1667C11.7084 12.1667 11.3334 12.5417 11.3334 13.0001Z" fill="white"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear_971_127" x1="6" y1="6.5" x2="29.5" y2="30.5" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#E96038"/>
|
||||||
|
<stop offset="1" stop-color="#E1451D"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |