Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
e80be8e7b6 | ||
|
|
fe0ad0f5cb | ||
|
|
cb8e162f4e | ||
|
|
ae8c30fe57 | ||
|
|
ab82db9e22 | ||
|
|
cb9d3098de | ||
|
|
c927419c99 | ||
|
|
c009026961 | ||
|
|
51cf442fa5 | ||
|
|
954e3553ee | ||
|
|
cde17385b4 | ||
|
|
5b9e078061 | ||
|
|
0290d9ddfc | ||
|
|
eab671d102 | ||
|
|
f9a96ff914 | ||
|
|
3ec2b46d28 | ||
|
|
aec30b89e0 | ||
|
|
309c33e190 | ||
|
|
9c0276f97b | ||
|
|
0a3402ff43 | ||
|
|
bd82308024 | ||
|
|
db3b634e62 | ||
|
|
40bcb22977 | ||
|
|
4bd94092f1 | ||
|
|
cac1ce6895 |
@@ -5,3 +5,9 @@ charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
|
||||
[*.rs]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
insert_final_newline = true
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
name: Bug report
|
||||
description: Create a report to help us improve
|
||||
title: "[BUG]"
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: textarea
|
||||
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: Platform
|
||||
options:
|
||||
- Windows
|
||||
- Linux
|
||||
- MacOS
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: System Version
|
||||
placeholder: "e.g. macOS 10.15.7"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Software Version
|
||||
placeholder: "e.g. 1.4.3"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Log
|
||||
description: "Log file content or screenshot"
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional Information
|
||||
@@ -0,0 +1,27 @@
|
||||
name: Feature request
|
||||
description: Suggest an idea for this project
|
||||
title: "[Feature]"
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Is your feature request related to a problem? Please describe.
|
||||
description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the solution you'd like
|
||||
description: A clear and concise description of what you want to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe alternatives you've considered
|
||||
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context or screenshots about the feature request here.
|
||||
@@ -0,0 +1,4 @@
|
||||
FROM rust:buster
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod a+x /entrypoint.sh
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
@@ -0,0 +1,37 @@
|
||||
#!/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 libssl-dev:arm64 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 libssl-dev:armhf 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
|
||||
else
|
||||
echo "Unknown target: $INPUT_TARGET" && exit 1
|
||||
fi
|
||||
|
||||
bash .github/build-for-linux/build.sh
|
||||
@@ -0,0 +1,169 @@
|
||||
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: Apply Patch
|
||||
if: matrix.target == 'aarch64-pc-windows-msvc'
|
||||
run: |
|
||||
git config --global user.email "clash-verge-rev@github.io"
|
||||
git config --global user.name "clash-verge-rev"
|
||||
git am patches/support-windows-aarch64.patch
|
||||
|
||||
- name: Init Submodule
|
||||
if: matrix.target == 'aarch64-pc-windows-msvc'
|
||||
run: git submodule update --init --recursive
|
||||
|
||||
- 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@v2
|
||||
name: Install pnpm
|
||||
with:
|
||||
version: 8
|
||||
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
|
||||
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: alpha
|
||||
name: "Clash Verge Rev Alpha"
|
||||
body: "More new features are now supported."
|
||||
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: "More new features are now supported."
|
||||
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'
|
||||
## Clash Verge Rev Alpha
|
||||
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
|
||||
@@ -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 update.json
|
||||
run: yarn run release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,159 @@
|
||||
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: Apply Patch
|
||||
if: matrix.target == 'aarch64-pc-windows-msvc'
|
||||
run: |
|
||||
git config --global user.email "clash-verge-rev@github.io"
|
||||
git config --global user.name "clash-verge-rev"
|
||||
git am patches/support-windows-aarch64.patch
|
||||
|
||||
- name: Init Submodule
|
||||
if: matrix.target == 'aarch64-pc-windows-msvc'
|
||||
run: git submodule update --init --recursive
|
||||
|
||||
- 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@v2
|
||||
name: Install pnpm
|
||||
with:
|
||||
version: 8
|
||||
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
|
||||
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 }}
|
||||
@@ -0,0 +1,29 @@
|
||||
name: Updater CI
|
||||
|
||||
on: workflow_dispatch
|
||||
permissions: write-all
|
||||
jobs:
|
||||
release-update:
|
||||
runs-on: ubuntu-latest
|
||||
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 }}
|
||||
@@ -4,3 +4,5 @@ dist
|
||||
dist-ssr
|
||||
*.local
|
||||
update.json
|
||||
scripts/_env.sh
|
||||
.vscode
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
yarn pretty-quick --staged
|
||||
pnpm pretty-quick --staged
|
||||
|
||||
@@ -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!
|
||||
@@ -1,83 +1,80 @@
|
||||
<h1 align="center">
|
||||
<img src="./src/assets/image/logo.png" alt="Clash" width="128" />
|
||||
<br>
|
||||
Clash Verge
|
||||
Continuation of <a href="https://github.com/zzzgydi/clash-verge">Clash Verge</a>
|
||||
<br>
|
||||
</h1>
|
||||
|
||||
<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>
|
||||
|
||||
## Features
|
||||
|
||||
- Full `clash` config supported, Partial `clash premium` config supported.
|
||||
- Profiles management and enhancement (by yaml and Javascript).
|
||||
- System proxy setting and guard.
|
||||
|
||||
## 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.
|
||||
|
||||
## Preview
|
||||
|
||||

|
||||
|
||||
### FAQ
|
||||
|
||||
Refer to [Doc FAQ Page](https://clash-verge-rev.github.io/faq.html)
|
||||
|
||||
## 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
|
||||
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
|
||||
|
||||
Issue and PR welcome!
|
||||
|
||||
## 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.
|
||||
- [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.
|
||||
- [vitejs/vite](https://github.com/vitejs/vite): Next generation frontend tooling. It's fast!
|
||||
|
||||
|
||||
@@ -1,3 +1,701 @@
|
||||
## 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
|
||||
|
||||
### Features
|
||||
|
||||
- supports custom theme color
|
||||
- tun mode setting control the final config
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix transition flickers (macOS)
|
||||
- reduce proxy page render
|
||||
|
||||
---
|
||||
|
||||
## v0.0.26
|
||||
|
||||
### Features
|
||||
|
||||
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 7.3 MiB |
@@ -1,57 +1,75 @@
|
||||
{
|
||||
"name": "clash-verge",
|
||||
"version": "0.0.26",
|
||||
"license": "GPL-3.0",
|
||||
"version": "1.5.6",
|
||||
"license": "GPL-3.0-only",
|
||||
"scripts": {
|
||||
"dev": "tauri dev",
|
||||
"dev:diff": "tauri dev -f verge-dev",
|
||||
"build": "tauri build",
|
||||
"tauri": "tauri",
|
||||
"web:dev": "vite",
|
||||
"web:build": "tsc && vite build",
|
||||
"web:serve": "vite preview",
|
||||
"check": "node scripts/check.mjs",
|
||||
"green": "node scripts/green.mjs",
|
||||
"publish": "node scripts/publish.mjs",
|
||||
"release": "node scripts/release.mjs",
|
||||
"updater": "node scripts/updater.mjs",
|
||||
"portable": "node scripts/portable.mjs",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.8.1",
|
||||
"@emotion/styled": "^11.8.1",
|
||||
"@mui/icons-material": "^5.4.4",
|
||||
"@mui/material": "^5.4.4",
|
||||
"@tauri-apps/api": "^1.0.0-rc.2",
|
||||
"ahooks": "^3.1.13",
|
||||
"axios": "^0.26.0",
|
||||
"dayjs": "^1.10.8",
|
||||
"i18next": "^21.6.14",
|
||||
"monaco-editor": "^0.33.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-i18next": "^11.15.6",
|
||||
"react-router-dom": "^6.2.2",
|
||||
"react-virtuoso": "^2.7.0",
|
||||
"recoil": "^0.6.1",
|
||||
"@dnd-kit/core": "^6.1.0",
|
||||
"@dnd-kit/sortable": "^8.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@emotion/react": "^11.11.3",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"@mui/icons-material": "^5.15.5",
|
||||
"@mui/lab": "5.0.0-alpha.149",
|
||||
"@mui/material": "^5.15.5",
|
||||
"@mui/x-data-grid": "^6.18.7",
|
||||
"@tauri-apps/api": "^1.5.3",
|
||||
"ahooks": "^3.7.8",
|
||||
"axios": "^1.6.5",
|
||||
"dayjs": "1.11.5",
|
||||
"i18next": "^23.7.16",
|
||||
"lodash-es": "^4.17.21",
|
||||
"monaco-editor": "^0.34.1",
|
||||
"nanoid": "^5.0.4",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-error-boundary": "^3.1.4",
|
||||
"react-hook-form": "^7.49.3",
|
||||
"react-i18next": "^13.5.0",
|
||||
"react-router-dom": "^6.21.2",
|
||||
"react-transition-group": "^4.4.5",
|
||||
"react-virtuoso": "^4.6.2",
|
||||
"recoil": "^0.7.7",
|
||||
"snarkdown": "^2.0.0",
|
||||
"swr": "^1.2.1"
|
||||
"swr": "^1.3.0",
|
||||
"tar": "^6.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/github": "^5.0.0",
|
||||
"@tauri-apps/cli": "^1.0.0-rc.7",
|
||||
"@actions/github": "^5.1.1",
|
||||
"@tauri-apps/cli": "^1.5.9",
|
||||
"@types/fs-extra": "^9.0.13",
|
||||
"@types/js-cookie": "^3.0.1",
|
||||
"@types/react": "^17.0.0",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"@vitejs/plugin-react": "^1.2.0",
|
||||
"adm-zip": "^0.5.9",
|
||||
"fs-extra": "^10.0.0",
|
||||
"husky": "^7.0.0",
|
||||
"node-fetch": "^3.2.0",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"sass": "^1.49.7",
|
||||
"typescript": "^4.5.5",
|
||||
"vite": "^2.8.6",
|
||||
"vite-plugin-monaco-editor": "^1.0.10"
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/react": "^18.2.48",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/react-transition-group": "^4.4.10",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"adm-zip": "^0.5.10",
|
||||
"cross-env": "^7.0.3",
|
||||
"fs-extra": "^11.2.0",
|
||||
"https-proxy-agent": "^5.0.1",
|
||||
"husky": "^7.0.4",
|
||||
"node-fetch": "^3.3.2",
|
||||
"prettier": "^2.8.8",
|
||||
"pretty-quick": "^3.3.1",
|
||||
"sass": "^1.70.0",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.11",
|
||||
"vite-plugin-monaco-editor": "^1.1.0",
|
||||
"vite-plugin-svgr": "^4.2.0"
|
||||
},
|
||||
"prettier": {
|
||||
"tabWidth": 2,
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
From 871c9a6d1ed014c93da2436a437df03734e9f76c Mon Sep 17 00:00:00 2001
|
||||
From: MystiPanda <mystipanda@proton.me>
|
||||
Date: Sun, 10 Dec 2023 19:47:45 +0800
|
||||
Subject: [PATCH] feat: Support windows aarch64
|
||||
|
||||
---
|
||||
.gitmodules | 3 +
|
||||
src-tauri/Cargo.toml | 2 +-
|
||||
src-tauri/quick-rs | 1 +
|
||||
src-tauri/src/enhance/script.rs | 130 +++++++++++++++++++-------------
|
||||
4 files changed, 81 insertions(+), 55 deletions(-)
|
||||
create mode 100644 .gitmodules
|
||||
create mode 160000 src-tauri/quick-rs
|
||||
|
||||
diff --git a/.gitmodules b/.gitmodules
|
||||
new file mode 100644
|
||||
index 0000000..2eda7e4
|
||||
--- /dev/null
|
||||
+++ b/.gitmodules
|
||||
@@ -0,0 +1,3 @@
|
||||
+[submodule "src-tauri/quick-rs"]
|
||||
+ path = src-tauri/quick-rs
|
||||
+ url = https://github.com/clash-verge-rev/quick-rs.git
|
||||
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
|
||||
index 2f1a3be..d67f6ed 100644
|
||||
--- a/src-tauri/Cargo.toml
|
||||
+++ b/src-tauri/Cargo.toml
|
||||
@@ -25,7 +25,6 @@ log4rs = "1"
|
||||
nanoid = "0.4"
|
||||
chrono = "0.4"
|
||||
sysinfo = "0.30"
|
||||
-rquickjs = "0.3" # 高版本不支持 Linux aarch64
|
||||
serde_json = "1.0"
|
||||
serde_yaml = "0.9"
|
||||
once_cell = "1.18"
|
||||
@@ -33,6 +32,7 @@ port_scanner = "0.1.5"
|
||||
delay_timer = "0.11.5"
|
||||
parking_lot = "0.12"
|
||||
percent-encoding = "2.3.1"
|
||||
+quick-rs = { path = "quick-rs" }
|
||||
window-shadows = { version = "0.2" }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
diff --git a/src-tauri/quick-rs b/src-tauri/quick-rs
|
||||
new file mode 160000
|
||||
index 0000000..78277c4
|
||||
--- /dev/null
|
||||
+++ b/src-tauri/quick-rs
|
||||
@@ -0,0 +1 @@
|
||||
+Subproject commit 78277c4509c64f18c0fc5c9f2b84671de7c83343
|
||||
diff --git a/src-tauri/src/enhance/script.rs b/src-tauri/src/enhance/script.rs
|
||||
index 30a922f..d47dc33 100644
|
||||
--- a/src-tauri/src/enhance/script.rs
|
||||
+++ b/src-tauri/src/enhance/script.rs
|
||||
@@ -3,61 +3,83 @@ use anyhow::Result;
|
||||
use serde_yaml::Mapping;
|
||||
|
||||
pub fn use_script(script: String, config: Mapping) -> Result<(Mapping, Vec<(String, String)>)> {
|
||||
- use rquickjs::{function::Func, Context, Runtime};
|
||||
- use std::sync::{Arc, Mutex};
|
||||
-
|
||||
- let runtime = Runtime::new().unwrap();
|
||||
- let context = Context::full(&runtime).unwrap();
|
||||
- let outputs = Arc::new(Mutex::new(vec![]));
|
||||
-
|
||||
- let copy_outputs = outputs.clone();
|
||||
- let result = context.with(|ctx| -> Result<Mapping> {
|
||||
- ctx.globals().set(
|
||||
- "__verge_log__",
|
||||
- Func::from(move |level: String, data: String| {
|
||||
- let mut out = copy_outputs.lock().unwrap();
|
||||
- out.push((level, data));
|
||||
- }),
|
||||
- )?;
|
||||
-
|
||||
- ctx.eval(
|
||||
- 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{{
|
||||
+ use quick_rs::{context::Context, function::Function, module::Module, runtime::Runtime};
|
||||
+
|
||||
+ let config = use_lowercase(config.clone());
|
||||
+ let config_str = serde_json::to_string(&config)?;
|
||||
+
|
||||
+ let runtime = Runtime::new();
|
||||
+ let context = Context::from(&runtime);
|
||||
+
|
||||
+ let code = format!(
|
||||
+ r#"
|
||||
+ let output = [];
|
||||
+
|
||||
+ function __verge_log__(type, data) {{
|
||||
+ output.push([type, data]);
|
||||
+ }}
|
||||
+
|
||||
+ 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)) }},
|
||||
+ }});
|
||||
+
|
||||
{script};
|
||||
- JSON.stringify(main({config_str})||'')
|
||||
- }} catch(err) {{
|
||||
- `__error_flag__ ${{err.toString()}}`
|
||||
- }}"#
|
||||
- );
|
||||
- let result: String = ctx.eval(code.as_str())?;
|
||||
- if result.starts_with("__error_flag__") {
|
||||
- anyhow::bail!(result[15..].to_owned());
|
||||
- }
|
||||
- if result == "\"\"" {
|
||||
- anyhow::bail!("main function should return object");
|
||||
- }
|
||||
- Ok(serde_json::from_str::<Mapping>(result.as_str())?)
|
||||
- });
|
||||
-
|
||||
- let mut out = outputs.lock().unwrap();
|
||||
- match result {
|
||||
- Ok(config) => Ok((use_lowercase(config), out.to_vec())),
|
||||
- Err(err) => {
|
||||
- out.push(("exception".into(), err.to_string()));
|
||||
- Ok((config, out.to_vec()))
|
||||
- }
|
||||
- }
|
||||
+
|
||||
+ export function _main(){{
|
||||
+ try{{
|
||||
+ let result = JSON.stringify(main({config_str})||"");
|
||||
+ return JSON.stringify({{result, output}});
|
||||
+ }} catch(err) {{
|
||||
+ output.push(["exception", err.toString()]);
|
||||
+ return JSON.stringify({{result: "__error__", output}});
|
||||
+ }}
|
||||
+ }}
|
||||
+ "#
|
||||
+ );
|
||||
+ let value = context.eval_module(&code, "_main")?;
|
||||
+ let module = Module::new(value)?;
|
||||
+ let value = module.get("_main")?;
|
||||
+ let function = Function::new(value)?;
|
||||
+ let value = function.call(vec![])?;
|
||||
+ let result = serde_json::from_str::<serde_json::Value>(&value.to_string()?)?;
|
||||
+ result
|
||||
+ .as_object()
|
||||
+ .map(|obj| {
|
||||
+ let result = obj.get("result").unwrap().as_str().unwrap();
|
||||
+ let output = obj.get("output").unwrap();
|
||||
+
|
||||
+ let mut out = output
|
||||
+ .as_array()
|
||||
+ .unwrap()
|
||||
+ .iter()
|
||||
+ .map(|item| {
|
||||
+ let item = item.as_array().unwrap();
|
||||
+ (
|
||||
+ item[0].as_str().unwrap().into(),
|
||||
+ item[1].as_str().unwrap().into(),
|
||||
+ )
|
||||
+ })
|
||||
+ .collect::<Vec<_>>();
|
||||
+ if result.is_empty() {
|
||||
+ anyhow::bail!("main function should return object");
|
||||
+ }
|
||||
+ if result == "__error__" {
|
||||
+ return Ok((config, out.to_vec()));
|
||||
+ }
|
||||
+ let result = serde_json::from_str::<Mapping>(result);
|
||||
+
|
||||
+ match result {
|
||||
+ Ok(config) => Ok((use_lowercase(config), out.to_vec())),
|
||||
+ Err(err) => {
|
||||
+ out.push(("exception".into(), err.to_string()));
|
||||
+ Ok((config, out.to_vec()))
|
||||
+ }
|
||||
+ }
|
||||
+ })
|
||||
+ .unwrap_or_else(|| anyhow::bail!("Unknown result"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
--
|
||||
2.43.0.windows.1
|
||||
|
||||
@@ -1,159 +1,313 @@
|
||||
import fs from "fs-extra";
|
||||
import zlib from "zlib";
|
||||
import tar from "tar";
|
||||
import path from "path";
|
||||
import AdmZip from "adm-zip";
|
||||
import fetch from "node-fetch";
|
||||
import proxyAgent from "https-proxy-agent";
|
||||
import { execSync } from "child_process";
|
||||
|
||||
const cwd = process.cwd();
|
||||
const TEMP_DIR = path.join(cwd, "node_modules/.verge");
|
||||
const FORCE = process.argv.includes("--force");
|
||||
|
||||
/**
|
||||
* get the correct clash release infomation
|
||||
*/
|
||||
function resolveClash() {
|
||||
const { platform, arch } = process;
|
||||
const PLATFORM_MAP = {
|
||||
"x86_64-pc-windows-msvc": "win32",
|
||||
"i686-pc-windows-msvc": "win32",
|
||||
"aarch64-pc-windows-msvc": "win32",
|
||||
"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",
|
||||
"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",
|
||||
"loongarch64-unknown-linux-gnu": "loong64",
|
||||
};
|
||||
|
||||
const CLASH_URL_PREFIX =
|
||||
"https://github.com/Dreamacro/clash/releases/download/premium/";
|
||||
const CLASH_LATEST_DATE = "2022.03.21";
|
||||
const arg1 = process.argv.slice(2)[0];
|
||||
const arg2 = process.argv.slice(2)[1];
|
||||
const target = arg1 === "--force" ? arg2 : arg1;
|
||||
const { platform, arch } = target
|
||||
? { platform: PLATFORM_MAP[target], arch: ARCH_MAP[target] }
|
||||
: process;
|
||||
|
||||
// todo
|
||||
const map = {
|
||||
"win32-x64": "clash-windows-amd64",
|
||||
"darwin-x64": "clash-darwin-amd64",
|
||||
"darwin-arm64": "clash-darwin-arm64",
|
||||
"linux-x64": "clash-linux-amd64",
|
||||
};
|
||||
const SIDECAR_HOST = target
|
||||
? target
|
||||
: execSync("rustc -vV")
|
||||
.toString()
|
||||
.match(/(?<=host: ).+(?=\s*)/g)[0];
|
||||
|
||||
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) {
|
||||
throw new Error(`unsupport platform "${platform}-${arch}"`);
|
||||
const META_ALPHA_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-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";
|
||||
const zip = isWin ? "zip" : "gz";
|
||||
const url = `${CLASH_URL_PREFIX}${name}-${CLASH_LATEST_DATE}.${zip}`;
|
||||
const exefile = `${name}${isWin ? ".exe" : ""}`;
|
||||
const zipfile = `${name}.${zip}`;
|
||||
/* ======= clash meta stable ======= */
|
||||
const META_VERSION_URL =
|
||||
"https://github.com/MetaCubeX/mihomo/releases/latest/download/version.txt";
|
||||
const META_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/download`;
|
||||
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-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() {
|
||||
const sidecarDir = path.join(cwd, "src-tauri", "sidecar");
|
||||
function clashMetaAlpha() {
|
||||
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();
|
||||
const ext = process.platform === "win32" ? ".exe" : "";
|
||||
const sidecarFile = `clash-${host}${ext}`;
|
||||
const sidecarPath = path.join(sidecarDir, sidecarFile);
|
||||
return {
|
||||
name: "clash-meta-alpha",
|
||||
targetFile: `clash-meta-alpha-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
|
||||
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);
|
||||
if (!FORCE && (await fs.pathExists(sidecarPath))) return;
|
||||
|
||||
// download sidecar
|
||||
const binInfo = resolveClash();
|
||||
const tempDir = path.join(cwd, "pre-dev-temp");
|
||||
const tempZip = path.join(tempDir, binInfo.zipfile);
|
||||
const tempExe = path.join(tempDir, binInfo.exefile);
|
||||
const tempDir = path.join(TEMP_DIR, name);
|
||||
const tempZip = path.join(tempDir, zipFile);
|
||||
const tempExe = path.join(tempDir, exeFile);
|
||||
|
||||
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") {
|
||||
const zip = new AdmZip(tempZip);
|
||||
zip.getEntries().forEach((entry) => {
|
||||
console.log("[INFO]: entry name", entry.entryName);
|
||||
});
|
||||
zip.extractAllTo(tempDir, true);
|
||||
// save as sidecar
|
||||
await fs.rename(tempExe, sidecarPath);
|
||||
console.log(`[INFO]: unzip finished`);
|
||||
} else {
|
||||
// gz
|
||||
const readStream = fs.createReadStream(tempZip);
|
||||
const writeStream = fs.createWriteStream(sidecarPath);
|
||||
readStream
|
||||
.pipe(zlib.createGunzip())
|
||||
.pipe(writeStream)
|
||||
.on("finish", () => {
|
||||
console.log(`[INFO]: gunzip finished`);
|
||||
if (zipFile.endsWith(".zip")) {
|
||||
const zip = new AdmZip(tempZip);
|
||||
zip.getEntries().forEach((entry) => {
|
||||
console.log(`[DEBUG]: "${name}" entry name`, entry.entryName);
|
||||
});
|
||||
zip.extractAllTo(tempDir, true);
|
||||
await fs.rename(tempExe, sidecarPath);
|
||||
console.log(`[INFO]: "${name}" unzip finished`);
|
||||
} else if (zipFile.endsWith(".tgz")) {
|
||||
// tgz
|
||||
await fs.mkdirp(tempDir);
|
||||
await tar.extract({
|
||||
cwd: tempDir,
|
||||
file: tempZip,
|
||||
//strip: 1, // 可能需要根据实际的 .tgz 文件结构调整
|
||||
});
|
||||
const files = await fs.readdir(tempDir);
|
||||
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}`);
|
||||
console.log(`[INFO]: chmod binary finished`);
|
||||
})
|
||||
.on("error", (error) => console.error(error));
|
||||
console.log(`[INFO]: "${name}" chmod binary finished`);
|
||||
} else {
|
||||
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
|
||||
* get the wintun.dll (not required)
|
||||
* download the file to the resources dir
|
||||
*/
|
||||
async function resolveWintun() {
|
||||
const { platform } = process;
|
||||
async function resolveResource(binInfo) {
|
||||
const { file, downloadURL } = binInfo;
|
||||
|
||||
if (platform !== "win32") return;
|
||||
|
||||
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");
|
||||
const resDir = path.join(cwd, "src-tauri/resources");
|
||||
const targetPath = path.join(resDir, file);
|
||||
|
||||
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 downloadFile(url, resPath);
|
||||
await downloadFile(downloadURL, targetPath);
|
||||
|
||||
console.log(`[INFO]: ${file} finished`);
|
||||
}
|
||||
|
||||
/**
|
||||
* download file and save to `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, {
|
||||
...options,
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/octet-stream" },
|
||||
});
|
||||
@@ -163,7 +317,125 @@ async function downloadFile(url, path) {
|
||||
console.log(`[INFO]: download finished "${url}"`);
|
||||
}
|
||||
|
||||
/// main
|
||||
resolveSidecar().catch(console.error);
|
||||
resolveWintun().catch(console.error);
|
||||
resolveMmdb().catch(console.error);
|
||||
// SimpleSC.dll
|
||||
const resolvePlugin = async () => {
|
||||
const url =
|
||||
"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);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* main
|
||||
*/
|
||||
|
||||
const SERVICE_URL = `https://github.com/clash-verge-rev/clash-verge-service/releases/download/${SIDECAR_HOST}`;
|
||||
|
||||
const resolveService = () =>
|
||||
resolveResource({
|
||||
file: "clash-verge-service.exe",
|
||||
downloadURL: `${SERVICE_URL}/clash-verge-service.exe`,
|
||||
});
|
||||
const resolveInstall = () =>
|
||||
resolveResource({
|
||||
file: "install-service.exe",
|
||||
downloadURL: `${SERVICE_URL}/install-service.exe`,
|
||||
});
|
||||
const resolveUninstall = () =>
|
||||
resolveResource({
|
||||
file: "uninstall-service.exe",
|
||||
downloadURL: `${SERVICE_URL}/uninstall-service.exe`,
|
||||
});
|
||||
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, winOnly: true },
|
||||
{ name: "install", func: resolveInstall, retry: 5, winOnly: true },
|
||||
{ name: "uninstall", func: resolveUninstall, retry: 5, winOnly: true },
|
||||
{ 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,
|
||||
},
|
||||
];
|
||||
|
||||
async function runTask() {
|
||||
const task = tasks.shift();
|
||||
if (!task) return;
|
||||
if (task.winOnly && process.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 { 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
|
||||
/// 打包绿色版/便携版 (only Windows)
|
||||
async function resolveGreen() {
|
||||
async function resolvePortable() {
|
||||
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))) {
|
||||
throw new Error("could not found the release dir");
|
||||
}
|
||||
|
||||
await fs.mkdir(configDir);
|
||||
await fs.createFile(path.join(configDir, "PORTABLE"));
|
||||
|
||||
const zip = new AdmZip();
|
||||
|
||||
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(configDir, ".config");
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const packageJson = require("../package.json");
|
||||
const { version } = packageJson;
|
||||
|
||||
const zipFile = `Clash.Verge_${version}_x64_green.zip`;
|
||||
const zipFile = `Clash.Verge_${version}_${ARCH_MAP[target]}_portable.zip`;
|
||||
zip.writeZip(zipFile);
|
||||
|
||||
console.log("[INFO]: create green zip successfully");
|
||||
console.log("[INFO]: create portable zip successfully");
|
||||
|
||||
// push release assets
|
||||
if (process.env.GITHUB_TOKEN === undefined) {
|
||||
@@ -37,12 +53,25 @@ async function resolveGreen() {
|
||||
|
||||
const options = { owner: context.repo.owner, repo: context.repo.repo };
|
||||
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({
|
||||
...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);
|
||||
|
||||
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();
|
||||
@@ -1,164 +0,0 @@
|
||||
import fetch from "node-fetch";
|
||||
import { getOctokit, context } from "@actions/github";
|
||||
import { resolveUpdateLog } from "./updatelog.mjs";
|
||||
|
||||
const UPDATE_TAG_NAME = "updater";
|
||||
const UPDATE_JSON_FILE = "update.json";
|
||||
const UPDATE_JSON_PROXY = "update-proxy.json";
|
||||
|
||||
/// generate update.json
|
||||
/// upload to update tag's release asset
|
||||
async function resolveRelease() {
|
||||
if (process.env.GITHUB_TOKEN === undefined) {
|
||||
throw new Error("GITHUB_TOKEN is required");
|
||||
}
|
||||
|
||||
const options = { owner: context.repo.owner, repo: context.repo.repo };
|
||||
const github = getOctokit(process.env.GITHUB_TOKEN);
|
||||
|
||||
const { data: tags } = await github.rest.repos.listTags({
|
||||
...options,
|
||||
per_page: 10,
|
||||
page: 1,
|
||||
});
|
||||
|
||||
// get the latest publish tag
|
||||
const tag = tags.find((t) => t.name.startsWith("v"));
|
||||
|
||||
console.log(tag);
|
||||
console.log();
|
||||
|
||||
const { data: latestRelease } = await github.rest.repos.getReleaseByTag({
|
||||
...options,
|
||||
tag: tag.name,
|
||||
});
|
||||
|
||||
const updateData = {
|
||||
name: tag.name,
|
||||
notes: await resolveUpdateLog(tag.name), // use updatelog.md
|
||||
pub_date: new Date().toISOString(),
|
||||
platforms: {
|
||||
win64: { signature: "", url: "" },
|
||||
linux: { signature: "", url: "" },
|
||||
darwin: { signature: "", url: "" },
|
||||
},
|
||||
};
|
||||
|
||||
const promises = latestRelease.assets.map(async (asset) => {
|
||||
const { name, browser_download_url } = asset;
|
||||
|
||||
// win64 url
|
||||
if (/\.msi\.zip$/.test(name)) {
|
||||
updateData.platforms.win64.url = browser_download_url;
|
||||
}
|
||||
// win64 signature
|
||||
if (/\.msi\.zip\.sig$/.test(name)) {
|
||||
updateData.platforms.win64.signature = await getSignature(
|
||||
browser_download_url
|
||||
);
|
||||
}
|
||||
|
||||
// darwin url
|
||||
if (/\.app\.tar\.gz$/.test(name)) {
|
||||
updateData.platforms.darwin.url = browser_download_url;
|
||||
}
|
||||
// darwin signature
|
||||
if (/\.app\.tar\.gz\.sig$/.test(name)) {
|
||||
updateData.platforms.darwin.signature = await getSignature(
|
||||
browser_download_url
|
||||
);
|
||||
}
|
||||
|
||||
// linux url
|
||||
if (/\.AppImage\.tar\.gz$/.test(name)) {
|
||||
updateData.platforms.linux.url = browser_download_url;
|
||||
}
|
||||
// linux signature
|
||||
if (/\.AppImage\.tar\.gz\.sig$/.test(name)) {
|
||||
updateData.platforms.linux.signature = await getSignature(
|
||||
browser_download_url
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
console.log(updateData);
|
||||
|
||||
// maybe should test the signature as well
|
||||
const { darwin, win64, linux } = updateData.platforms;
|
||||
if (!darwin.url) {
|
||||
console.log(`[Error]: failed to parse release for darwin`);
|
||||
delete updateData.platforms.darwin;
|
||||
}
|
||||
if (!win64.url) {
|
||||
console.log(`[Error]: failed to parse release for win64`);
|
||||
delete updateData.platforms.win64;
|
||||
}
|
||||
if (!linux.url) {
|
||||
console.log(`[Error]: failed to parse release for linux`);
|
||||
delete updateData.platforms.linux;
|
||||
}
|
||||
|
||||
// 生成一个代理github的更新文件
|
||||
// 使用 https://hub.fastgit.xyz/ 做github资源的加速
|
||||
const updateDataNew = JSON.parse(JSON.stringify(updateData));
|
||||
|
||||
Object.keys(updateDataNew.platforms).forEach((key) => {
|
||||
if (updateDataNew.platforms[key]) {
|
||||
updateDataNew.platforms[key].url = updateDataNew.platforms[
|
||||
key
|
||||
].url.replace("https://github.com/", "https://hub.fastgit.xyz/");
|
||||
} else {
|
||||
console.log(`[Error]: updateDataNew.platforms.${key} is null`);
|
||||
}
|
||||
});
|
||||
|
||||
// update the update.json
|
||||
const { data: updateRelease } = await github.rest.repos.getReleaseByTag({
|
||||
...options,
|
||||
tag: UPDATE_TAG_NAME,
|
||||
});
|
||||
|
||||
// delete the old assets
|
||||
for (let asset of updateRelease.assets) {
|
||||
if (asset.name === UPDATE_JSON_FILE) {
|
||||
await github.rest.repos.deleteReleaseAsset({
|
||||
...options,
|
||||
asset_id: asset.id,
|
||||
});
|
||||
}
|
||||
|
||||
if (asset.name === UPDATE_JSON_PROXY) {
|
||||
await github.rest.repos
|
||||
.deleteReleaseAsset({ ...options, asset_id: asset.id })
|
||||
.catch(console.error); // do not break the pipeline
|
||||
}
|
||||
}
|
||||
|
||||
// upload assets
|
||||
await github.rest.repos.uploadReleaseAsset({
|
||||
...options,
|
||||
release_id: updateRelease.id,
|
||||
name: UPDATE_JSON_FILE,
|
||||
data: JSON.stringify(updateData, null, 2),
|
||||
});
|
||||
|
||||
await github.rest.repos.uploadReleaseAsset({
|
||||
...options,
|
||||
release_id: updateRelease.id,
|
||||
name: UPDATE_JSON_PROXY,
|
||||
data: JSON.stringify(updateDataNew, null, 2),
|
||||
});
|
||||
}
|
||||
|
||||
// get the signature file content
|
||||
async function getSignature(url) {
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/octet-stream" },
|
||||
});
|
||||
|
||||
return response.text();
|
||||
}
|
||||
|
||||
resolveRelease().catch(console.error);
|
||||
@@ -0,0 +1,196 @@
|
||||
import fetch from "node-fetch";
|
||||
import { getOctokit, context } from "@actions/github";
|
||||
import { resolveUpdateLog } from "./updatelog.mjs";
|
||||
|
||||
const UPDATE_TAG_NAME = "updater";
|
||||
const UPDATE_JSON_FILE = "update.json";
|
||||
const UPDATE_JSON_PROXY = "update-proxy.json";
|
||||
|
||||
/// generate update.json
|
||||
/// upload to update tag's release asset
|
||||
async function resolveUpdater() {
|
||||
if (process.env.GITHUB_TOKEN === undefined) {
|
||||
throw new Error("GITHUB_TOKEN is required");
|
||||
}
|
||||
|
||||
const options = { owner: context.repo.owner, repo: context.repo.repo };
|
||||
const github = getOctokit(process.env.GITHUB_TOKEN);
|
||||
|
||||
const { data: tags } = await github.rest.repos.listTags({
|
||||
...options,
|
||||
per_page: 10,
|
||||
page: 1,
|
||||
});
|
||||
|
||||
// get the latest publish tag
|
||||
const tag = tags.find((t) => t.name.startsWith("v"));
|
||||
|
||||
console.log(tag);
|
||||
console.log();
|
||||
|
||||
const { data: latestRelease } = await github.rest.repos.getReleaseByTag({
|
||||
...options,
|
||||
tag: tag.name,
|
||||
});
|
||||
|
||||
const updateData = {
|
||||
name: tag.name,
|
||||
notes: await resolveUpdateLog(tag.name), // use updatelog.md
|
||||
pub_date: new Date().toISOString(),
|
||||
platforms: {
|
||||
win64: { signature: "", url: "" }, // compatible with older formats
|
||||
linux: { signature: "", url: "" }, // compatible with older formats
|
||||
darwin: { signature: "", url: "" }, // compatible with older formats
|
||||
"darwin-aarch64": { signature: "", url: "" },
|
||||
"darwin-intel": { signature: "", url: "" },
|
||||
"darwin-x86_64": { signature: "", url: "" },
|
||||
"linux-x86_64": { signature: "", url: "" },
|
||||
"linux-aarch64": { signature: "", url: "" },
|
||||
"linux-armv7": { signature: "", url: "" },
|
||||
"windows-x86_64": { signature: "", url: "" },
|
||||
"windows-aarch64": { signature: "", url: "" },
|
||||
},
|
||||
};
|
||||
|
||||
const promises = latestRelease.assets.map(async (asset) => {
|
||||
const { name, browser_download_url } = asset;
|
||||
|
||||
// win64 url
|
||||
if (name.endsWith("x64-setup.nsis.zip")) {
|
||||
updateData.platforms.win64.url = browser_download_url;
|
||||
updateData.platforms["windows-x86_64"].url = browser_download_url;
|
||||
}
|
||||
// win64 signature
|
||||
if (name.endsWith("x64-setup.nsis.zip.sig")) {
|
||||
const sig = await getSignature(browser_download_url);
|
||||
updateData.platforms.win64.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)
|
||||
if (name.endsWith(".app.tar.gz") && !name.includes("aarch")) {
|
||||
updateData.platforms.darwin.url = browser_download_url;
|
||||
updateData.platforms["darwin-intel"].url = browser_download_url;
|
||||
updateData.platforms["darwin-x86_64"].url = browser_download_url;
|
||||
}
|
||||
// darwin signature (intel)
|
||||
if (name.endsWith(".app.tar.gz.sig") && !name.includes("aarch")) {
|
||||
const sig = await getSignature(browser_download_url);
|
||||
updateData.platforms.darwin.signature = sig;
|
||||
updateData.platforms["darwin-intel"].signature = sig;
|
||||
updateData.platforms["darwin-x86_64"].signature = sig;
|
||||
}
|
||||
|
||||
// darwin url (aarch)
|
||||
if (name.endsWith("aarch64.app.tar.gz")) {
|
||||
updateData.platforms["darwin-aarch64"].url = browser_download_url;
|
||||
}
|
||||
// darwin signature (aarch)
|
||||
if (name.endsWith("aarch64.app.tar.gz.sig")) {
|
||||
const sig = await getSignature(browser_download_url);
|
||||
updateData.platforms["darwin-aarch64"].signature = sig;
|
||||
}
|
||||
|
||||
// linux x64 url
|
||||
if (name.endsWith("amd64.AppImage.tar.gz")) {
|
||||
updateData.platforms.linux.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 x64 signature
|
||||
if (name.endsWith("amd64.AppImage.tar.gz.sig")) {
|
||||
const sig = await getSignature(browser_download_url);
|
||||
updateData.platforms.linux.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;
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
console.log(updateData);
|
||||
|
||||
// maybe should test the signature as well
|
||||
// delete the null field
|
||||
Object.entries(updateData.platforms).forEach(([key, value]) => {
|
||||
if (!value.url) {
|
||||
console.log(`[Error]: failed to parse release for "${key}"`);
|
||||
delete updateData.platforms[key];
|
||||
}
|
||||
});
|
||||
|
||||
// 生成一个代理github的更新文件
|
||||
// 使用 https://hub.fastgit.xyz/ 做github资源的加速
|
||||
const updateDataNew = JSON.parse(JSON.stringify(updateData));
|
||||
|
||||
Object.entries(updateDataNew.platforms).forEach(([key, value]) => {
|
||||
if (value.url) {
|
||||
updateDataNew.platforms[key].url =
|
||||
"https://mirror.ghproxy.com/" + value.url;
|
||||
} else {
|
||||
console.log(`[Error]: updateDataNew.platforms.${key} is null`);
|
||||
}
|
||||
});
|
||||
|
||||
// update the update.json
|
||||
const { data: updateRelease } = await github.rest.repos.getReleaseByTag({
|
||||
...options,
|
||||
tag: UPDATE_TAG_NAME,
|
||||
});
|
||||
|
||||
// delete the old assets
|
||||
for (let asset of updateRelease.assets) {
|
||||
if (asset.name === UPDATE_JSON_FILE) {
|
||||
await github.rest.repos.deleteReleaseAsset({
|
||||
...options,
|
||||
asset_id: asset.id,
|
||||
});
|
||||
}
|
||||
|
||||
if (asset.name === UPDATE_JSON_PROXY) {
|
||||
await github.rest.repos
|
||||
.deleteReleaseAsset({ ...options, asset_id: asset.id })
|
||||
.catch(console.error); // do not break the pipeline
|
||||
}
|
||||
}
|
||||
|
||||
// upload new assets
|
||||
await github.rest.repos.uploadReleaseAsset({
|
||||
...options,
|
||||
release_id: updateRelease.id,
|
||||
name: UPDATE_JSON_FILE,
|
||||
data: JSON.stringify(updateData, null, 2),
|
||||
});
|
||||
|
||||
await github.rest.repos.uploadReleaseAsset({
|
||||
...options,
|
||||
release_id: updateRelease.id,
|
||||
name: UPDATE_JSON_PROXY,
|
||||
data: JSON.stringify(updateDataNew, null, 2),
|
||||
});
|
||||
}
|
||||
|
||||
// get the signature file content
|
||||
async function getSignature(url) {
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/octet-stream" },
|
||||
});
|
||||
|
||||
return response.text();
|
||||
}
|
||||
|
||||
resolveUpdater().catch(console.error);
|
||||
@@ -1,46 +1,57 @@
|
||||
[package]
|
||||
name = "clash-verge"
|
||||
version = "0.1.0"
|
||||
version = "1.5.6"
|
||||
description = "clash verge"
|
||||
authors = ["zzzgydi"]
|
||||
license = "GPL-3.0"
|
||||
repository = "https://github.com/zzzgydi/clash-verge.git"
|
||||
authors = ["zzzgydi", "wonfen", "MystiPanda"]
|
||||
license = "GPL-3.0-only"
|
||||
repository = "https://github.com/clash-verge-rev/clash-verge-rev.git"
|
||||
default-run = "clash-verge"
|
||||
edition = "2021"
|
||||
build = "build.rs"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "1.0.0-rc.4", features = [] }
|
||||
tauri-build = { version = "1", features = [] }
|
||||
|
||||
[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.4", 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"
|
||||
which = "4.2.2"
|
||||
auto-launch = "0.2"
|
||||
which = "6.0.0"
|
||||
anyhow = "1.0"
|
||||
dirs = "5.0"
|
||||
open = "5.0"
|
||||
log = "0.4"
|
||||
ctrlc = "3.4"
|
||||
dunce = "1.0"
|
||||
log4rs = "1"
|
||||
nanoid = "0.4"
|
||||
chrono = "0.4"
|
||||
sysinfo = "0.30"
|
||||
rquickjs = "0.3" # 高版本不支持 Linux aarch64
|
||||
serde_json = "1.0"
|
||||
serde_yaml = "0.9"
|
||||
once_cell = "1.18"
|
||||
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.11", 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.5", features = [ "path-all", "protocol-asset", "dialog-open", "notification-all", "icon-png", "clipboard-all", "global-shortcut-all", "process-all", "shell-all", "system-tray", "updater", "window-all"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winreg = { version = "0.10", features = ["transactions"] }
|
||||
runas = "=1.0.0" # 高版本会返回错误 Status
|
||||
deelevate = "0.2.0"
|
||||
winreg = "0.52.0"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
#openssl
|
||||
|
||||
[features]
|
||||
default = [ "custom-protocol" ]
|
||||
custom-protocol = [ "tauri/custom-protocol" ]
|
||||
default = ["custom-protocol"]
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
verge-dev = []
|
||||
|
||||
[profile.release]
|
||||
|
||||
@@ -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() {
|
||||
tauri_build::build()
|
||||
tauri_build::build()
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
@@ -1,6 +1,6 @@
|
||||
max_width = 100
|
||||
hard_tabs = false
|
||||
tab_spaces = 2
|
||||
tab_spaces = 4
|
||||
newline_style = "Auto"
|
||||
use_small_heuristics = "Default"
|
||||
reorder_imports = true
|
||||
|
||||
@@ -1,363 +1,353 @@
|
||||
use crate::{
|
||||
core::{ClashInfo, PrfItem, PrfOption, Profiles, VergeConfig},
|
||||
states::{ClashState, ProfilesState, VergeState},
|
||||
utils::{dirs, sysopt::SysProxyConfig},
|
||||
config::*,
|
||||
core::*,
|
||||
feat,
|
||||
utils::{dirs, help, resolve},
|
||||
};
|
||||
use crate::{ret_err, wrap_err};
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context, Result};
|
||||
use serde_yaml::Mapping;
|
||||
use std::process::Command;
|
||||
use tauri::{api, Manager, State};
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use sysproxy::Sysproxy;
|
||||
use tauri::api;
|
||||
type CmdResult<T = ()> = Result<T, String>;
|
||||
|
||||
/// get all profiles from `profiles.yaml`
|
||||
#[tauri::command]
|
||||
pub fn get_profiles<'a>(profiles_state: State<'_, ProfilesState>) -> Result<Profiles, String> {
|
||||
let profiles = profiles_state.0.lock().unwrap();
|
||||
Ok(profiles.clone())
|
||||
pub fn get_profiles() -> CmdResult<IProfiles> {
|
||||
Ok(Config::profiles().data().clone())
|
||||
}
|
||||
|
||||
/// synchronize data irregularly
|
||||
#[tauri::command]
|
||||
pub fn sync_profiles(profiles_state: State<'_, ProfilesState>) -> Result<(), String> {
|
||||
let mut profiles = profiles_state.0.lock().unwrap();
|
||||
wrap_err!(profiles.sync_file())
|
||||
pub async fn enhance_profiles() -> CmdResult {
|
||||
wrap_err!(CoreManager::global().update_config().await)?;
|
||||
handle::Handle::refresh_clash();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// import the profile from url
|
||||
/// and save to `profiles.yaml`
|
||||
#[tauri::command]
|
||||
pub async fn import_profile(
|
||||
url: String,
|
||||
option: Option<PrfOption>,
|
||||
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))
|
||||
pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult {
|
||||
let item = wrap_err!(PrfItem::from_url(&url, None, None, option).await)?;
|
||||
wrap_err!(Config::profiles().data().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]
|
||||
pub async fn create_profile(
|
||||
item: PrfItem, // partial
|
||||
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))
|
||||
pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
|
||||
wrap_err!(Config::profiles().data().reorder(active_id, over_id))
|
||||
}
|
||||
|
||||
/// Update the profile
|
||||
#[tauri::command]
|
||||
pub async fn update_profile(
|
||||
index: String,
|
||||
option: Option<PrfOption>,
|
||||
clash_state: State<'_, ClashState>,
|
||||
profiles_state: State<'_, ProfilesState>,
|
||||
) -> Result<(), String> {
|
||||
let (url, opt) = {
|
||||
// must release the lock here
|
||||
let profiles = profiles_state.0.lock().unwrap();
|
||||
pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResult {
|
||||
let item = wrap_err!(PrfItem::from(item, file_data).await)?;
|
||||
wrap_err!(Config::profiles().data().append_item(item))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult {
|
||||
wrap_err!(feat::update_profile(index, option).await)
|
||||
}
|
||||
|
||||
#[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();
|
||||
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 data = wrap_err!(item.read_file())?;
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
// check the profile type
|
||||
if let Some(typ) = item.itype.as_ref() {
|
||||
if *typ != "remote" {
|
||||
ret_err!(format!("could not update the `{typ}` profile"));
|
||||
}
|
||||
#[tauri::command]
|
||||
pub fn save_profile_file(index: String, file_data: Option<String>) -> CmdResult {
|
||||
if file_data.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if item.url.is_none() {
|
||||
ret_err!("failed to get the item url");
|
||||
}
|
||||
|
||||
(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(())
|
||||
let profiles = Config::profiles();
|
||||
let profiles = profiles.latest();
|
||||
let item = wrap_err!(profiles.get_item(&index))?;
|
||||
wrap_err!(item.save_file(file_data.unwrap()))
|
||||
}
|
||||
|
||||
/// change the current profile
|
||||
#[tauri::command]
|
||||
pub fn select_profile(
|
||||
index: String,
|
||||
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))
|
||||
pub fn get_clash_info() -> CmdResult<ClashInfo> {
|
||||
Ok(Config::clash().latest().get_client_info())
|
||||
}
|
||||
|
||||
/// change the profile chain
|
||||
#[tauri::command]
|
||||
pub fn change_profile_chain(
|
||||
chain: Option<Vec<String>>,
|
||||
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))
|
||||
pub fn get_runtime_config() -> CmdResult<Option<Mapping>> {
|
||||
Ok(Config::runtime().latest().config.clone())
|
||||
}
|
||||
|
||||
/// manually exec enhanced profile
|
||||
#[tauri::command]
|
||||
pub fn enhance_profiles(
|
||||
app_handle: tauri::AppHandle,
|
||||
clash_state: State<'_, ClashState>,
|
||||
profiles_state: State<'_, ProfilesState>,
|
||||
) -> Result<(), String> {
|
||||
let mut clash = clash_state.0.lock().unwrap();
|
||||
let profiles = profiles_state.0.lock().unwrap();
|
||||
|
||||
clash.set_window(app_handle.get_window("main"));
|
||||
|
||||
wrap_err!(clash.activate_enhanced(&profiles, false, false))
|
||||
pub fn get_runtime_yaml() -> CmdResult<String> {
|
||||
let runtime = Config::runtime();
|
||||
let runtime = runtime.latest();
|
||||
let config = runtime.config.as_ref();
|
||||
wrap_err!(config
|
||||
.ok_or(anyhow::anyhow!("failed to parse config to yaml file"))
|
||||
.and_then(
|
||||
|config| serde_yaml::to_string(config).context("failed to convert config to yaml")
|
||||
))
|
||||
}
|
||||
|
||||
/// delete profile item
|
||||
#[tauri::command]
|
||||
pub fn delete_profile(
|
||||
index: String,
|
||||
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(())
|
||||
pub fn get_runtime_exists() -> CmdResult<Vec<String>> {
|
||||
Ok(Config::runtime().latest().exists_keys.clone())
|
||||
}
|
||||
|
||||
/// patch the profile config
|
||||
#[tauri::command]
|
||||
pub fn patch_profile(
|
||||
index: String,
|
||||
profile: PrfItem,
|
||||
profiles_state: State<'_, ProfilesState>,
|
||||
) -> Result<(), String> {
|
||||
let mut profiles = profiles_state.0.lock().unwrap();
|
||||
wrap_err!(profiles.patch_item(index, profile))
|
||||
pub fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String)>>> {
|
||||
Ok(Config::runtime().latest().chain_logs.clone())
|
||||
}
|
||||
|
||||
/// run vscode command to edit the profile
|
||||
#[tauri::command]
|
||||
pub fn view_profile(index: String, profiles_state: State<'_, ProfilesState>) -> Result<(), String> {
|
||||
let profiles = profiles_state.0.lock().unwrap();
|
||||
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))
|
||||
pub async fn patch_clash_config(payload: Mapping) -> CmdResult {
|
||||
wrap_err!(feat::patch_clash(payload).await)
|
||||
}
|
||||
|
||||
/// read the profile item file data
|
||||
#[tauri::command]
|
||||
pub fn read_profile_file(
|
||||
index: String,
|
||||
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)
|
||||
pub fn get_verge_config() -> CmdResult<IVerge> {
|
||||
Ok(Config::verge().data().clone())
|
||||
}
|
||||
|
||||
/// save the profile item file data
|
||||
#[tauri::command]
|
||||
pub fn save_profile_file(
|
||||
index: String,
|
||||
file_data: Option<String>,
|
||||
profiles_state: State<'_, ProfilesState>,
|
||||
) -> Result<(), String> {
|
||||
if file_data.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
pub async fn patch_verge_config(payload: IVerge) -> CmdResult {
|
||||
wrap_err!(feat::patch_verge(payload).await)
|
||||
}
|
||||
|
||||
let profiles = profiles_state.0.lock().unwrap();
|
||||
let item = wrap_err!(profiles.get_item(&index))?;
|
||||
wrap_err!(item.save_file(file_data.unwrap()))
|
||||
#[tauri::command]
|
||||
pub async fn change_clash_core(clash_core: Option<String>) -> CmdResult {
|
||||
wrap_err!(CoreManager::global().change_core(clash_core).await)
|
||||
}
|
||||
|
||||
/// restart the sidecar
|
||||
#[tauri::command]
|
||||
pub fn restart_sidecar(
|
||||
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();
|
||||
|
||||
wrap_err!(clash.restart_sidecar(&mut profiles))
|
||||
pub async fn restart_sidecar() -> CmdResult {
|
||||
wrap_err!(CoreManager::global().run_core().await)
|
||||
}
|
||||
|
||||
/// get the clash core info from the state
|
||||
/// the caller can also get the infomation by clash's api
|
||||
#[tauri::command]
|
||||
pub fn get_clash_info(clash_state: State<'_, ClashState>) -> Result<ClashInfo, String> {
|
||||
let clash = clash_state.0.lock().unwrap();
|
||||
Ok(clash.info.clone())
|
||||
}
|
||||
pub fn grant_permission(_core: String) -> CmdResult {
|
||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||
return wrap_err!(manager::grant_permission(_core));
|
||||
|
||||
/// update the clash core config
|
||||
/// after putting the change to the clash core
|
||||
/// 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))
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
|
||||
return Err("Unsupported target".into());
|
||||
}
|
||||
|
||||
/// get the system proxy
|
||||
#[tauri::command]
|
||||
pub fn get_sys_proxy() -> Result<SysProxyConfig, String> {
|
||||
wrap_err!(SysProxyConfig::get_sys())
|
||||
pub fn get_sys_proxy() -> CmdResult<Mapping> {
|
||||
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]
|
||||
pub fn get_cur_proxy(verge_state: State<'_, VergeState>) -> Result<Option<SysProxyConfig>, String> {
|
||||
let verge = verge_state.0.lock().unwrap();
|
||||
Ok(verge.cur_sysproxy.clone())
|
||||
pub fn get_clash_logs() -> CmdResult<VecDeque<String>> {
|
||||
Ok(logger::Logger::global().get_log())
|
||||
}
|
||||
|
||||
/// get the verge config
|
||||
#[tauri::command]
|
||||
pub fn get_verge_config(verge_state: State<'_, VergeState>) -> Result<VergeConfig, String> {
|
||||
let verge = verge_state.0.lock().unwrap();
|
||||
let mut config = verge.config.clone();
|
||||
|
||||
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)
|
||||
pub fn open_app_dir() -> CmdResult<()> {
|
||||
let app_dir = wrap_err!(dirs::app_home_dir())?;
|
||||
wrap_err!(open::that(app_dir))
|
||||
}
|
||||
|
||||
/// patch the verge config
|
||||
/// this command only save the config and not responsible for other things
|
||||
#[tauri::command]
|
||||
pub fn patch_verge_config(
|
||||
payload: VergeConfig,
|
||||
app_handle: tauri::AppHandle,
|
||||
clash_state: State<'_, ClashState>,
|
||||
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();
|
||||
|
||||
// change tun mode
|
||||
if tun_mode.is_some() {
|
||||
let mut clash = clash_state.0.lock().unwrap();
|
||||
let profiles = profiles_state.0.lock().unwrap();
|
||||
|
||||
wrap_err!(clash.tun_mode(tun_mode.unwrap()))?;
|
||||
clash.update_config();
|
||||
wrap_err!(clash.activate_enhanced(&profiles, false, false))?;
|
||||
}
|
||||
|
||||
let mut verge = verge_state.0.lock().unwrap();
|
||||
wrap_err!(verge.patch_config(payload))?;
|
||||
|
||||
// change system tray
|
||||
if system_proxy.is_some() {
|
||||
app_handle
|
||||
.tray_handle()
|
||||
.get_item("system_proxy")
|
||||
.set_selected(system_proxy.unwrap())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
pub fn open_core_dir() -> CmdResult<()> {
|
||||
let core_dir = wrap_err!(tauri::utils::platform::current_exe())?;
|
||||
let core_dir = core_dir.parent().ok_or("failed to get core dir")?;
|
||||
wrap_err!(open::that(core_dir))
|
||||
}
|
||||
|
||||
/// kill all sidecars when update app
|
||||
#[tauri::command]
|
||||
pub fn kill_sidecars() {
|
||||
api::process::kill_children();
|
||||
pub fn open_logs_dir() -> CmdResult<()> {
|
||||
let log_dir = wrap_err!(dirs::app_logs_dir())?;
|
||||
wrap_err!(open::that(log_dir))
|
||||
}
|
||||
|
||||
/// open app config dir
|
||||
#[tauri::command]
|
||||
pub fn open_app_dir() -> Result<(), String> {
|
||||
let app_dir = dirs::app_home_dir();
|
||||
wrap_err!(open::that(app_dir))
|
||||
pub fn open_web_url(url: String) -> CmdResult<()> {
|
||||
wrap_err!(open::that(url))
|
||||
}
|
||||
|
||||
/// open logs dir
|
||||
#[cfg(windows)]
|
||||
pub mod uwp {
|
||||
use super::*;
|
||||
use crate::core::win_uwp;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn invoke_uwp_tool() -> CmdResult {
|
||||
wrap_err!(win_uwp::invoke_uwptools().await)
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn open_logs_dir() -> Result<(), String> {
|
||||
let log_dir = dirs::app_logs_dir();
|
||||
wrap_err!(open::that(log_dir))
|
||||
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 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 dest_path = icon_dir.join(name);
|
||||
|
||||
if file_path.exists() {
|
||||
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 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);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub mod service {
|
||||
use super::*;
|
||||
use crate::core::win_service;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn check_service() -> CmdResult<win_service::JsonResponse> {
|
||||
wrap_err!(win_service::check_service().await)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn install_service() -> CmdResult {
|
||||
wrap_err!(win_service::install_service().await)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn uninstall_service() -> CmdResult {
|
||||
wrap_err!(win_service::uninstall_service().await)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub mod service {
|
||||
use super::*;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn check_service() -> CmdResult {
|
||||
Ok(())
|
||||
}
|
||||
#[tauri::command]
|
||||
pub async fn install_service() -> CmdResult {
|
||||
Ok(())
|
||||
}
|
||||
#[tauri::command]
|
||||
pub async fn uninstall_service() -> CmdResult {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub mod uwp {
|
||||
use super::*;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn invoke_uwp_tool() -> CmdResult {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
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(), 9000.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 {
|
||||
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);
|
||||
|
||||
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,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
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>>,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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::*;
|
||||
@@ -0,0 +1,388 @@
|
||||
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>,
|
||||
|
||||
/// 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,
|
||||
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 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,
|
||||
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,
|
||||
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,
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
// 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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
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>,
|
||||
|
||||
/// 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 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 mixed port 用于覆盖 clash 的 mixed port
|
||||
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),
|
||||
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),
|
||||
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),
|
||||
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!(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);
|
||||
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!(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};
|
||||
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(())
|
||||
}
|
||||
|
||||
/// enable tun mode
|
||||
/// only revise the config
|
||||
pub fn tun_mode(&mut self, enable: bool) -> Result<()> {
|
||||
// Windows 需要wintun.dll文件
|
||||
#[cfg(target_os = "windows")]
|
||||
if enable {
|
||||
let wintun_dll = dirs::app_home_dir().join("wintun.dll");
|
||||
if !wintun_dll.exists() {
|
||||
bail!("failed to enable TUN for missing `wintun.dll`");
|
||||
}
|
||||
}
|
||||
|
||||
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 = self.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);
|
||||
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!(self.config, "tun", new_tun);
|
||||
|
||||
// dns config
|
||||
let dns_val = self.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);
|
||||
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!(self.config, "dns", new_dns);
|
||||
|
||||
self.save_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 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
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,
|
||||
};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
if *self.use_service_mode.lock() {
|
||||
log::debug!(target: "app", "stop the core by service");
|
||||
log_err!(super::win_service::stop_core_by_service().await);
|
||||
should_kill = true;
|
||||
}
|
||||
|
||||
// 这里得等一会儿
|
||||
if should_kill {
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use super::win_service;
|
||||
|
||||
// 服务模式
|
||||
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 {
|
||||
win_service::check_service().await?;
|
||||
win_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<()> {
|
||||
// 服务模式不管
|
||||
#[cfg(target_os = "windows")]
|
||||
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<()> {
|
||||
#[cfg(target_os = "windows")]
|
||||
if *self.use_service_mode.lock() {
|
||||
log::debug!(target: "app", "stop the core by service");
|
||||
tauri::async_runtime::block_on(async move {
|
||||
log_err!(super::win_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(())
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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=+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;
|
||||
mod profiles;
|
||||
mod verge;
|
||||
pub mod clash_api;
|
||||
mod core;
|
||||
pub mod handle;
|
||||
pub mod hotkey;
|
||||
pub mod logger;
|
||||
pub mod manager;
|
||||
pub mod sysopt;
|
||||
pub mod timer;
|
||||
pub mod tray;
|
||||
pub mod win_service;
|
||||
pub mod win_uwp;
|
||||
|
||||
pub use self::clash::*;
|
||||
pub use self::profiles::*;
|
||||
pub use self::verge::*;
|
||||
pub use self::core::*;
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
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::sync::Arc;
|
||||
use sysproxy::Sysproxy;
|
||||
use tauri::{async_runtime::Mutex as TokioMutex, utils::platform::current_exe};
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
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");
|
||||
|
||||
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 path = dirs::app_home_dir()?.join("icons").join("sysproxy.png");
|
||||
if path.exists() {
|
||||
icon = std::fs::read(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 path = dirs::app_home_dir()?.join("icons").join("common.png");
|
||||
if path.exists() {
|
||||
icon = std::fs::read(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 path = dirs::app_home_dir()?.join("icons").join("tun.png");
|
||||
if path.exists() {
|
||||
icon = std::fs::read(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);
|
||||
|
||||
let switch_map = {
|
||||
let mut map = std::collections::HashMap::new();
|
||||
map.insert(true, "on");
|
||||
map.insert(false, "off");
|
||||
map
|
||||
};
|
||||
|
||||
let _ = tray.set_tooltip(&format!(
|
||||
"Clash Verge {version}\n{}: {}\n{}: {}",
|
||||
t!("System Proxy", "系统代理"),
|
||||
switch_map[system_proxy],
|
||||
t!("TUN Mode", "Tun 模式"),
|
||||
switch_map[tun_mode]
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn on_left_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 {
|
||||
SystemTrayEvent::LeftClick { .. } => Tray::on_left_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,332 +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>,
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
#![cfg(target_os = "windows")]
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::utils::dirs;
|
||||
use anyhow::{bail, Context, Result};
|
||||
use deelevate::{PrivilegeLevel, Token};
|
||||
use runas::Command as RunasCommand;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::os::windows::process::CommandExt;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
use std::{env::current_exe, process::Command as StdCommand};
|
||||
use tokio::time::sleep;
|
||||
|
||||
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弹窗阻塞主线程
|
||||
pub async fn install_service() -> Result<()> {
|
||||
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(())
|
||||
}
|
||||
|
||||
/// Uninstall the Clash Verge Service
|
||||
/// 该函数应该在协程或者线程中执行,避免UAC弹窗阻塞主线程
|
||||
pub async fn uninstall_service() -> Result<()> {
|
||||
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(())
|
||||
}
|
||||
|
||||
/// 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 clash_bin = format!("{clash_core}.exe");
|
||||
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(())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
function main(params) {
|
||||
if (params.mode === "script") {
|
||||
params.mode = "rule";
|
||||
}
|
||||
return params;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
function main(params) {
|
||||
if (Array.isArray(params.proxies)) {
|
||||
params.proxies.forEach((p, i) => {
|
||||
if (p.type === "hysteria" && typeof p.alpn === "string") {
|
||||
params.proxies[i].alpn = [p.alpn];
|
||||
}
|
||||
});
|
||||
}
|
||||
return params;
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
use serde_yaml::{Mapping, Value};
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub const HANDLE_FIELDS: [&str; 9] = [
|
||||
"mode",
|
||||
"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()
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
use super::use_lowercase;
|
||||
use anyhow::Result;
|
||||
use serde_yaml::Mapping;
|
||||
|
||||
pub fn use_script(script: String, config: Mapping) -> Result<(Mapping, Vec<(String, String)>)> {
|
||||
use rquickjs::{function::Func, Context, Runtime};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
let runtime = Runtime::new().unwrap();
|
||||
let context = Context::full(&runtime).unwrap();
|
||||
let outputs = Arc::new(Mutex::new(vec![]));
|
||||
|
||||
let copy_outputs = outputs.clone();
|
||||
let result = context.with(|ctx| -> Result<Mapping> {
|
||||
ctx.globals().set(
|
||||
"__verge_log__",
|
||||
Func::from(move |level: String, data: String| {
|
||||
let mut out = copy_outputs.lock().unwrap();
|
||||
out.push((level, data));
|
||||
}),
|
||||
)?;
|
||||
|
||||
ctx.eval(
|
||||
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()}}`
|
||||
}}"#
|
||||
);
|
||||
let result: String = ctx.eval(code.as_str())?;
|
||||
if result.starts_with("__error_flag__") {
|
||||
anyhow::bail!(result[15..].to_owned());
|
||||
}
|
||||
if result == "\"\"" {
|
||||
anyhow::bail!("main function should return object");
|
||||
}
|
||||
Ok(serde_json::from_str::<Mapping>(result.as_str())?)
|
||||
});
|
||||
|
||||
let mut out = outputs.lock().unwrap();
|
||||
match result {
|
||||
Ok(config) => Ok((use_lowercase(config), out.to_vec())),
|
||||
Err(err) => {
|
||||
out.push(("exception".into(), err.to_string()));
|
||||
Ok((config, out.to_vec()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
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 {
|
||||
use_dns_for_tun(config)
|
||||
} else {
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
//!
|
||||
//! 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 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 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 {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
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?;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
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} \n set 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,160 +1,135 @@
|
||||
#![cfg_attr(
|
||||
all(not(debug_assertions), target_os = "windows"),
|
||||
windows_subsystem = "windows"
|
||||
all(not(debug_assertions), target_os = "windows"),
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
|
||||
mod cmds;
|
||||
mod config;
|
||||
mod core;
|
||||
mod states;
|
||||
mod enhance;
|
||||
mod feat;
|
||||
mod utils;
|
||||
|
||||
use crate::{
|
||||
core::VergeConfig,
|
||||
utils::{resolve, server},
|
||||
};
|
||||
use tauri::{
|
||||
api, CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem,
|
||||
};
|
||||
use crate::utils::{init, resolve, server};
|
||||
use tauri::{api, SystemTray};
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
if server::check_singleton().is_err() {
|
||||
println!("app exists");
|
||||
return Ok(());
|
||||
}
|
||||
// 单例检测
|
||||
if server::check_singleton().is_err() {
|
||||
println!("app exists");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let tray_menu = SystemTrayMenu::new()
|
||||
.add_item(CustomMenuItem::new("open_window", "Show"))
|
||||
.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"));
|
||||
crate::log_err!(init::init_config());
|
||||
|
||||
#[allow(unused_mut)]
|
||||
let mut builder = tauri::Builder::default()
|
||||
.manage(states::VergeState::default())
|
||||
.manage(states::ClashState::default())
|
||||
.manage(states::ProfilesState::default())
|
||||
.setup(|app| Ok(resolve::resolve_setup(app)))
|
||||
.system_tray(SystemTray::new().with_menu(tray_menu))
|
||||
.on_system_tray_event(move |app_handle, event| match event {
|
||||
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
|
||||
"open_window" => {
|
||||
let window = app_handle.get_window("main").unwrap();
|
||||
window.unminimize().unwrap();
|
||||
window.show().unwrap();
|
||||
window.set_focus().unwrap();
|
||||
#[allow(unused_mut)]
|
||||
let mut builder = tauri::Builder::default()
|
||||
.system_tray(SystemTray::new())
|
||||
.setup(|app| {
|
||||
resolve::resolve_setup(app);
|
||||
Ok(())
|
||||
})
|
||||
.on_system_tray_event(core::tray::Tray::on_system_tray_event)
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
// common
|
||||
cmds::get_sys_proxy,
|
||||
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::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" => {
|
||||
let verge_state = app_handle.state::<states::VergeState>();
|
||||
let mut verge = verge_state.0.lock().unwrap();
|
||||
|
||||
let old_value = verge.config.enable_system_proxy.clone().unwrap_or(false);
|
||||
let new_value = !old_value;
|
||||
|
||||
match verge.patch_config(VergeConfig {
|
||||
enable_system_proxy: Some(new_value),
|
||||
..VergeConfig::default()
|
||||
}) {
|
||||
Ok(_) => {
|
||||
app_handle
|
||||
.tray_handle()
|
||||
.get_item(id.as_str())
|
||||
.set_selected(new_value)
|
||||
.unwrap();
|
||||
|
||||
// update verge config
|
||||
let window = app_handle.get_window("main").unwrap();
|
||||
window.emit("verge://refresh-verge-config", "yes").unwrap();
|
||||
tauri::RunEvent::Updater(tauri::UpdaterEvent::Downloaded) => {
|
||||
resolve::resolve_reset();
|
||||
api::process::kill_children();
|
||||
}
|
||||
tauri::RunEvent::WindowEvent { label, event, .. } => {
|
||||
if label == "main" {
|
||||
match event {
|
||||
tauri::WindowEvent::Destroyed => {
|
||||
let _ = resolve::save_window_size_position(app_handle, true);
|
||||
}
|
||||
tauri::WindowEvent::CloseRequested { .. } => {
|
||||
let _ = resolve::save_window_size_position(app_handle, true);
|
||||
}
|
||||
tauri::WindowEvent::Moved(_) | tauri::WindowEvent::Resized(_) => {
|
||||
let _ = resolve::save_window_size_position(app_handle, false);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
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::CloseRequested { label, 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,57 +1,121 @@
|
||||
use std::env::temp_dir;
|
||||
use std::path::{Path, PathBuf};
|
||||
use crate::core::handle;
|
||||
use anyhow::Result;
|
||||
use once_cell::sync::OnceCell;
|
||||
use std::path::PathBuf;
|
||||
use tauri::{
|
||||
api::path::{home_dir, resource_dir},
|
||||
Env, PackageInfo,
|
||||
api::path::{data_dir, resource_dir},
|
||||
Env,
|
||||
};
|
||||
|
||||
#[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")]
|
||||
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 VERGE_CONFIG: &str = "verge.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
|
||||
pub fn app_home_dir() -> PathBuf {
|
||||
home_dir()
|
||||
.unwrap()
|
||||
.join(Path::new(".config"))
|
||||
.join(Path::new(APP_DIR))
|
||||
pub fn app_home_dir() -> Result<PathBuf> {
|
||||
use tauri::utils::platform::current_exe;
|
||||
|
||||
let flag = PORTABLE_FLAG.get().unwrap_or(&false);
|
||||
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
|
||||
pub fn app_resources_dir(package_info: &PackageInfo) -> PathBuf {
|
||||
resource_dir(package_info, &Env::default())
|
||||
.unwrap()
|
||||
.join("resources")
|
||||
pub fn app_resources_dir() -> Result<PathBuf> {
|
||||
let handle = handle::Handle::global();
|
||||
let app_handle = handle.app_handle.lock();
|
||||
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
|
||||
pub fn app_profiles_dir() -> PathBuf {
|
||||
app_home_dir().join("profiles")
|
||||
pub fn app_profiles_dir() -> Result<PathBuf> {
|
||||
Ok(app_home_dir()?.join("profiles"))
|
||||
}
|
||||
|
||||
/// logs dir
|
||||
pub fn app_logs_dir() -> PathBuf {
|
||||
app_home_dir().join("logs")
|
||||
pub fn app_logs_dir() -> Result<PathBuf> {
|
||||
Ok(app_home_dir()?.join("logs"))
|
||||
}
|
||||
|
||||
pub fn clash_path() -> PathBuf {
|
||||
app_home_dir().join(CLASH_CONFIG)
|
||||
pub fn clash_path() -> Result<PathBuf> {
|
||||
Ok(app_home_dir()?.join(CLASH_CONFIG))
|
||||
}
|
||||
|
||||
pub fn verge_path() -> PathBuf {
|
||||
app_home_dir().join(VERGE_CONFIG)
|
||||
pub fn verge_path() -> Result<PathBuf> {
|
||||
Ok(app_home_dir()?.join(VERGE_CONFIG))
|
||||
}
|
||||
|
||||
pub fn profiles_path() -> PathBuf {
|
||||
app_home_dir().join(PROFILE_YAML)
|
||||
pub fn profiles_path() -> Result<PathBuf> {
|
||||
Ok(app_home_dir()?.join(PROFILE_YAML))
|
||||
}
|
||||
|
||||
pub fn profiles_temp_path() -> PathBuf {
|
||||
temp_dir().join(PROFILE_TEMP)
|
||||
pub fn clash_pid_path() -> Result<PathBuf> {
|
||||
Ok(app_home_dir()?.join("clash.pid"))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn service_path() -> Result<PathBuf> {
|
||||
Ok(app_resources_dir()?.join("clash-verge-service.exe"))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
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 std::str::FromStr;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use serde_yaml::{Mapping, Value};
|
||||
use std::{fs, path::PathBuf, str::FromStr};
|
||||
use tauri::{
|
||||
api::shell::{open, Program},
|
||||
Manager,
|
||||
};
|
||||
|
||||
pub fn get_now() -> usize {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs() as _
|
||||
/// read data from yaml as struct T
|
||||
pub fn read_yaml<T: DeserializeOwned>(path: &PathBuf) -> Result<T> {
|
||||
if !path.exists() {
|
||||
bail!("file not found \"{}\"", path.display());
|
||||
}
|
||||
|
||||
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] = [
|
||||
'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',
|
||||
'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U',
|
||||
'V', 'W', 'X', 'Y', 'Z',
|
||||
'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',
|
||||
'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U',
|
||||
'V', 'W', 'X', 'Y', 'Z',
|
||||
];
|
||||
|
||||
/// generate the uid
|
||||
pub fn get_uid(prefix: &str) -> String {
|
||||
let id = nanoid!(11, &ALPHABET);
|
||||
format!("{prefix}{id}")
|
||||
let id = nanoid!(11, &ALPHABET);
|
||||
format!("{prefix}{id}")
|
||||
}
|
||||
|
||||
/// parse the string
|
||||
/// xxx=123123; => 123123
|
||||
pub fn parse_str<T: FromStr>(target: &str, key: &str) -> Option<T> {
|
||||
match target.find(key) {
|
||||
Some(idx) => {
|
||||
let idx = idx + key.len();
|
||||
let value = &target[idx..];
|
||||
match match value.split(';').nth(0) {
|
||||
Some(value) => value.trim().parse(),
|
||||
None => value.trim().parse(),
|
||||
} {
|
||||
Ok(r) => Some(r),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
target.split(';').map(str::trim).find_map(|s| {
|
||||
let mut parts = s.splitn(2, '=');
|
||||
match (parts.next(), parts.next()) {
|
||||
(Some(k), Some(v)) if k == key => v.parse::<T>().ok(),
|
||||
_ => 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
|
||||
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_rules! log_if_err {
|
||||
($result: expr) => {
|
||||
if let Err(err) = $result {
|
||||
log::error!("{err}");
|
||||
macro_rules! error {
|
||||
($result: expr) => {
|
||||
log::error!(target: "app", "{}", $result);
|
||||
};
|
||||
}
|
||||
|
||||
#[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
|
||||
/// transform the error to String
|
||||
#[macro_export]
|
||||
macro_rules! wrap_err {
|
||||
($stat: expr) => {
|
||||
match $stat {
|
||||
Ok(a) => Ok(a),
|
||||
Err(err) => {
|
||||
log::error!("{}", err.to_string());
|
||||
Err(format!("{}", err.to_string()))
|
||||
}
|
||||
}
|
||||
};
|
||||
($stat: expr) => {
|
||||
match $stat {
|
||||
Ok(a) => Ok(a),
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "{}", err.to_string());
|
||||
Err(format!("{}", err.to_string()))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// return the string literal error
|
||||
#[macro_export]
|
||||
macro_rules! ret_err {
|
||||
($str: expr) => {
|
||||
return Err($str.into())
|
||||
};
|
||||
($str: expr) => {
|
||||
return Err($str.into())
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_value() {
|
||||
let test_1 = "upload=111; download=2222; total=3333; expire=444";
|
||||
let test_2 = "attachment; filename=Clash.yaml";
|
||||
let test_1 = "upload=111; download=2222; total=3333; expire=444";
|
||||
let test_2 = "attachment; filename=Clash.yaml";
|
||||
|
||||
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, "total=").unwrap(), 3333);
|
||||
assert_eq!(parse_str::<usize>(test_1, "expire=").unwrap(), 444);
|
||||
assert_eq!(
|
||||
parse_str::<String>(test_2, "filename=").unwrap(),
|
||||
format!("Clash.yaml")
|
||||
);
|
||||
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, "total").unwrap(), 3333);
|
||||
assert_eq!(parse_str::<usize>(test_1, "expire").unwrap(), 444);
|
||||
assert_eq!(
|
||||
parse_str::<String>(test_2, "filename").unwrap(),
|
||||
format!("Clash.yaml")
|
||||
);
|
||||
|
||||
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, "expire1="), None);
|
||||
assert_eq!(parse_str::<usize>(test_2, "attachment="), 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, "expire1"), None);
|
||||
assert_eq!(parse_str::<usize>(test_2, "attachment"), None);
|
||||
}
|
||||
|
||||
@@ -1,100 +1,324 @@
|
||||
use crate::utils::{dirs, tmpl};
|
||||
use chrono::Local;
|
||||
use crate::config::*;
|
||||
use crate::utils::{dirs, help};
|
||||
use anyhow::Result;
|
||||
use chrono::{Local, TimeZone};
|
||||
use log::LevelFilter;
|
||||
use log4rs::append::console::ConsoleAppender;
|
||||
use log4rs::append::file::FileAppender;
|
||||
use log4rs::config::{Appender, Config, Root};
|
||||
use log4rs::config::{Appender, Logger, Root};
|
||||
use log4rs::encode::pattern::PatternEncoder;
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::fs::{self, DirEntry};
|
||||
use std::path::PathBuf;
|
||||
use tauri::PackageInfo;
|
||||
use std::str::FromStr;
|
||||
use tauri::api::process::Command;
|
||||
|
||||
/// initialize this instance's log file
|
||||
fn init_log(log_dir: &PathBuf) {
|
||||
let local_time = Local::now().format("%Y-%m-%d-%H%M%S").to_string();
|
||||
let log_file = format!("{}.log", local_time);
|
||||
let log_file = log_dir.join(log_file);
|
||||
|
||||
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();
|
||||
fn init_log() -> Result<()> {
|
||||
let log_dir = dirs::app_logs_dir()?;
|
||||
if !log_dir.exists() {
|
||||
let _ = fs::create_dir_all(&log_dir);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
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 = "cmd";
|
||||
}
|
||||
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,6 @@
|
||||
pub mod config;
|
||||
pub mod dirs;
|
||||
pub mod help;
|
||||
pub mod init;
|
||||
pub mod resolve;
|
||||
pub mod server;
|
||||
pub mod sysopt;
|
||||
pub mod tmpl;
|
||||
|
||||
@@ -1,96 +1,268 @@
|
||||
use super::{init, server};
|
||||
use crate::{core::Profiles, log_if_err, states};
|
||||
use crate::config::{IVerge, PrfOption};
|
||||
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 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
|
||||
pub fn resolve_setup(app: &App) {
|
||||
// setup a simple http server for singleton
|
||||
server::embed_server(&app.handle());
|
||||
pub fn resolve_setup(app: &mut App) {
|
||||
#[cfg(target_os = "macos")]
|
||||
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
|
||||
init::init_app(app.package_info());
|
||||
log_err!(init::init_resources());
|
||||
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 clash_state = app.state::<states::ClashState>();
|
||||
let verge_state = app.state::<states::VergeState>();
|
||||
let profiles_state = app.state::<states::ProfilesState>();
|
||||
let mut port = Config::verge()
|
||||
.latest()
|
||||
.verge_mixed_port
|
||||
.unwrap_or(Config::clash().data().get_mixed_port());
|
||||
|
||||
let mut clash = clash_state.0.lock().unwrap();
|
||||
let mut verge = verge_state.0.lock().unwrap();
|
||||
let mut profiles = profiles_state.0.lock().unwrap();
|
||||
if enable_random_port {
|
||||
port = find_unused_port().unwrap_or(
|
||||
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_if_err!(clash.activate(&profiles));
|
||||
log_if_err!(clash.activate_enhanced(&profiles, true, true));
|
||||
log::trace!("launch core");
|
||||
log_err!(CoreManager::global().init());
|
||||
|
||||
verge.init_sysproxy(clash.info.port.clone());
|
||||
// enable tun mode
|
||||
if verge.config.enable_tun_mode.clone().unwrap_or(false)
|
||||
&& verge.cur_sysproxy.is_some()
|
||||
&& verge.cur_sysproxy.as_ref().unwrap().enable
|
||||
{
|
||||
log::info!("enable tun mode");
|
||||
clash.tun_mode(true).unwrap();
|
||||
}
|
||||
// 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| {
|
||||
log_if_err!(app
|
||||
.tray_handle()
|
||||
.get_item("system_proxy")
|
||||
.set_selected(enable));
|
||||
});
|
||||
let silent_start = { Config::verge().data().enable_silent_start };
|
||||
if !silent_start.unwrap_or(false) {
|
||||
create_window(&app.app_handle());
|
||||
}
|
||||
|
||||
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
|
||||
pub fn resolve_reset(app_handle: &AppHandle) {
|
||||
let verge_state = app_handle.state::<states::VergeState>();
|
||||
let mut verge = verge_state.0.lock().unwrap();
|
||||
|
||||
verge.reset_sysproxy();
|
||||
pub fn resolve_reset() {
|
||||
log_err!(sysopt::Sysopt::global().reset_sysproxy());
|
||||
log_err!(CoreManager::global().stop_core());
|
||||
}
|
||||
|
||||
/// customize the window theme
|
||||
fn resolve_window(app: &App, hide: Option<bool>) {
|
||||
let window = app.get_window("main").unwrap();
|
||||
|
||||
// silent start
|
||||
hide.map(|hide| {
|
||||
if hide {
|
||||
window.hide().unwrap();
|
||||
/// create main window
|
||||
pub fn create_window(app_handle: &AppHandle) {
|
||||
if let Some(window) = app_handle.get_window("main") {
|
||||
trace_err!(window.unminimize(), "set win unminimize");
|
||||
trace_err!(window.show(), "set win visible");
|
||||
trace_err!(window.set_focus(), "set win focus");
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use window_shadows::set_shadow;
|
||||
use window_vibrancy::apply_blur;
|
||||
let mut builder = tauri::window::WindowBuilder::new(
|
||||
app_handle,
|
||||
"main".to_string(),
|
||||
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();
|
||||
set_shadow(&window, true).unwrap();
|
||||
apply_blur(&window, None).unwrap();
|
||||
}
|
||||
match Config::verge().latest().window_size_position.clone() {
|
||||
Some(size_pos) if size_pos.len() == 4 => {
|
||||
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
|
||||
.additional_browser_args("--enable-features=msWebView2EnableDraggableRegions")
|
||||
.inner_size(800.0, 636.0)
|
||||
.center();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use tauri::LogicalSize;
|
||||
use tauri::Size::Logical;
|
||||
window.set_decorations(true).unwrap();
|
||||
window
|
||||
.set_size(Logical(LogicalSize {
|
||||
width: 800.0,
|
||||
height: 610.0,
|
||||
}))
|
||||
.unwrap();
|
||||
// use tauri_plugin_vibrancy::MacOSVibrancy;
|
||||
// #[allow(deprecated)]
|
||||
// window.apply_vibrancy(MacOSVibrancy::AppearanceBased);
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
builder = builder.inner_size(800.0, 642.0).center();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
builder = builder.inner_size(800.0, 642.0).center();
|
||||
}
|
||||
}
|
||||
};
|
||||
#[cfg(target_os = "windows")]
|
||||
let window = builder
|
||||
.decorations(false)
|
||||
.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(true).transparent(false).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;
|
||||
|
||||
use super::resolve;
|
||||
use crate::config::IVerge;
|
||||
use anyhow::{bail, Result};
|
||||
use port_scanner::local_port_available;
|
||||
use tauri::{AppHandle, Manager};
|
||||
use std::convert::Infallible;
|
||||
use tauri::AppHandle;
|
||||
use warp::Filter;
|
||||
|
||||
#[cfg(not(feature = "verge-dev"))]
|
||||
const SERVER_PORT: u16 = 33333;
|
||||
#[cfg(feature = "verge-dev")]
|
||||
const SERVER_PORT: u16 = 11233;
|
||||
#[derive(serde::Deserialize, Debug)]
|
||||
struct QueryParam {
|
||||
param: String,
|
||||
}
|
||||
|
||||
/// check whether there is already exists
|
||||
pub fn check_singleton() -> Result<(), ()> {
|
||||
if !local_port_available(SERVER_PORT) {
|
||||
tauri::async_runtime::block_on(async {
|
||||
let url = format!("http://127.0.0.1:{}/commands/visible", SERVER_PORT);
|
||||
reqwest::get(url).await.unwrap();
|
||||
Err(())
|
||||
})
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
pub fn check_singleton() -> Result<()> {
|
||||
let port = IVerge::get_singleton_port();
|
||||
|
||||
if !local_port_available(port) {
|
||||
tauri::async_runtime::block_on(async {
|
||||
let resp = reqwest::get(format!("http://127.0.0.1:{port}/commands/ping"))
|
||||
.await?
|
||||
.text()
|
||||
.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
|
||||
/// maybe it can be used as pac server later
|
||||
pub fn embed_server(app: &AppHandle) {
|
||||
let window = app.get_window("main").unwrap();
|
||||
pub fn embed_server(app_handle: AppHandle) {
|
||||
let port = IVerge::get_singleton_port();
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let commands = warp::path!("commands" / "visible").map(move || {
|
||||
window.show().unwrap();
|
||||
window.set_focus().unwrap();
|
||||
return format!("ok");
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let ping = warp::path!("commands" / "ping").map(move || "ok");
|
||||
|
||||
let visible = warp::path!("commands" / "visible").map(move || {
|
||||
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,36 +1,4 @@
|
||||
///! 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>
|
||||
";
|
||||
//! Some config file template
|
||||
|
||||
/// template for new a profile item
|
||||
pub const ITEM_LOCAL: &str = "# Profile Template for clash verge
|
||||
@@ -48,22 +16,28 @@ pub const ITEM_MERGE: &str = "# Merge Template for clash verge
|
||||
|
||||
prepend-rules:
|
||||
|
||||
prepend-rule-providers:
|
||||
|
||||
prepend-proxies:
|
||||
|
||||
prepend-proxy-providers:
|
||||
|
||||
prepend-proxy-groups:
|
||||
|
||||
append-rules:
|
||||
|
||||
append-rule-providers:
|
||||
|
||||
append-proxies:
|
||||
|
||||
append-proxy-providers:
|
||||
|
||||
append-proxy-groups:
|
||||
";
|
||||
|
||||
/// enhanced profile
|
||||
pub const ITEM_SCRIPT: &str = "// Should define the `main` function
|
||||
// The argument to this function is the clash config
|
||||
// or the result of the previous handler
|
||||
// so you should return the config after processing
|
||||
pub const ITEM_SCRIPT: &str = "// Define the `main` function
|
||||
|
||||
function main(params) {
|
||||
return params;
|
||||
}
|
||||
|
||||
@@ -1,62 +1,40 @@
|
||||
{
|
||||
"package": {
|
||||
"productName": "Clash Verge",
|
||||
"version": "0.0.26"
|
||||
"version": "1.5.6"
|
||||
},
|
||||
"build": {
|
||||
"distDir": "../dist",
|
||||
"devPath": "http://localhost:3000/",
|
||||
"beforeDevCommand": "yarn run web:dev",
|
||||
"beforeBuildCommand": "yarn run web:build"
|
||||
"beforeDevCommand": "pnpm run web:dev",
|
||||
"beforeBuildCommand": "pnpm run web:build"
|
||||
},
|
||||
"tauri": {
|
||||
"systemTray": {
|
||||
"iconPath": "icons/icon.png",
|
||||
"iconAsTemplate": true
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"identifier": "top.gydi.clashverge",
|
||||
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon-new.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"resources": ["resources"],
|
||||
"externalBin": ["sidecar/clash"],
|
||||
"externalBin": ["sidecar/clash-meta", "sidecar/clash-meta-alpha"],
|
||||
"copyright": "© 2022 zzzgydi All Rights Reserved",
|
||||
"category": "DeveloperTool",
|
||||
"shortDescription": "A Clash GUI based on tauri.",
|
||||
"longDescription": "A Clash GUI based on tauri.",
|
||||
"deb": {
|
||||
"depends": [],
|
||||
"useBootstrapper": false
|
||||
},
|
||||
"macOS": {
|
||||
"frameworks": [],
|
||||
"minimumSystemVersion": "",
|
||||
"useBootstrapper": false,
|
||||
"exceptionDomain": "",
|
||||
"signingIdentity": null,
|
||||
"entitlements": null
|
||||
},
|
||||
"windows": {
|
||||
"certificateThumbprint": null,
|
||||
"digestAlgorithm": "sha256",
|
||||
"timestampUrl": ""
|
||||
}
|
||||
"shortDescription": "A Clash Meta GUI based on tauri.",
|
||||
"longDescription": "A Clash Meta GUI based on tauri."
|
||||
},
|
||||
"updater": {
|
||||
"active": true,
|
||||
"endpoints": [
|
||||
"https://github.com/zzzgydi/clash-verge/releases/download/updater/update.json",
|
||||
"https://hub.fastgit.xyz/zzzgydi/clash-verge/releases/download/updater/update-proxy.json"
|
||||
"https://mirror.ghproxy.com/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json",
|
||||
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update.json"
|
||||
],
|
||||
"dialog": false,
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDExNUFBNTBBN0FDNEFBRTUKUldUbHFzUjZDcVZhRVRJM25NS3NkSFlFVElxUkNZMzZ6bHUwRVJjb2F3alJXVzRaeDdSaTA2YWYK"
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK"
|
||||
},
|
||||
"allowlist": {
|
||||
"shell": {
|
||||
@@ -67,23 +45,31 @@
|
||||
},
|
||||
"process": {
|
||||
"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
|
||||
}
|
||||
},
|
||||
"windows": [
|
||||
{
|
||||
"title": "Clash Verge",
|
||||
"width": 800,
|
||||
"height": 600,
|
||||
"resizable": true,
|
||||
"fullscreen": false,
|
||||
"decorations": false,
|
||||
"transparent": true,
|
||||
"minWidth": 600,
|
||||
"minHeight": 520
|
||||
}
|
||||
],
|
||||
"windows": [],
|
||||
"security": {
|
||||
"csp": "script-src 'unsafe-eval' 'self'; default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; img-src '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';"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"tauri": {
|
||||
"systemTray": {
|
||||
"iconPath": "icons/tray-icon.png"
|
||||
},
|
||||
"bundle": {
|
||||
"targets": ["deb", "appimage", "updater"],
|
||||
"deb": {
|
||||
"depends": ["openssl"],
|
||||
"desktopTemplate": "./template/clash-verge.desktop"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"tauri": {
|
||||
"systemTray": {
|
||||
"iconPath": "icons/mac-tray-icon.png",
|
||||
"iconAsTemplate": true
|
||||
},
|
||||
"bundle": {
|
||||
"targets": ["app", "dmg", "updater"],
|
||||
"macOS": {
|
||||
"frameworks": [],
|
||||
"minimumSystemVersion": "10.15",
|
||||
"exceptionDomain": "",
|
||||
"signingIdentity": null,
|
||||
"entitlements": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"tauri": {
|
||||
"systemTray": {
|
||||
"iconPath": "icons/tray-icon.png"
|
||||
},
|
||||
"bundle": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,827 @@
|
||||
; 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
|
||||
|
||||
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 external binaries
|
||||
{{#each binaries}}
|
||||
Delete "$INSTDIR\\{{this}}"
|
||||
{{/each}}
|
||||
|
||||
; Delete uninstaller
|
||||
Delete "$INSTDIR\uninstall.exe"
|
||||
|
||||
${If} $DeleteAppDataCheckboxState == 1
|
||||
RMDir /R /REBOOTOK "$INSTDIR"
|
||||
${Else}
|
||||
{{#each resources_ancestors}}
|
||||
RMDir /REBOOTOK "$INSTDIR\\{{this}}"
|
||||
{{/each}}
|
||||
RMDir "$INSTDIR"
|
||||
${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
|
||||
@@ -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_290_83)"/>
|
||||
<path d="M26.1791 19.6042C25.8714 21.1676 25.1224 22.6103 24.0208 23.7615C22.9191 24.9128 21.5109 25.7246 19.9625 26.1008C20.3875 25.5992 20.7558 24.9492 21.0616 24.2117C21.3658 23.4783 21.62 22.6275 21.8083 21.695C23.2058 21.335 24.4716 20.7817 25.5333 20.0742C25.7566 19.925 25.9725 19.7683 26.1791 19.6042ZM9.82082 19.6042C10.0275 19.7683 10.2433 19.925 10.4666 20.0742C11.5275 20.7817 12.7933 21.335 14.1916 21.695C14.3791 22.6275 14.6333 23.4783 14.9383 24.2117C15.2433 24.9483 15.6116 25.5992 16.0366 26.1008C14.4884 25.7244 13.0803 24.9126 11.9788 23.7613C10.8773 22.6101 10.1285 21.1675 9.82082 19.6042ZM20.0225 22.0375C19.8927 22.5611 19.7254 23.0747 19.5216 23.5742C19.2325 24.2725 18.9141 24.7867 18.6125 25.1125C18.3041 25.4458 18.0925 25.5 18 25.5C17.9075 25.5 17.6966 25.4458 17.3875 25.1125C17.0858 24.7875 16.7675 24.2725 16.4783 23.5742C16.2746 23.0747 16.1072 22.5611 15.9775 22.0375C16.6341 22.1225 17.3108 22.1667 18 22.1667C18.6762 22.1671 19.3518 22.1239 20.0225 22.0375ZM20.4691 16.9408C20.5344 18.0629 20.4947 19.1886 20.3508 20.3033C19.6033 20.4317 18.815 20.5 18 20.5C17.2119 20.5009 16.4253 20.4351 15.6483 20.3033C15.5023 19.1888 15.463 18.0629 15.5308 16.9408C16.3191 17.0883 17.1475 17.1667 18 17.1667C18.8282 17.1676 19.6548 17.092 20.4691 16.9408ZM25.555 14.4792C25.9383 15.3008 26.1925 16.1958 26.2883 17.1367C25.8708 17.6975 25.3075 18.2217 24.6083 18.6867C23.8833 19.17 23.0283 19.5783 22.0758 19.8842C22.1845 18.7637 22.1962 17.6359 22.1108 16.5133C22.8976 16.2518 23.6486 15.8932 24.3466 15.4458C24.7812 15.1654 25.186 14.8416 25.555 14.4792ZM10.445 14.4792C10.8033 14.835 11.2116 15.1583 11.6533 15.4458C12.3512 15.8934 13.1023 16.252 13.8891 16.5133C13.804 17.6359 13.8154 18.7636 13.9233 19.8842C12.9708 19.5783 12.1158 19.1708 11.3908 18.6875C10.6925 18.2217 10.1291 17.6975 9.71082 17.1367C9.80748 16.195 10.0608 15.3008 10.4441 14.4783L10.445 14.4792ZM18 10.5C18.0916 10.5 18.3033 10.555 18.6125 10.8883C18.9141 11.2133 19.2325 11.7283 19.5216 12.4267C19.8466 13.2125 20.1166 14.1833 20.29 15.2775C19.5716 15.4217 18.8025 15.5 18 15.5C17.2309 15.5015 16.4636 15.4269 15.7091 15.2775C15.8825 14.1833 16.1525 13.2125 16.4783 12.4275C16.7675 11.7283 17.0858 11.2142 17.3866 10.8883C17.6966 10.555 17.9075 10.5 18 10.5ZM19.9625 9.89917C21.8406 10.3553 23.5019 11.4496 24.6625 12.995C24.3075 13.4034 23.8951 13.7582 23.4383 14.0483C22.9554 14.3584 22.4408 14.6162 21.9033 14.8175C21.7108 13.6833 21.42 12.655 21.0616 11.7892C20.7558 11.0517 20.3883 10.4008 19.9625 9.89917ZM16.0375 9.89917C15.6125 10.4008 15.2441 11.0517 14.9383 11.7892C14.58 12.6558 14.2883 13.6833 14.0966 14.8175C13.5592 14.6162 13.0446 14.3584 12.5616 14.0483C12.1046 13.7583 11.6919 13.4035 11.3366 12.995C12.4974 11.4494 14.1591 10.3551 16.0375 9.89917Z" fill="white"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_290_83" 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: 3.1 KiB |
@@ -0,0 +1,21 @@
|
||||
<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_289_53)"/>
|
||||
<rect width="36" height="36" rx="18" fill="url(#paint1_linear_289_53)"/>
|
||||
<mask id="mask0_289_53" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="8" y="8" width="20" height="20">
|
||||
<path d="M28 8V28H8V8H28ZM18.4942 27.3817L18.485 27.3833L18.4258 27.4125L18.4092 27.4158L18.3975 27.4125L18.3383 27.3833C18.33 27.38 18.3225 27.3825 18.3183 27.3875L18.315 27.3958L18.3008 27.7525L18.305 27.7692L18.3133 27.78L18.4 27.8417L18.4125 27.845L18.4225 27.8417L18.5092 27.78L18.5192 27.7667L18.5225 27.7525L18.5083 27.3967C18.5067 27.3883 18.5008 27.3825 18.4942 27.3817ZM18.715 27.2875L18.7042 27.2892L18.55 27.3667L18.5417 27.375L18.5392 27.3842L18.5542 27.7425L18.5583 27.7525L18.565 27.7583L18.7325 27.8358C18.7425 27.8392 18.7517 27.8358 18.7567 27.8292L18.76 27.8175L18.7317 27.3058C18.7292 27.2958 18.7233 27.2892 18.715 27.2875ZM18.1192 27.2892C18.1154 27.2874 18.1111 27.287 18.1071 27.2879C18.103 27.2888 18.0994 27.291 18.0967 27.2942L18.0917 27.3058L18.0633 27.8175C18.0633 27.8275 18.0692 27.8342 18.0775 27.8375L18.09 27.8358L18.2575 27.7583L18.2658 27.7517L18.2692 27.7425L18.2833 27.3842L18.2808 27.3742L18.2725 27.3658L18.1192 27.2892Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_289_53)">
|
||||
<path d="M15.9167 9.66669C15.5852 9.66669 15.2672 9.79838 15.0328 10.0328C14.7984 10.2672 14.6667 10.5852 14.6667 10.9167V11.75C14.6667 12.0815 14.7984 12.3995 15.0328 12.6339C15.2672 12.8683 15.5852 13 15.9167 13H20.0834C20.4149 13 20.7328 12.8683 20.9672 12.6339C21.2017 12.3995 21.3334 12.0815 21.3334 11.75V10.9167C21.3334 10.5852 21.2017 10.2672 20.9672 10.0328C20.7328 9.79838 20.4149 9.66669 20.0834 9.66669H15.9167Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.4167 11.3642C12.3683 11.4225 11.7067 11.5892 11.2325 12.0642C10.5 12.7967 10.5 13.975 10.5 16.3317V21.3317C10.5 23.6892 10.5 24.8675 11.2325 25.6C11.9642 26.3317 13.1433 26.3317 15.5 26.3317H20.5C22.8567 26.3317 24.0358 26.3317 24.7675 25.6C25.5 24.8667 25.5 23.6892 25.5 21.3317V16.3317C25.5 13.975 25.5 12.7967 24.7675 12.0642C24.2933 11.5892 23.6317 11.4225 22.5833 11.3642V11.75C22.5833 12.4131 22.3199 13.049 21.8511 13.5178C21.3823 13.9866 20.7464 14.25 20.0833 14.25H15.9167C15.2536 14.25 14.6177 13.9866 14.1489 13.5178C13.6801 13.049 13.4167 12.4131 13.4167 11.75V11.3642ZM13.8333 16.125C13.6676 16.125 13.5086 16.1909 13.3914 16.3081C13.2742 16.4253 13.2083 16.5843 13.2083 16.75C13.2083 16.9158 13.2742 17.0748 13.3914 17.192C13.5086 17.3092 13.6676 17.375 13.8333 17.375H14.25C14.4158 17.375 14.5747 17.3092 14.6919 17.192C14.8092 17.0748 14.875 16.9158 14.875 16.75C14.875 16.5843 14.8092 16.4253 14.6919 16.3081C14.5747 16.1909 14.4158 16.125 14.25 16.125H13.8333ZM16.75 16.125C16.5842 16.125 16.4253 16.1909 16.3081 16.3081C16.1908 16.4253 16.125 16.5843 16.125 16.75C16.125 16.9158 16.1908 17.0748 16.3081 17.192C16.4253 17.3092 16.5842 17.375 16.75 17.375H22.1667C22.3324 17.375 22.4914 17.3092 22.6086 17.192C22.7258 17.0748 22.7917 16.9158 22.7917 16.75C22.7917 16.5843 22.7258 16.4253 22.6086 16.3081C22.4914 16.1909 22.3324 16.125 22.1667 16.125H16.75ZM13.8333 19.0417C13.6676 19.0417 13.5086 19.1075 13.3914 19.2248C13.2742 19.342 13.2083 19.5009 13.2083 19.6667C13.2083 19.8325 13.2742 19.9914 13.3914 20.1086C13.5086 20.2258 13.6676 20.2917 13.8333 20.2917H14.25C14.4158 20.2917 14.5747 20.2258 14.6919 20.1086C14.8092 19.9914 14.875 19.8325 14.875 19.6667C14.875 19.5009 14.8092 19.342 14.6919 19.2248C14.5747 19.1075 14.4158 19.0417 14.25 19.0417H13.8333ZM16.75 19.0417C16.5842 19.0417 16.4253 19.1075 16.3081 19.2248C16.1908 19.342 16.125 19.5009 16.125 19.6667C16.125 19.8325 16.1908 19.9914 16.3081 20.1086C16.4253 20.2258 16.5842 20.2917 16.75 20.2917H22.1667C22.3324 20.2917 22.4914 20.2258 22.6086 20.1086C22.7258 19.9914 22.7917 19.8325 22.7917 19.6667C22.7917 19.5009 22.7258 19.342 22.6086 19.2248C22.4914 19.1075 22.3324 19.0417 22.1667 19.0417H16.75ZM13.8333 21.9584C13.6676 21.9584 13.5086 22.0242 13.3914 22.1414C13.2742 22.2586 13.2083 22.4176 13.2083 22.5834C13.2083 22.7491 13.2742 22.9081 13.3914 23.0253C13.5086 23.1425 13.6676 23.2084 13.8333 23.2084H14.25C14.4158 23.2084 14.5747 23.1425 14.6919 23.0253C14.8092 22.9081 14.875 22.7491 14.875 22.5834C14.875 22.4176 14.8092 22.2586 14.6919 22.1414C14.5747 22.0242 14.4158 21.9584 14.25 21.9584H13.8333ZM16.75 21.9584C16.5842 21.9584 16.4253 22.0242 16.3081 22.1414C16.1908 22.2586 16.125 22.4176 16.125 22.5834C16.125 22.7491 16.1908 22.9081 16.3081 23.0253C16.4253 23.1425 16.5842 23.2084 16.75 23.2084H22.1667C22.3324 23.2084 22.4914 23.1425 22.6086 23.0253C22.7258 22.9081 22.7917 22.7491 22.7917 22.5834C22.7917 22.4176 22.7258 22.2586 22.6086 22.1414C22.4914 22.0242 22.3324 21.9584 22.1667 21.9584H16.75Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_289_53" x1="6" y1="6.5" x2="29.5" y2="30.5" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#56718E"/>
|
||||
<stop offset="1" stop-color="#4B6683"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_289_53" 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: 5.2 KiB |
@@ -0,0 +1,15 @@
|
||||
<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_293_142)"/>
|
||||
<g clip-path="url(#clip0_293_142)">
|
||||
<path d="M24.5167 13C24.7587 13.0001 24.9978 13.0529 25.2173 13.1547C25.4369 13.2566 25.6316 13.405 25.788 13.5897C25.9444 13.7744 26.0587 13.9909 26.1229 14.2243C26.1872 14.4576 26.1998 14.7021 26.16 14.9408L24.7717 23.2742C24.7068 23.6633 24.5059 24.0168 24.2049 24.2718C23.9038 24.5268 23.5221 24.6667 23.1275 24.6667H12.8725C12.478 24.6667 12.0962 24.5268 11.7952 24.2718C11.4941 24.0168 11.2933 23.6633 11.2284 23.2742L9.84003 14.9408C9.80022 14.7021 9.81287 14.4576 9.87711 14.2243C9.94135 13.9909 10.0556 13.7744 10.212 13.5897C10.3684 13.405 10.5632 13.2566 10.7827 13.1547C11.0023 13.0529 11.2413 13.0001 11.4834 13H24.5167ZM23 10.5C23.221 10.5 23.433 10.5878 23.5893 10.7441C23.7456 10.9004 23.8334 11.1123 23.8334 11.3333C23.8334 11.5543 23.7456 11.7663 23.5893 11.9226C23.433 12.0789 23.221 12.1667 23 12.1667H13C12.779 12.1667 12.5671 12.0789 12.4108 11.9226C12.2545 11.7663 12.1667 11.5543 12.1667 11.3333C12.1667 11.1123 12.2545 10.9004 12.4108 10.7441C12.5671 10.5878 12.779 10.5 13 10.5H23Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_293_142" x1="31" y1="27.5" x2="6.5" y2="7" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#6038CB"/>
|
||||
<stop offset="1" stop-color="#704ADC"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_293_142">
|
||||
<rect width="20" height="20" fill="white" transform="translate(8 8)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |