Compare commits
2447 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
84fbccbfd9 | ||
|
49c81f6201 | ||
|
c894a15d13 | ||
|
196b887381 | ||
|
2ad20ed239 | ||
|
98de91771e | ||
|
9dfd9bad20 | ||
|
0b8d08d13b | ||
|
0de304d4e3 | ||
|
55f1766ebc | ||
|
6d1a8fb264 | ||
|
d3958594d9 | ||
|
b5952f320b | ||
|
98be9621a6 | ||
|
e4eb13ce22 | ||
|
ecf2da7c0a | ||
|
7d7c8988d7 | ||
|
5e11d36972 | ||
|
3039f39d40 | ||
|
7b5fd104de | ||
|
30ea408019 | ||
|
3fa130695c | ||
|
fece31438a | ||
|
ad1f2bea3b | ||
|
cd4bfdd743 | ||
|
cde8c4004f | ||
|
c53514e060 | ||
|
8e99672265 | ||
|
5b9b5cb6a8 | ||
|
62141380d8 | ||
|
52a15bb281 | ||
|
b092f74c88 | ||
|
937f43c270 | ||
|
9b02088918 | ||
|
1bd503a654 | ||
|
c6477dfda4 | ||
|
4831d88467 | ||
|
9ebde802d4 | ||
|
a9cccc7b97 | ||
|
d54a765bd6 | ||
|
5a2751162f | ||
|
492a5a6de7 | ||
|
f6c0f144a6 | ||
|
1c046f3ca3 | ||
|
4a47c5bb6f | ||
|
b7e01aefb4 | ||
|
e8e16f7d57 | ||
|
59caa22431 | ||
|
e2046f3e48 | ||
|
8fdcffc731 | ||
|
3ec77c6256 | ||
|
b09313756e | ||
|
f800e2e3b6 | ||
|
daad623855 | ||
|
7716e2bc87 | ||
|
70bd5ec03c | ||
|
ce5c86c3b0 | ||
|
a6a6d9d036 | ||
|
971dd6a2cf | ||
|
42d0ea7e36 | ||
|
006bcffe8c | ||
|
ff4101fa47 | ||
|
7280635741 | ||
|
7ede91599c | ||
|
6e40dd9862 | ||
|
42db9ea0bb | ||
|
ca0cf4552c | ||
|
d91653b218 | ||
|
1ace560531 | ||
|
81968a579d | ||
|
5a0eb56f70 | ||
|
804fad6083 | ||
|
98d3a48710 | ||
|
0ec4f46052 | ||
|
a891341e35 | ||
|
10426af3ad | ||
|
5be1d604ee | ||
|
1baa840160 | ||
|
14347f60d5 | ||
|
df5424d55e | ||
|
12065330e1 | ||
|
e054ac67fb | ||
|
31a7750482 | ||
|
2e38307f65 | ||
|
47accdd2b1 | ||
|
cb0146573f | ||
|
cf78bb3686 | ||
|
b5b5ae4e7b | ||
|
b99bc7fcd1 | ||
|
f50fe9159d | ||
|
09f6917638 | ||
|
e330d75a89 | ||
|
4f0ce7458e | ||
|
a2811c4803 | ||
|
1c233783a7 | ||
|
3e45cc4650 | ||
|
1a7c076e07 | ||
|
da4fddf150 | ||
|
970eb62aa6 | ||
|
d669650758 | ||
|
69347160e9 | ||
|
a345b54a77 | ||
|
8aabcd77a5 | ||
|
c30f54609d | ||
|
0830236a73 | ||
|
44f21444bb | ||
|
1d88d98ea1 | ||
|
86f69fd574 | ||
|
e21846a2ce | ||
|
d5981ca94f | ||
|
8c5eb3b550 | ||
|
55dc416109 | ||
|
ec30b888d1 | ||
|
2a92755e65 | ||
|
6976ea3c09 | ||
|
b07ed2dbf5 | ||
|
2ab923da87 | ||
|
9799d4f747 | ||
|
f739836891 | ||
|
a28887be8e | ||
|
0f13691ae0 | ||
|
ae72b83dbe | ||
|
2e38404434 | ||
|
11b8c8be45 | ||
|
a06597a3a6 | ||
|
108840c4be | ||
|
16c8672aeb | ||
|
167edcf8ef | ||
|
d6dd89b674 | ||
|
fac2ee6374 | ||
|
dd7876845a | ||
|
56e6139c2b | ||
|
04bdd48a2a | ||
|
5b47fe5b88 | ||
|
84a5cf6b89 | ||
|
618ba52bca | ||
|
5c0cde517f | ||
|
1b249564a3 | ||
|
81b5501b0e | ||
|
91ccb3045c | ||
|
e31f176c25 | ||
|
ad45485009 | ||
|
25e5cf2ac2 | ||
|
bd58d935c6 | ||
|
da2705ff7d | ||
|
61f019f194 | ||
|
74e441df5b | ||
|
772ecdd3b0 | ||
|
baa535b609 | ||
|
a2ff0a7e20 | ||
|
84732f9835 | ||
|
dd17bcb0d6 | ||
|
cab8e613a6 | ||
|
fe1227618a | ||
|
596c52de87 | ||
|
ba5d5e9f86 | ||
|
530669d288 | ||
|
70b0f9a03a | ||
|
105de99d06 | ||
|
6239f81f36 | ||
|
697d200ffe | ||
|
16d5077f55 | ||
|
e0e1a05448 | ||
|
bcaafa67a3 | ||
|
36142656a4 | ||
|
d6a48deb5a | ||
|
e98ce0c2ae | ||
|
8118fc754c | ||
|
1ec7a0f23c | ||
|
488e8ef1d5 | ||
|
1f99cee78b | ||
|
c25015ed54 | ||
|
aaefc5b479 | ||
|
1c58816c73 | ||
|
0fd99358aa | ||
|
d4012bace9 | ||
|
af7660686d | ||
|
b57c6e408a | ||
|
124934b012 | ||
|
1bef6d085d | ||
|
c73927c5ba | ||
|
692deb6012 | ||
|
8ec499f631 | ||
|
2bcd653a56 | ||
|
0f10952979 | ||
|
58fa67100f | ||
|
8e294916c4 | ||
|
6877e0c95d | ||
|
37a333a023 | ||
|
48f1da963a | ||
|
e1905aced4 | ||
|
f18202a3a4 | ||
|
c1a9de4d66 | ||
|
18f86874ee | ||
|
e6686e0b82 | ||
|
4bf166986d | ||
|
0f60d84f6c | ||
|
15e54df67c | ||
|
4cb6ad7736 | ||
|
e27a32395a | ||
|
eddcf209c1 | ||
|
10a151d411 | ||
|
54d5586a60 | ||
|
30ca547e50 | ||
|
a1944d1a90 | ||
|
c2b35fdaa5 | ||
|
805b54d81e | ||
|
e3579dac65 | ||
|
f80591242e | ||
|
69cb9769c1 | ||
|
efd42d9da0 | ||
|
21a6340095 | ||
|
6c96724dce | ||
|
ebb194d2a2 | ||
|
1a51a92b70 | ||
|
5760f16272 | ||
|
4ed36f6223 | ||
|
7ea7ca1415 | ||
|
1ee8786ab7 | ||
|
44ca513241 | ||
|
73310b466b | ||
|
1ba688727e | ||
|
3b69465016 | ||
|
3e53ea7209 | ||
|
07bdc108ed | ||
|
a18efb0e71 | ||
|
de1c825ad3 | ||
|
de2cff824e | ||
|
aff504bddc | ||
|
277390e597 | ||
|
fdcefe458e | ||
|
9bb2160abe | ||
|
97d683541d | ||
|
9f7ffb80e1 | ||
|
c957ea7b24 | ||
|
181fce16b1 | ||
|
825f023505 | ||
|
347ea53b32 | ||
|
d525e0dd70 | ||
|
365e844b83 | ||
|
44bdeb555a | ||
|
520c33557e | ||
|
bfad5ac091 | ||
|
3ecf4bc238 | ||
|
028e4012aa | ||
|
dc6d429b9c | ||
|
625cf1a803 | ||
|
9ee011514a | ||
|
184fd4a1ba | ||
|
23dcfd9401 | ||
|
1cdba297fb | ||
|
1ed6743bbb | ||
|
41c42bba32 | ||
|
8fb66ea32c | ||
|
51d4c1c4a5 | ||
|
86e069994e | ||
|
1cb923b6d8 | ||
|
b1d003b073 | ||
|
0fb4254481 | ||
|
ae7f456011 | ||
|
cd4bec6bfd | ||
|
5906e0126d | ||
|
dce1395af1 | ||
|
e7db13af37 | ||
|
2eee8cd7d3 | ||
|
836a2abae1 | ||
|
a68a86d6db | ||
|
ee9f0990fd | ||
|
59d0629e3f | ||
|
17af292761 | ||
|
a4dd4bcc8a | ||
|
1a9b0a476b | ||
|
bb015506e7 | ||
|
76be5d8469 | ||
|
1258e187f5 | ||
|
4056a4c35f | ||
|
b6677f0f72 | ||
|
e8c1e6f241 | ||
|
618595ac4c | ||
|
d54ba48c11 | ||
|
a489012a0c | ||
|
a5acdc04e3 | ||
|
709a20ed7b | ||
|
c88f2099c1 | ||
|
f72a2a943b | ||
|
c51199719d | ||
|
bf374f2e85 | ||
|
34f450fcdb | ||
|
23f75598e5 | ||
|
afc238d60e | ||
|
1291c38d58 | ||
|
16caccde51 | ||
|
33f199fcd2 | ||
|
2b534e0d51 | ||
|
23d1d210c7 | ||
|
39a1d6202a | ||
|
f00a5af6c9 | ||
|
48d68f5766 | ||
|
f948da748e | ||
|
8b25d45109 | ||
|
c4ddc35746 | ||
|
0122e2bdcf | ||
|
94b75f463b | ||
|
3c2e04290c | ||
|
f0331ec2d9 | ||
|
3b4013a1b0 | ||
|
31ddccd3e1 | ||
|
d29fe4cb6c | ||
|
6763537f22 | ||
|
8ab4bd6293 | ||
|
31bc644763 | ||
|
fcd672abeb | ||
|
5f550da0bb | ||
|
e865a86eef | ||
|
80ee2e4289 | ||
|
4d327594d3 | ||
|
3363c37457 | ||
|
cee9be81bf | ||
|
932d36462f | ||
|
75c930f7ef | ||
|
bdb178d893 | ||
|
3b0635e8a1 | ||
|
f5760784bf | ||
|
67f3554095 | ||
|
3bb3872e38 | ||
|
c98330ea1f | ||
|
d895b68f04 | ||
|
e230981ac4 | ||
|
20763a741a | ||
|
4babcd9442 | ||
|
d75f36066a | ||
|
26ca4670ad | ||
|
c4d6c167a2 | ||
|
e5af9541da | ||
|
89d9f47191 | ||
|
ff2cf30238 | ||
|
ebe0899eb1 | ||
|
a3d0a38b1e | ||
|
63bd0c87b2 | ||
|
db593fb188 | ||
|
67ae10b593 | ||
|
c8d91c9e14 | ||
|
6c54f5e9b4 | ||
|
8749648d97 | ||
|
0b75b5ef26 | ||
|
fbcadd0493 | ||
|
75bb7a4dd7 | ||
|
4604fe4841 | ||
|
e8badb0c0f | ||
|
8906a8f3c6 | ||
|
4e32990a5d | ||
|
9f2583d1f2 | ||
|
a9b3d8885d | ||
|
29ec4dc546 | ||
|
d0d5204cbc | ||
|
3d84acd7ac | ||
|
5057221f59 | ||
|
3ddfbc5d2f | ||
|
362270e3ea | ||
|
db91177e90 | ||
|
d2f51ce509 | ||
|
146a66fb09 | ||
|
03305f03c1 | ||
|
82e76bc58e | ||
|
e8ff6c785a | ||
|
a8fafb469a | ||
|
b9a220cb63 | ||
|
0b44d40b39 | ||
|
6b349eda45 | ||
|
ad335ba005 | ||
|
04d766884a | ||
|
44d1ec433d | ||
|
97864e8df3 | ||
|
3916293e8f | ||
|
a527177b67 | ||
|
9c027b10b2 | ||
|
5da7086475 | ||
|
0006012ae7 | ||
|
a3f46ec037 | ||
|
bfea52f9dd | ||
|
53334f05b8 | ||
|
ba195c41b6 | ||
|
cca2f1ce61 | ||
|
a51191c661 | ||
|
4cdb5f93b9 | ||
|
fae658c9c2 | ||
|
886a469634 | ||
|
c3c1394e86 | ||
|
6a00255fff | ||
|
4f0aae0879 | ||
|
4f4fe4c41c | ||
|
2a4a3c8250 | ||
|
7864acbadb | ||
|
ba18e64be0 | ||
|
ba8c1e5eb2 | ||
|
2737fb2d87 | ||
|
899285735f | ||
|
f561d12d35 | ||
|
be258b13e0 | ||
|
dbce6b5f1a | ||
|
30f0c99a58 | ||
|
49880c05d9 | ||
|
dc2fc84f58 | ||
|
78c2a1694f | ||
|
baf34dd0d3 | ||
|
48f9dede7b | ||
|
a1f2a621ef | ||
|
a1e8ddb461 | ||
|
d33d90a36e | ||
|
c3114b876f | ||
|
50285aebde | ||
|
0eb5ee6ea8 | ||
|
16c9c95e19 | ||
|
3bc4da3e85 | ||
|
a028a2e1cc | ||
|
9675a35dff | ||
|
c1546fdd64 | ||
|
a109efc1d6 | ||
|
0782b25830 | ||
|
0041ff13b8 | ||
|
4693a25aa0 | ||
|
609df5b4a6 | ||
|
6df8140cb1 | ||
|
65b4cb3191 | ||
|
e1de481349 | ||
|
6e1cc80b91 | ||
|
b658ce7e75 | ||
|
f91f374dfa | ||
|
56f6de5410 | ||
|
94d22ecfc3 | ||
|
e25d71c6c8 | ||
|
bb1b156d2f | ||
|
1b80ddf1e9 | ||
|
66d2fe9074 | ||
|
6e36910734 | ||
|
a553a33c46 | ||
|
2a6f8b401b | ||
|
fa30567140 | ||
|
243f685b83 | ||
|
6cf2373b34 | ||
|
e842ea745a | ||
|
9696c7cec0 | ||
|
c4986eec50 | ||
|
61079e769e | ||
|
00d2c915d1 | ||
|
6ad975c420 | ||
|
1cd1a2d907 | ||
|
39a3c3d3a7 | ||
|
dfefcf03ad | ||
|
825b00e618 | ||
|
ae562e1e92 | ||
|
21c7888595 | ||
|
3b87a4f9d0 | ||
|
8564a58eab | ||
|
c5d009c2cd | ||
|
c2e165d825 | ||
|
922020c57a | ||
|
8cdc33beab | ||
|
23eafdfe00 | ||
|
e72e8ea631 | ||
|
a610a43db0 | ||
|
4a90ffe619 | ||
|
18e8357b6a | ||
|
df39347b19 | ||
|
a36261d705 | ||
|
f133d22124 | ||
|
6ba276b43f | ||
|
44db98f260 | ||
|
8873526619 | ||
|
37c2599754 | ||
|
a079b470b8 | ||
|
0f9ed02bf0 | ||
|
9aeb68205c | ||
|
82b4cf259c | ||
|
566fd3e88b | ||
|
fbecf4f47b | ||
|
52899d4def | ||
|
a89a828b35 | ||
|
4d0dbdaced | ||
|
8003f9902e | ||
|
15bd7324fe | ||
|
bb44fc51bd | ||
|
67a32e60c7 | ||
|
960725777c | ||
|
98c6e0311b | ||
|
95b7641f9c | ||
|
18b0c3f7aa | ||
|
49d3644d6a | ||
|
ee9d12d933 | ||
|
5d37015f4d | ||
|
dca25637c9 | ||
|
a7020fd46c | ||
|
1ef2b1aaf1 | ||
|
a7a661e60f | ||
|
2a9e2d47f5 | ||
|
e33b3043df | ||
|
3f41618aa1 | ||
|
a507d7567f | ||
|
62a6f58705 | ||
|
77dd074fc3 | ||
|
e8c0051be3 | ||
|
b3923eafc7 | ||
|
9ebd96611a | ||
|
824325a2eb | ||
|
e8b3bd5bdc | ||
|
a59fda512c | ||
|
ae181f6835 | ||
|
67bb242778 | ||
|
2028c189aa | ||
|
ba0dc4fb81 | ||
|
c40db417d2 | ||
|
0eb776cdd3 | ||
|
c79a7a7f6f | ||
|
1e3c995e6a | ||
|
3f79e42628 | ||
|
c16ae89a3d | ||
|
7132eaeb11 | ||
|
aef96f0d27 | ||
|
b20a56f1de | ||
|
3073b4e48e | ||
|
7b53752ccd | ||
|
03eedf6175 | ||
|
2330a4bc93 | ||
|
36afae50b1 | ||
|
272ee7577c | ||
|
7f34073da6 | ||
|
f46ee2a0a3 | ||
|
4a79f0c75d | ||
|
27a78af269 | ||
|
586af67829 | ||
|
575d8c4240 | ||
|
d32734214b | ||
|
22ce5aab25 | ||
|
9f90a1c58e | ||
|
b5e0374946 | ||
|
44cb1c7f3e | ||
|
80aba859e7 | ||
|
08360edd26 | ||
|
6e69b3f032 | ||
|
19bb9c7f50 | ||
|
f5dee51e9c | ||
|
bd37fef720 | ||
|
c22e4e5e2c | ||
|
2887a2b6d3 | ||
|
01bde19701 | ||
|
792f1826ee | ||
|
c16795dce9 | ||
|
c1597a0968 | ||
|
590aa950df | ||
|
402018b95c | ||
|
d5101ac2f3 | ||
|
251942c91d | ||
|
fe86b812cd | ||
|
cb3bff589f | ||
|
ec7d7ec559 | ||
|
24c7a5b805 | ||
|
0a4ecb1507 | ||
|
d736dace50 | ||
|
70bbab909f | ||
|
6625f78e4f | ||
|
da2b4c8858 | ||
|
12df415dfd | ||
|
2493f463f3 | ||
|
f4238b1fb9 | ||
|
794783ab4e | ||
|
02634622a5 | ||
|
ac24501e76 | ||
|
e40ea38112 | ||
|
b809b9bb80 | ||
|
73bad8f355 | ||
|
ac884da56b | ||
|
c35ab2e1cd | ||
|
ed3907c273 | ||
|
5e00287045 | ||
|
95c6578911 | ||
|
d22097ee33 | ||
|
74251af163 | ||
|
7c1b11851f | ||
|
f8724c4cb9 | ||
|
3baac034e5 | ||
|
114f1426f3 | ||
|
7de63cea5c | ||
|
34e3af2b38 | ||
|
da907d0eea | ||
|
9cbc2d9206 | ||
|
db2e466d60 | ||
|
e5cdbf7361 | ||
|
a919f493d6 | ||
|
3660298683 | ||
|
c845efe475 | ||
|
38eb47132d | ||
|
b1f097f32b | ||
|
d3123253b3 | ||
|
7b1ec1ec22 | ||
|
4cefacfe73 | ||
|
978acfa471 | ||
|
fb2d138cbf | ||
|
0edd63edb5 | ||
|
97b730668c | ||
|
26b8cf6d52 | ||
|
a979638368 | ||
|
97f434ad4a | ||
|
34af040c48 | ||
|
cc81b443be | ||
|
d44f3c22c7 | ||
|
3795b537f6 | ||
|
ecb5f0885c | ||
|
86d2234713 | ||
|
62ddf26150 | ||
|
ec14b7c52f | ||
|
5f9cc38e82 | ||
|
f48c58f299 | ||
|
c2843f3c4b | ||
|
5d33df4e12 | ||
|
c030fb47ca | ||
|
964daadb18 | ||
|
71a5698ac7 | ||
|
d41d74d0f8 | ||
|
f6c7a611a3 | ||
|
06f4e79e5c | ||
|
154cf44f0a | ||
|
f6e2ff0e44 | ||
|
2aba616f7f | ||
|
95e21386b8 | ||
|
250e908d9a | ||
|
9742fb296c | ||
|
9ff3c2c0d4 | ||
|
9dd7bd9530 | ||
|
6f477b7147 | ||
|
8389826e30 | ||
|
aa31fb7470 | ||
|
a013fe663c | ||
|
89ce497431 | ||
|
60c0b649e8 | ||
|
2bbb5ea23b | ||
|
4f9c1533c1 | ||
|
a6a3847e30 | ||
|
118f38dba3 | ||
|
879f946b28 | ||
|
6acb8a5a91 | ||
|
a38f1e92e3 | ||
|
4ca977466e | ||
|
ec45dc56fb | ||
|
5686302653 | ||
|
6322773513 | ||
|
8e845fc919 | ||
|
f90c8f2ae5 | ||
|
b5af06529f | ||
|
fac3669f8e | ||
|
28ff8d6dcc | ||
|
d0e7f6673c | ||
|
800dc21202 | ||
|
f52089a674 | ||
|
82543de95e | ||
|
12db69407e | ||
|
9b2b447b8b | ||
|
35f5e4ca41 | ||
|
8a69713f6c | ||
|
efd8ef0380 | ||
|
c5eacd1627 | ||
|
5fdb52d8d0 | ||
|
7869ce060f | ||
|
e0d96c0ce1 | ||
|
30678904ee | ||
|
953be61d89 | ||
|
d8c85007d4 | ||
|
591c1cb454 | ||
|
0ca90ed082 | ||
|
4c963b3978 | ||
|
071665f0c3 | ||
|
9a7826752f | ||
|
44b4187365 | ||
|
148807543f | ||
|
2b9fa09293 | ||
|
10211d1d03 | ||
|
46811f33ad | ||
|
32b16790d3 | ||
|
10592ca5a8 | ||
|
dc5cb2e1b8 | ||
|
c10d782524 | ||
|
d73366984f | ||
|
60b1e47ae6 | ||
|
9591fb2c21 | ||
|
c3cba03ac6 | ||
|
ce7818c436 | ||
|
f367a81e44 | ||
|
de507f7ec9 | ||
|
0a8be603c8 | ||
|
d7f033bd46 | ||
|
1fb3b87697 | ||
|
b9c8fa61b2 | ||
|
99ea6d5080 | ||
|
0bacfa9286 | ||
|
b350b605a8 | ||
|
d1eeeab7b1 | ||
|
54296ba84a | ||
|
f82b0f259c | ||
|
57f1c005e6 | ||
|
d9e5387bff | ||
|
3154b8ce55 | ||
|
45b48ede44 | ||
|
961b86dcd2 | ||
|
4d57c64b0d | ||
|
1c894f3cfa | ||
|
3d6faecaed | ||
|
2263ade187 | ||
|
2cdf33d8a1 | ||
|
f6fce6bd31 | ||
|
f9f1721d66 | ||
|
0792ac7de8 | ||
|
98edb048b7 | ||
|
52fcdf28fa | ||
|
d9671faca7 | ||
|
f456004543 | ||
|
a38040d0ea | ||
|
f18cd92318 | ||
|
06e1d0f8da | ||
|
dffd663d7a | ||
|
414f9e9e96 | ||
|
c17ea74856 | ||
|
9a08740e5b | ||
|
8bea0db843 | ||
|
2c612e371f | ||
|
2f61dc9bc6 | ||
|
d18b78c11c | ||
|
95fb4f2e50 | ||
|
f31fe1440d | ||
|
10cd0a1f30 | ||
|
659f854f62 | ||
|
4a7f8dbe09 | ||
|
3e19e574e6 | ||
|
2af2d3664f | ||
|
7dad46adb4 | ||
|
b6fc6a751a | ||
|
934674a8d7 | ||
|
c66986f065 | ||
|
fc49e4a0da | ||
|
b6e1d71b81 | ||
|
b3626a786d | ||
|
a398e28ac0 | ||
|
892d4e597e | ||
|
e8311dd306 | ||
|
a0b266fef8 | ||
|
983d1ea361 | ||
|
db615b932c | ||
|
07415e512f | ||
|
0c6d417d8c | ||
|
772e01ad40 | ||
|
1b2509d5bc | ||
|
fd963a8e66 | ||
|
3c17fca369 | ||
|
4a76997044 | ||
|
894960ef5a | ||
|
3c24d4bc4e | ||
|
ed7e6a3495 | ||
|
07de032e62 | ||
|
c07165531a | ||
|
b1a22d4412 | ||
|
6734e5ef57 | ||
|
6cc81fe6b8 | ||
|
ad80d21e89 | ||
|
97689c6cbb | ||
|
41c83dabde | ||
|
f909f0dcf9 | ||
|
5c9bf30c79 | ||
|
6efc518eed | ||
|
198bd3a3dc | ||
|
7b1055702b | ||
|
d21bcce3c4 | ||
|
b5a26941ef | ||
|
6ab7131378 | ||
|
22bcc2e438 | ||
|
32edc0f1fe | ||
|
8faa0ce2c2 | ||
|
bf05e5999b | ||
|
3d7bdded31 | ||
|
7507182097 | ||
|
6853b3c531 | ||
|
f2372a13e8 | ||
|
3f321c8801 | ||
|
8b47107df8 | ||
|
97e7136293 | ||
|
c8db58150e | ||
|
5e1067df59 | ||
|
175444c59f | ||
|
b09d5ff3c9 | ||
|
43fc97137e | ||
|
c67eee57d6 | ||
|
607aa78059 | ||
|
7edbae7b4c | ||
|
232ff38084 | ||
|
d9d9ca67cd | ||
|
57fa48aef4 | ||
|
5a8e0749c2 | ||
|
f834f069cd | ||
|
9b2dc10da2 | ||
|
c3730b7efd | ||
|
23499497a3 | ||
|
3b78d609b7 | ||
|
65529c3356 | ||
|
899849d4dc | ||
|
52393206e6 | ||
|
2cd1fa6601 | ||
|
e371bbedc0 | ||
|
9dde385073 | ||
|
afa3d39cb3 | ||
|
fe41817f25 | ||
|
a865465514 | ||
|
9278e74e9e | ||
|
689a1f739f | ||
|
19dee57b7e | ||
|
8690b91632 | ||
|
fa31cab11b | ||
|
46ee783f99 | ||
|
199bba5da4 | ||
|
16e8791472 | ||
|
1728442d62 | ||
|
22f7f059ce | ||
|
97629c1fc3 | ||
|
4727d613c0 | ||
|
9ed138ea2b | ||
|
2cbd998941 | ||
|
806d70c243 | ||
|
74a1c7d489 | ||
|
f6ed5dc126 | ||
|
e25185b9b8 | ||
|
5e6d8873b9 | ||
|
951b48c337 | ||
|
7f209b76bf | ||
|
890bfbe02d | ||
|
70f8c28ca6 | ||
|
47bacdaed0 | ||
|
bcd8eb2a09 | ||
|
e4855d0143 | ||
|
94f0ff1ed1 | ||
|
b5f0243a89 | ||
|
17e59b8783 | ||
|
ffdf308b40 | ||
|
cdadc80945 | ||
|
294e1f5b10 | ||
|
1c5eab6055 | ||
|
f74f06e403 | ||
|
f04ee0baf2 | ||
|
689273fc24 | ||
|
6f4c59a15c | ||
|
dc87097dfe | ||
|
24f4ab7597 | ||
|
ad94f0a292 | ||
|
695613a063 | ||
|
6f1828eabc | ||
|
bf158b3bf0 | ||
|
13618e6a0a | ||
|
c424e9dec8 | ||
|
a2e9523707 | ||
|
606817ae06 | ||
|
7124d326fc | ||
|
f9f4653e33 | ||
|
bf8eebe537 | ||
|
bd9eef6502 | ||
|
e343b1790e | ||
|
d81ef1d67c | ||
|
6e374bcd4e | ||
|
fb4648d2af | ||
|
a63fc25f14 | ||
|
5e20e9ae1c | ||
|
7d5d604ea6 | ||
|
a722581868 | ||
|
28f3044bdd | ||
|
497804434d | ||
|
d2d6ee806d | ||
|
2e106265f9 | ||
|
28fb0b433b | ||
|
171bd6b327 | ||
|
198e215d54 | ||
|
4d424e70bc | ||
|
3efef52398 | ||
|
b85929772e | ||
|
041522f94e | ||
|
80d3c9e96f | ||
|
6ee5e560cc | ||
|
2be9eb4bae | ||
|
0a8935686a | ||
|
0109d9148b | ||
|
212518c682 | ||
|
59b4f1ebab | ||
|
ee9462c221 | ||
|
c648dc6c99 | ||
|
4f1b8094a3 | ||
|
c89ccf7185 | ||
|
753395965a | ||
|
04be747d52 | ||
|
f828ed3edf | ||
|
b98d9c2932 | ||
|
aba2ce8390 | ||
|
8bd8e149cf | ||
|
e7c359a2e7 | ||
|
d64d25380a | ||
|
6cba6166fb | ||
|
e66f5fe253 | ||
|
1d4388d444 | ||
|
28ab08a7ca | ||
|
6fa0f92ceb | ||
|
3083ab74a6 | ||
|
b6ea73af83 | ||
|
1fa3ffb1ff | ||
|
af89630095 | ||
|
18f0177fce | ||
|
d89eecacba | ||
|
a9149fb92e | ||
|
9a04208a11 | ||
|
6cdf199531 | ||
|
44dc7fe24a | ||
|
455892b414 | ||
|
4f5227782a | ||
|
481e473b60 | ||
|
2c2a1f638b | ||
|
973e269f46 | ||
|
9f76e0e056 | ||
|
3a5f1b41a4 | ||
|
e2d8369daf | ||
|
a20d4959bf | ||
|
7b887e4cdd | ||
|
bc9cbd2993 | ||
|
9baa0e247f | ||
|
2df8d2bc69 | ||
|
b8165fb06e | ||
|
c698b24e01 | ||
|
e70249cb2e | ||
|
8a9bfe8281 | ||
|
8c2a4e627e | ||
|
bf35c92c14 | ||
|
019293a034 | ||
|
46dc40149e | ||
|
444643eb6f | ||
|
2913b911e3 | ||
|
ca323371a7 | ||
|
a06cb39777 | ||
|
b0ec8767a2 | ||
|
353fb49a87 | ||
|
e453b40e0b | ||
|
0c6f8ce77d | ||
|
c0219662bb | ||
|
aae71d375c | ||
|
b6228e4c59 | ||
|
3a9a1439d9 | ||
|
7cf256dc7c | ||
|
c3c26998bf | ||
|
02e860480b | ||
|
7737b8b596 | ||
|
2725322fd5 | ||
|
6c6ccda6b3 | ||
|
d71269e223 | ||
|
36266d2b10 | ||
|
acae62de87 | ||
|
9fe4197cae | ||
|
7fa1a8d54a | ||
|
2333271c20 | ||
|
5b83149567 | ||
|
250a35baab | ||
|
d60ba95532 | ||
|
c901472198 | ||
|
2a5b70fb13 | ||
|
dc6db6e4b3 | ||
|
8df6f32314 | ||
|
a2d8c894fe | ||
|
a1996768f1 | ||
|
205587cb9e | ||
|
224b2ef952 | ||
|
90a83dc753 | ||
|
a7cf968d04 | ||
|
80ff72bae1 | ||
|
5320fc8111 | ||
|
8753531e82 | ||
|
03a845f2b3 | ||
|
25b05f127d | ||
|
073beb0135 | ||
|
ef7659691b | ||
|
e69c0c079e | ||
|
dc31269a06 | ||
|
df9eccabea | ||
|
7788f5ae4c | ||
|
ff5456c178 | ||
|
40ed702437 | ||
|
65924e9a5d | ||
|
a88d149dad | ||
|
b9ec94d835 | ||
|
fc1675575a | ||
|
2f7229720f | ||
|
0187fc7b22 | ||
|
540e1a9650 | ||
|
a371cd1d79 | ||
|
16b11fee31 | ||
|
1bc46f22b4 | ||
|
554c8fe163 | ||
|
8f6bf6e002 | ||
|
d5dd8e9346 | ||
|
4a67e1021a | ||
|
c97061770a | ||
|
55b331511e | ||
|
4b9b5e861f | ||
|
b8599a0642 | ||
|
ae6530585a | ||
|
39aa1fa2a4 | ||
|
4f740acabd | ||
|
2cc9b91895 | ||
|
4eedc39e97 | ||
|
b99e8d7f46 | ||
|
a8a27aeadd | ||
|
21176d2fd3 | ||
|
224c65438f | ||
|
f1c21b642f | ||
|
030d1f374a | ||
|
0b29fa2288 | ||
|
b721f148f0 | ||
|
63434a2f87 | ||
|
0d6f0e66be | ||
|
fb3f1365c5 | ||
|
8de7d5d377 | ||
|
952d7494ac | ||
|
9aeba20086 | ||
|
9150c9c40e | ||
|
3e75897154 | ||
|
b0aa4402c2 | ||
|
41f80bcafd | ||
|
c67359c49d | ||
|
2f7c3cf21e | ||
|
9731c8a750 | ||
|
8092e5c3a8 | ||
|
f7ab8cc471 | ||
|
b593f62c4f | ||
|
6905b7a410 | ||
|
402f27b2a3 | ||
|
c9c46d05d0 | ||
|
6591575d22 | ||
|
6064119779 | ||
|
00cd9b581d | ||
|
a3d7b72485 | ||
|
88c73be2f4 | ||
|
39a9181cdd | ||
|
0e5c6f56a0 | ||
|
5147a070a1 | ||
|
b11be1838a | ||
|
be99768a32 | ||
|
fe439a0cb6 | ||
|
87f49ec879 | ||
|
20f2730125 | ||
|
bc5b34db6b | ||
|
bcf9df3744 | ||
|
4d3674ee0a | ||
|
28567e4629 | ||
|
1180a4fb0b | ||
|
71928d2c9f | ||
|
3853072a2e | ||
|
0cf630ef23 | ||
|
202015fe34 | ||
|
ae43e5cae4 | ||
|
67b67bae6a | ||
|
dbb8fe15cf | ||
|
56efa10f64 | ||
|
5ff776f90d | ||
|
a25b072bf6 | ||
|
c95951c0e4 | ||
|
4c8193b801 | ||
|
598a544ff8 | ||
|
465ef3fa9a | ||
|
2f876d93e3 | ||
|
84e8c44e4f | ||
|
855d794bdb | ||
|
a3333f8fe1 | ||
|
2e64d62ca4 | ||
|
26a3dbcbe1 | ||
|
fa2e86df29 | ||
|
b4f0ece78f | ||
|
630b319a37 | ||
|
4a37e49798 | ||
|
cfbe98a39a | ||
|
91f097d514 | ||
|
c545521cd9 | ||
|
11e0f49ada | ||
|
19ce53128b | ||
|
deccff623a | ||
|
b2a210ec0d | ||
|
bdb5169a6f | ||
|
0865b702a3 | ||
|
d13b8fd486 | ||
|
494911805e | ||
|
cba3a2be24 | ||
|
0686781359 | ||
|
e5b82dca4d | ||
|
2144a42a22 | ||
|
07a989e004 | ||
|
4f7e8116cb | ||
|
c0f650d7dc | ||
|
d4a0136504 | ||
|
5f25e027c4 | ||
|
9610dcce20 | ||
|
3ee3e7c17b | ||
|
98536250bd | ||
|
e95808e6be | ||
|
9fc819a410 | ||
|
b0f1ce1fa0 | ||
|
503579a638 | ||
|
3ad216751a | ||
|
ca8e3179bb | ||
|
fd84e56c00 | ||
|
8b67fb7290 | ||
|
2d1fdb319d | ||
|
c200e18434 | ||
|
b3ffcd020f | ||
|
9258a3dcd4 | ||
|
57f5478731 | ||
|
973d75ebdd | ||
|
33519b27c8 | ||
|
7da7ff4a69 | ||
|
dbd2f697f9 | ||
|
f435762b88 | ||
|
ae46332e42 | ||
|
d003883de9 | ||
|
4e438a44f1 | ||
|
02e19b3d44 | ||
|
ccc19512e7 | ||
|
c34539e389 | ||
|
f8aeacb949 | ||
|
9940190679 | ||
|
e2498b3e91 | ||
|
82246fd9c7 | ||
|
11538552eb | ||
|
4ce28f54de | ||
|
e887ed74a3 | ||
|
28c086e97c | ||
|
11465e89a3 | ||
|
b2197187c1 | ||
|
2b26a10745 | ||
|
daf726ebbf | ||
|
88dd886687 | ||
|
609da457f7 | ||
|
363e28ff69 | ||
|
bdd6bf9020 | ||
|
5c3dab3466 | ||
|
9b2c8fa25d | ||
|
df2f102d9e | ||
|
95ebb0e6d2 | ||
|
e5d03652a9 | ||
|
56b53e2dd8 | ||
|
cf0b7b213f | ||
|
d214c8e01b | ||
|
fba0f362a5 | ||
|
ec05d0857c | ||
|
54b744b7de | ||
|
1a76780fff | ||
|
58f5c44533 | ||
|
d085da4dbf | ||
|
c4a5c356f7 | ||
|
18fdc5c6a2 | ||
|
35dabaab9c | ||
|
9bf31b10bb | ||
|
7186575cb1 | ||
|
2ecae40130 | ||
|
778ed62a90 | ||
|
5e863a87dc | ||
|
7ce8597c25 | ||
|
1992237ce5 | ||
|
8f247b0f73 | ||
|
e2159d80af | ||
|
057023531e | ||
|
dfed65bf9f | ||
|
739161849a | ||
|
c65b280020 | ||
|
d3bcf25ef0 | ||
|
f6bd3340e7 | ||
|
6a7c09bfe3 | ||
|
8b9f294a5d | ||
|
4a282d9629 | ||
|
909b88864f | ||
|
1346a7992c | ||
|
bba607e987 | ||
|
f9cc490c35 | ||
|
a5aec2d9fa | ||
|
c0df368dc6 | ||
|
aa77433523 | ||
|
44ad99f693 | ||
|
b7f7a82ea9 | ||
|
c69978c9fd | ||
|
6174aa6ee1 | ||
|
74b8d2e908 | ||
|
63a515944f | ||
|
5b5db7b860 | ||
|
b3bbacf2ef | ||
|
3a0429d049 | ||
|
ab539081fa | ||
|
f0d88d4e73 | ||
|
772cbd6ffd | ||
|
17b9dbe9d7 | ||
|
75e5d42d8b | ||
|
031c15fd7d | ||
|
de1924cefc | ||
|
66db0a4751 | ||
|
c309410965 | ||
|
7f461b99e2 | ||
|
7bfe0eeae9 | ||
|
8619bd5be3 | ||
|
56011d37d4 | ||
|
c2852c8a82 | ||
|
6f546a424e | ||
|
447f7530af | ||
|
6136f1206b | ||
|
36a3c5b501 | ||
|
dcd6c1f522 | ||
|
2b074bcdcb | ||
|
b20ec7f0eb | ||
|
58cf69a2fe | ||
|
096c148228 | ||
|
a68005d4ab | ||
|
bf3a281987 | ||
|
2965a6827d | ||
|
9f43a73c36 | ||
|
7551b45da2 | ||
|
bca3685eda | ||
|
6d20175800 | ||
|
a62dd4c020 | ||
|
d1d9620a61 | ||
|
5106d77c77 | ||
|
69cf237d7a | ||
|
7d9d1c82b6 | ||
|
228ff5edf4 | ||
|
2baac618a8 | ||
|
e8f499a938 | ||
|
96ca20d0b4 | ||
|
54acdc86e7 | ||
|
0815e02895 | ||
|
30cbb72b57 | ||
|
0b91397709 | ||
|
89934c17ca | ||
|
e4a38e62eb | ||
|
5e1e09d7bf | ||
|
51ce3a1e42 | ||
|
54b46dfad9 | ||
|
787917ac66 | ||
|
619b49bdc4 | ||
|
4b3a73d440 | ||
|
54f9c59d6e | ||
|
1d123996f6 | ||
|
c4768f6138 | ||
|
5f3551ff34 | ||
|
d3985c2e3b | ||
|
3b1843f3a3 | ||
|
2c36796362 | ||
|
655ccba89b | ||
|
150d72f0f8 | ||
|
6a316b34a2 | ||
|
8e6b600609 | ||
|
5630a4dd67 | ||
|
38ee8aedc1 | ||
|
d594615532 | ||
|
2739fa60be | ||
|
327301782d | ||
|
8781c5db8d | ||
|
31c34ea158 | ||
|
ef9bbaca19 | ||
|
02d89072bc | ||
|
4e8bd640a7 | ||
|
8c71a00600 | ||
|
3d36c70d53 | ||
|
c72479c4d6 | ||
|
4bb88d8e44 | ||
|
0ee0958539 | ||
|
b6dd6f3a94 | ||
|
d11c322e1f | ||
|
cd92b34ef1 | ||
|
b6481cfcda | ||
|
db7eb92638 | ||
|
f2d0477550 | ||
|
776e207f09 | ||
|
733e8a0043 | ||
|
4fa19006ad | ||
|
73a597e3e5 | ||
|
d776f1765d | ||
|
741b6f6f9a | ||
|
b6f4695bcd | ||
|
b7d3b807d2 | ||
|
0cc386bc28 | ||
|
cb155707cd | ||
|
9b6b250cbd | ||
|
5b7e29b8ad | ||
|
b6d748b414 | ||
|
8fc4b338c2 | ||
|
6d3ea19ac5 | ||
|
d01ef48bf0 | ||
|
71103bb7b9 | ||
|
1c9bc00acc | ||
|
bed128e8cf | ||
|
b5c3f18f24 | ||
|
cbccdf5d93 | ||
|
a46f3a31e1 | ||
|
a808f7b04e | ||
|
3a883b9e41 | ||
|
17d8691300 | ||
|
523ce1dbdd | ||
|
4a3a9bf62c | ||
|
3b30177959 | ||
|
7e66f89260 | ||
|
1a93ba634f | ||
|
d93f823fc6 | ||
|
f2198bf938 | ||
|
965f10698b | ||
|
b71367cd2a | ||
|
abe18ac825 | ||
|
bb193d3768 | ||
|
7a7f5cd4a8 | ||
|
ac9f49f8c9 | ||
|
8f53859e00 | ||
|
bfb7ff88d9 | ||
|
c36425fd3a | ||
|
981f9d0b01 | ||
|
fa89fe3e87 | ||
|
d132357c20 | ||
|
a719237556 | ||
|
8955ca5216 | ||
|
f665762cc8 | ||
|
d16fc2b68b | ||
|
e1df32c32d | ||
|
943c6f77dc | ||
|
4964382966 | ||
|
021c6fdbe2 | ||
|
711f9805c9 | ||
|
0aaae3afd6 | ||
|
eadd1042fb | ||
|
16fa2c9f5e | ||
|
d4ab1df870 | ||
|
4a5aa1bcc1 | ||
|
01a9cda99a | ||
|
dcdf606ff6 | ||
|
ea021de5eb | ||
|
5cc3526f8f | ||
|
3060fc2af4 | ||
|
e4395dfeb4 | ||
|
f65dadf1d7 | ||
|
f048762fd9 | ||
|
bb985f826e | ||
|
bb239cba2a | ||
|
e7e66e580a | ||
|
689d689d3b | ||
|
125c3d3e0d | ||
|
ffaa06560e | ||
|
f9b716201f | ||
|
8b14a5f0d8 | ||
|
c5855119d8 | ||
|
a036597f5f | ||
|
b8688e2e66 | ||
|
d8b2e08717 | ||
|
7da78d3312 | ||
|
2292b107dc | ||
|
1fcc74c658 | ||
|
73be027951 | ||
|
132d91c2ab | ||
|
f08ce82c3f | ||
|
8de2712652 | ||
|
55835785c0 | ||
|
cb711b7758 | ||
|
19c75293bf | ||
|
33219f00d5 | ||
|
4aaedbb0e6 | ||
|
ee1f14d4eb | ||
|
feac8085c9 | ||
|
0a5ec11b1b | ||
|
03937174e5 | ||
|
e00529c05f | ||
|
5dbf8e1d2b | ||
|
bebf672186 | ||
|
6d4a8f2a5a | ||
|
14db0ae663 | ||
|
88d1c1d140 | ||
|
9106055be1 | ||
|
2cf52f15ab | ||
|
b7e9d61c72 | ||
|
0bc22db296 | ||
|
6a943acc70 | ||
|
7c97416e7d | ||
|
dd75504a66 | ||
|
04c4ab2289 | ||
|
310a2e2511 | ||
|
f6314431f0 | ||
|
89100d0ca0 | ||
|
2b646636c1 | ||
|
b10c1d5006 | ||
|
225b829c1a | ||
|
5f4c7076ab | ||
|
61c9b304d7 | ||
|
789d7000cf | ||
|
d23ef2bd59 | ||
|
8a77f832a3 | ||
|
5a6d318cfb | ||
|
e9f14de05d | ||
|
2d8da45bda | ||
|
83de33f5b8 | ||
|
e63844f786 | ||
|
0a805c16fd | ||
|
653c7d4430 | ||
|
306c3bea21 | ||
|
389ce60bc9 | ||
|
2d453a1a6c | ||
|
0775560ad2 | ||
|
2680c1e8b3 | ||
|
50ba2e3ad4 | ||
|
c16875d0de | ||
|
a4205cd0c2 | ||
|
2fb3e373c6 | ||
|
ac1fa7209c | ||
|
fd820d6af8 | ||
|
b88486601b | ||
|
92e712a508 | ||
|
64a9079ce4 | ||
|
f18d0ab923 | ||
|
def49e6d20 | ||
|
f142db3d49 | ||
|
e7b04a89e2 | ||
|
1cb59f46e3 | ||
|
a604746e0b | ||
|
aba706a293 | ||
|
c1b9113347 | ||
|
5e7db2807d | ||
|
203f830a30 | ||
|
3dbe9193e2 | ||
|
717cf29595 | ||
|
72300fec5e | ||
|
ec50b1d67a | ||
|
408a4420c9 | ||
|
568218204a | ||
|
bd39e98c66 | ||
|
01be65a624 | ||
|
a59d8a6a17 | ||
|
70b71aa4f9 | ||
|
aaf7991139 | ||
|
265d579fd9 | ||
|
f6dd91e47c | ||
|
cdae552ab0 | ||
|
730e887454 | ||
|
5a3852d82c | ||
|
5b35580d2d | ||
|
4b0705fe36 | ||
|
d219b4d12a | ||
|
6e38331328 | ||
|
c4bc9aea22 | ||
|
6c692d9308 | ||
|
7ce8bd8988 | ||
|
b4ead4076a | ||
|
052893bbf8 | ||
|
a319fe8632 | ||
|
8a84e68c13 | ||
|
0ca7defe83 | ||
|
b704706ee9 | ||
|
23fb634847 | ||
|
0bd37eb8f9 | ||
|
b86294e9cc | ||
|
f11f968f99 | ||
|
ad81642954 | ||
|
ea42103ae7 | ||
|
82435e37be | ||
|
b61d29b22a | ||
|
04392a6a4c | ||
|
6e9798d596 | ||
|
bef553c9ae | ||
|
7a76efa7a0 | ||
|
c5f64374ed | ||
|
b767caa704 | ||
|
63974f97ab | ||
|
37764a7caa | ||
|
fb586f0043 | ||
|
9990f4d8cf | ||
|
80f73cc5d0 | ||
|
8775f67416 | ||
|
2ce302f6f8 | ||
|
69b9944b8e | ||
|
37f35e8b2d | ||
|
8ad9531fa6 | ||
|
5ec37c08bf | ||
|
84cb008421 | ||
|
a744cca35a | ||
|
1950cf5f99 | ||
|
fbf230cd01 | ||
|
05abf1e419 | ||
|
f0c13980ed | ||
|
d136207d2f | ||
|
b331ee5d93 | ||
|
73cd6e981a | ||
|
6826be73c7 | ||
|
0a61a607c9 | ||
|
7444e1566b | ||
|
e401661cfc | ||
|
89d32f7109 | ||
|
5b4c88f933 | ||
|
966bce973a | ||
|
69cbcae7ed | ||
|
e26cf282d6 | ||
|
6ec7d55cbc | ||
|
9888f2a322 | ||
|
15d29b2c79 | ||
|
7e768e0a17 | ||
|
affa41b5a9 | ||
|
c8cf179ed9 | ||
|
9ef7310fc2 | ||
|
cdd7d0f4c5 | ||
|
813d706dac | ||
|
223d4133c5 | ||
|
bb62ebc23e | ||
|
a4c2adf69a | ||
|
0baff0a1e1 | ||
|
51766ad72c | ||
|
bf00b42941 | ||
|
56482acfc2 | ||
|
8898b1a608 | ||
|
525bc37649 | ||
|
9d29450f47 | ||
|
26c98bdace | ||
|
19ccd35f3f | ||
|
efa7529c5c | ||
|
334c11ccd1 | ||
|
c318a5dd79 | ||
|
25d1a8957d | ||
|
9f4853b9d6 | ||
|
ac58f50b0a | ||
|
36f22b660e | ||
|
398046eca6 | ||
|
a6f2ce4bdd | ||
|
950a8ea8df | ||
|
2b6b979f88 | ||
|
4b80e5ff47 | ||
|
a14d18c6b6 | ||
|
fe88c7ce97 | ||
|
c54b00a701 | ||
|
4a4b8c2e12 | ||
|
f1770711cb | ||
|
94757f0f0c | ||
|
15cf9be90d | ||
|
cb1955c217 | ||
|
2ce944034d | ||
|
53a207e859 | ||
|
dbdd2411c3 | ||
|
4ac3fbd726 | ||
|
8f8f62555c | ||
|
73b980c6a9 | ||
|
d2cce3cc40 | ||
|
4139360788 | ||
|
ca7bb50212 | ||
|
48ebf626b8 | ||
|
564eb802b1 | ||
|
c968949f49 | ||
|
74268ecce6 | ||
|
93f9db3af4 | ||
|
4cc0a9d4a9 | ||
|
a04dfba16b | ||
|
7f83b58b92 | ||
|
ca2e6353ae | ||
|
6bf7795529 | ||
|
401f4828b6 | ||
|
7a0ce1efe7 | ||
|
cdc2550e34 | ||
|
2d760241f3 | ||
|
3748e420a0 | ||
|
031a253101 | ||
|
cfce6d548b | ||
|
038e93ea6a | ||
|
8028e145f1 | ||
|
ddfb9fd0cc | ||
|
a36ed6ab1e | ||
|
0c7fe0664e | ||
|
7ae2f1980b | ||
|
0c89583b1b | ||
|
f5ec43276a | ||
|
e55e46011c | ||
|
77c0304faf | ||
|
ea476e26ae | ||
|
2eb47aef62 | ||
|
3e4084d99c | ||
|
955ac92043 | ||
|
5a65a07a39 | ||
|
554d73c6ee | ||
|
7bc7bc7c49 | ||
|
7b8d47cdeb | ||
|
63a8509f1f | ||
|
4e69454f72 | ||
|
ec3e237093 | ||
|
edd224a185 | ||
|
b5142b8ef5 | ||
|
ddfdf1d49d | ||
|
e7675c29da | ||
|
0bf3fef431 | ||
|
8cb0e81e89 | ||
|
d468cb051d | ||
|
0b24be7532 | ||
|
6316a0ede9 | ||
|
3c9d0e02f6 | ||
|
02d4360526 | ||
|
aedf80e382 | ||
|
51b492e1e2 | ||
|
d283f236db | ||
|
371e937a0e | ||
|
5778c25477 | ||
|
d2cd3ec879 | ||
|
ba0a59629b | ||
|
03fea7558d | ||
|
4901673c4e | ||
|
a4d84bd2e8 | ||
|
0289218c92 | ||
|
c3d32e59e7 | ||
|
8cab08351b | ||
|
5de0e58d5d | ||
|
438106f42e | ||
|
6b21f38047 | ||
|
1b07a5f3c0 | ||
|
a39ec5a061 | ||
|
015df9e6dd | ||
|
bf95284b72 | ||
|
07c04f0c2f | ||
|
b9976ee68e | ||
|
c8d9951ae4 | ||
|
97339512e1 | ||
|
95682a7a0b | ||
|
87038c4c75 | ||
|
1e5b5193fe | ||
|
9cc47678a2 | ||
|
198109cd43 | ||
|
16490541e4 | ||
|
6d5ca25e03 | ||
|
0ca44bd859 | ||
|
124b5b0549 | ||
|
fed62fae3c | ||
|
9e2812d55c | ||
|
fdcbc3904a | ||
|
cc0114cd90 | ||
|
f97753c1d0 | ||
|
fcdf545a3c | ||
|
bf28f887eb | ||
|
5f1f04e486 | ||
|
2242174749 | ||
|
e8af077fe2 | ||
|
055252f746 | ||
|
8dc84d7c17 | ||
|
f140c75fb9 | ||
|
c63dd46bac | ||
|
a23b0ba945 | ||
|
7e768aa6e9 | ||
|
18765508c0 | ||
|
efb3464ee2 | ||
|
f3319c5f75 | ||
|
384623fbb7 | ||
|
3bcccdf3dd | ||
|
d9b913fa69 | ||
|
7b9a430e8a | ||
|
4f3ab0dc69 | ||
|
427caaf9aa | ||
|
85eefdebbb | ||
|
b31b70302b | ||
|
c830ea676f | ||
|
9249059cb7 | ||
|
9fee228d1a | ||
|
20718c6ec1 | ||
|
b8754f3f44 | ||
|
6f598ae59e | ||
|
ca81fcaf37 | ||
|
75867da94f | ||
|
53c11701de | ||
|
4741793bb6 | ||
|
ab161a42ee | ||
|
4642b79b5b | ||
|
1cb43fe110 | ||
|
0123135237 | ||
|
1d77f91acc | ||
|
9b89333b1b | ||
|
829361d767 | ||
|
d3fca26499 | ||
|
ff3460e100 | ||
|
1a209a54c6 | ||
|
3bf0ac087f | ||
|
96cf6215ce | ||
|
71ce6120c3 | ||
|
21e82d0daa | ||
|
8c20bb003b | ||
|
90913d2486 | ||
|
ef0de04a0f | ||
|
8879a0ce8a | ||
|
18db46800e | ||
|
955c5b5605 | ||
|
c3b2c88213 | ||
|
7412bb35ad | ||
|
2a6fbc5c5d | ||
|
90c9b87f8d | ||
|
ffe2557e84 | ||
|
a77798e5b4 | ||
|
c1fa7bfce6 | ||
|
bf50da1e6b | ||
|
2cd4aac5ce | ||
|
208b96c092 | ||
|
36a53f8134 | ||
|
e8d8a8737e | ||
|
41b9f90a0b | ||
|
5e39493e89 | ||
|
31f3c60401 | ||
|
519c74e4dd | ||
|
4419770b15 | ||
|
b7fa86c848 | ||
|
4d620c3db9 | ||
|
319c8cb54c | ||
|
868b6f141c | ||
|
589fdd4cfe | ||
|
3f36dd9a14 | ||
|
0a6568bbab | ||
|
4e26d56746 | ||
|
d4e363fbd7 | ||
|
b721b822eb | ||
|
58d6985080 | ||
|
ee2abd415e | ||
|
09c9321159 | ||
|
0900a7cf80 | ||
|
d3ec7f65fb | ||
|
b24a94d6ce | ||
|
65769c7766 | ||
|
cbeef9fe06 | ||
|
b199209d0d | ||
|
cb48545600 | ||
|
5e626e2cc5 | ||
|
2709d1ff6e | ||
|
de3ca6e237 | ||
|
c2253e868c | ||
|
3a77c6f7eb | ||
|
8e3ff7670b | ||
|
7870147c16 | ||
|
64a371e5d8 | ||
|
f837736a20 | ||
|
dec503da81 | ||
|
1328299fda | ||
|
05190a52f1 | ||
|
46cc3105c4 | ||
|
8ba5853cdd | ||
|
e5fbcc4a8c | ||
|
e559194e8e | ||
|
c0d2994b8e | ||
|
89009aa1f8 | ||
|
ee8b580a98 | ||
|
f4656fa493 | ||
|
adf9405fd7 | ||
|
f46db7ce1a | ||
|
f00d726347 | ||
|
e8d8063ebc | ||
|
b4bc4f5ddc | ||
|
0d3ffe210f | ||
|
8eb88a161a | ||
|
346c964419 | ||
|
bc8be2460f | ||
|
a33f24d19c | ||
|
8f839fbf8e | ||
|
10dd9101cd | ||
|
aa5d3af8a1 | ||
|
dace993c21 | ||
|
32b72f0ef6 | ||
|
3dbc54c8ae | ||
|
9dd3b8fd68 | ||
|
5fb1afc681 | ||
|
a4c985a219 | ||
|
a1f819a458 | ||
|
166d7ba1cf | ||
|
a089accd23 | ||
|
38546e557f | ||
|
080dca7ee0 | ||
|
a89e433f3d | ||
|
f30ef61b06 | ||
|
03a36c1100 | ||
|
546eb6e73e | ||
|
bedd3abf8a | ||
|
ce2d4498e1 | ||
|
a786023160 | ||
|
98cf8aa445 | ||
|
d733d9fd4c | ||
|
58e9cb8b93 | ||
|
bb669acf95 | ||
|
4f3751b7ce | ||
|
84c12dee80 | ||
|
f5f865a139 | ||
|
902aed671a | ||
|
1423fe7e16 | ||
|
58ed2e6f33 | ||
|
a90939f46a | ||
|
db7456b9c5 | ||
|
a964b30c34 | ||
|
d566629d51 | ||
|
837422fbb8 | ||
|
afc37c71a6 | ||
|
e8b014ea6d | ||
|
b124512020 | ||
|
158f352328 | ||
|
f1895e32fb | ||
|
6c3be01093 | ||
|
ebe548438c | ||
|
a45c61f19e | ||
|
b07a4b95aa | ||
|
777b4e13e6 | ||
|
92cfc9324d | ||
|
97dfa18b9b | ||
|
515c07ea2b | ||
|
4d17e45f86 | ||
|
32095daf90 | ||
|
aa23ed892b | ||
|
3f079c8501 | ||
|
0aaf4bfde8 | ||
|
b967cfc775 | ||
|
2ca0483bf4 | ||
|
03cedc7b35 | ||
|
35049b5830 | ||
|
b707dde4da | ||
|
1ec8339b13 | ||
|
8bbe04a174 | ||
|
0b6a173ba0 | ||
|
8b41dfe94f | ||
|
a18b1ac99b | ||
|
e007bcb640 | ||
|
1090e7ceae | ||
|
933035dd7e | ||
|
aaaac78170 | ||
|
b5d2392def | ||
|
b88a736735 | ||
|
f238da416b | ||
|
10c7e75491 | ||
|
60af0735f4 | ||
|
273cbf333e | ||
|
189b17ad8f | ||
|
1eecf26429 | ||
|
2f9a3fa942 | ||
|
f166fb132e | ||
|
0adb69139e | ||
|
ab309e4b90 | ||
|
532c6d4fa5 | ||
|
14f627d0d3 | ||
|
947c38c124 | ||
|
f991e49203 | ||
|
4fbb6ed4ff | ||
|
2fa49303de | ||
|
f9527e9d2d | ||
|
80c63cb9d6 | ||
|
a2455eeade | ||
|
5d7ad4b3bd | ||
|
f46fa61cf3 | ||
|
da24a9637b | ||
|
2cdc11c2ad | ||
|
191cd3f25c | ||
|
f158bce8bd | ||
|
94eebb2dd6 | ||
|
375b146690 | ||
|
8c13214ff2 | ||
|
681dfadb57 | ||
|
b8b5d1cb09 | ||
|
e187dd86ed | ||
|
c380d6988c | ||
|
444acc8ef5 | ||
|
068f08aa45 | ||
|
bdc101f69c | ||
|
0ece530b9e | ||
|
5853a86091 | ||
|
20ba87bc88 | ||
|
be166ec158 | ||
|
02147891c2 | ||
|
c08cd588eb | ||
|
882bb564b1 | ||
|
18edf06a20 | ||
|
68b52b6130 | ||
|
3503f3eb4b | ||
|
a75706f329 | ||
|
29fe9d973c | ||
|
87d3320fa1 | ||
|
0b96dbc04c | ||
|
55ed58c4a0 | ||
|
8503e8c9ad | ||
|
fcdf783c40 | ||
|
008b92acb2 | ||
|
baf8ef8516 | ||
|
0cf89467e3 | ||
|
37d8a563c0 | ||
|
f3de93d73a | ||
|
4de23bd160 | ||
|
29397ca04f | ||
|
6038a36532 | ||
|
186a922d06 | ||
|
be1f9e66e3 | ||
|
9ac015e550 | ||
|
f7a9ac0ba2 | ||
|
39a8ddcfec | ||
|
3a9e28ba48 | ||
|
b6a479355e | ||
|
a791c964e8 | ||
|
9686a7f9bf | ||
|
6b65b89350 | ||
|
09a1915ec6 | ||
|
3424c421a4 | ||
|
77b89b6841 | ||
|
09807cfcad | ||
|
6ca81df40c | ||
|
2d00ddad2b | ||
|
04f4934adb | ||
|
0593007fd0 | ||
|
c07cbf9dbb | ||
|
ddd60aa1ff | ||
|
b5ecdbbc48 | ||
|
0cca613b04 | ||
|
b254ad177a | ||
|
4fd9213d2c | ||
|
982d1e219c | ||
|
3923b8631d | ||
|
72dcdf4483 | ||
|
5f3a71ed5f | ||
|
000d6ebcd0 | ||
|
7f0a04e3e3 | ||
|
005c6bb9f1 | ||
|
59c856fc0e | ||
|
5bf26619ee | ||
|
676c1d560b | ||
|
5afaa7c079 | ||
|
b0b49b72b8 | ||
|
e7600458a9 | ||
|
e0fa308984 | ||
|
6ce6fed10d | ||
|
b23ca2e138 | ||
|
2798dc3d42 | ||
|
0586d4d7a9 | ||
|
d2b3a52424 | ||
|
ffb97242d9 | ||
|
13ab400d6d | ||
|
db6d47b3f3 | ||
|
393943bb9f | ||
|
4c72ac14e6 | ||
|
7156871412 | ||
|
20665a49ed | ||
|
ad1dfc3137 | ||
|
8de67411ac | ||
|
2e75c9951b | ||
|
77f66f44a4 | ||
|
3278a397f4 | ||
|
d1c2abbaf3 | ||
|
8a443013c0 | ||
|
d3735c4763 | ||
|
f9887434d1 | ||
|
12afa005d9 | ||
|
7c0129b911 | ||
|
332f92d08e | ||
|
281ac16dca | ||
|
e8a2b9446d | ||
|
4e33f3e722 | ||
|
e46e60153d | ||
|
d3559bb1b7 | ||
|
c8a046996b | ||
|
6c85b8717f | ||
|
142a62e371 | ||
|
ff6abf08b7 | ||
|
2fd921cd60 | ||
|
1d7bd57357 | ||
|
1a4d76b4c3 | ||
|
853d869086 | ||
|
c99d669b06 | ||
|
19d3921637 | ||
|
4ee716457f | ||
|
5fb19bb187 | ||
|
8b83c5349d | ||
|
ac91942452 | ||
|
5ef23ddc1d | ||
|
2ad6c66a8d | ||
|
630f877e95 | ||
|
7dd5ce1356 | ||
|
54b18d15b6 | ||
|
2b0880af80 | ||
|
786ea17f95 | ||
|
c699bae99c | ||
|
588e8e019c | ||
|
1e7a86b7c0 | ||
|
2bb18b18b7 | ||
|
da77c549a7 | ||
|
fc707c157a | ||
|
5be6f550be | ||
|
66abf27edd | ||
|
3d1b6d7de7 | ||
|
115e604627 | ||
|
f9aeec8eb5 | ||
|
acf4d15a8d | ||
|
e5e08d1762 | ||
|
178c7817fa | ||
|
a93a428a89 | ||
|
d126a361e1 | ||
|
9e42ec0c06 | ||
|
1f05654faa | ||
|
3ddef6440a | ||
|
036b47acbc | ||
|
42e585e7a2 | ||
|
93d066bef7 | ||
|
580c6fca00 | ||
|
03300da93a | ||
|
2a1fc38799 | ||
|
646afbfa36 | ||
|
45a1435ce7 | ||
|
f4d7d32fd7 | ||
|
1b00f93091 | ||
|
9c9e5a54a7 | ||
|
4cf8c9c477 | ||
|
a78e1d7f2b | ||
|
00c1d6c42c | ||
|
5d92f09449 | ||
|
ac7d820e6f | ||
|
7d752443ca | ||
|
3311b5a6e3 | ||
|
3801443c9c | ||
|
9b2a2eb229 | ||
|
d165f66815 | ||
|
6715d52b81 | ||
|
ae160556d0 | ||
|
c040e1f9b7 | ||
|
db14a8ac1a | ||
|
9beafd03ee | ||
|
4f081c5e14 | ||
|
ff4e9e42e5 | ||
|
d5e3e6f256 | ||
|
cc9660c0e7 | ||
|
90d92707c8 | ||
|
fe77bc7c37 | ||
|
54a7d8291b | ||
|
7bb924ce5b | ||
|
9f2e25309e | ||
|
b32b39579e | ||
|
00513e66bc | ||
|
8850867b82 | ||
|
d6dfbbf8dd | ||
|
6c4797bce4 | ||
|
2c97eb4115 | ||
|
54b0828c20 | ||
|
0d241a4993 | ||
|
802b0d1a75 | ||
|
e76a7716bc | ||
|
a9cc0a61d9 | ||
|
fe50e47fc6 | ||
|
0006b286ca | ||
|
17f99da45e | ||
|
f7ce03a819 | ||
|
c073501ac0 | ||
|
d4a4747c08 | ||
|
e7b19a5f66 | ||
|
0070ec09a2 | ||
|
b3ec62a3db | ||
|
34ca4ad3b0 | ||
|
e0909731a4 | ||
|
d8d1cb2eda | ||
|
2a24fbadae | ||
|
85e1c4bd47 | ||
|
09fcb0739d | ||
|
0882c2b5c7 | ||
|
bb7763d72b | ||
|
b919d7364a | ||
|
942a37290c | ||
|
ef24c6cbbd | ||
|
cf05571f1c | ||
|
0fa038b4f2 | ||
|
10a33a2978 | ||
|
60a88381cf | ||
|
48d83c7d44 | ||
|
b967b7ed6b | ||
|
b60ecb6171 | ||
|
e8b80216e8 | ||
|
69a5472a16 | ||
|
bd6eb606ca | ||
|
e7f1e83a43 | ||
|
357fe3b793 | ||
|
ad6c06409e | ||
|
2a7feba808 | ||
|
45b7e28db2 | ||
|
daa9479352 | ||
|
0f688bd71a | ||
|
ffe0843bd6 | ||
|
0ee4039853 | ||
|
a3a6e8cdf6 | ||
|
30f60f87f4 | ||
|
0d91657557 | ||
|
ec9b80a64e | ||
|
91909f8886 | ||
|
cc335dae38 | ||
|
0d6ea03ac8 | ||
|
e1370b75e8 | ||
|
c2d0ccea3c | ||
|
9319851118 | ||
|
d45ad76fbc | ||
|
d8b9c3b832 | ||
|
9f2534af3d | ||
|
2c2978b93a | ||
|
c9b0f22a2e | ||
|
2959cd140b | ||
|
7834cfe5f0 | ||
|
2b9c5d0cff | ||
|
cb3b51dcde | ||
|
0c23fe96be | ||
|
9d0bd73ee6 | ||
|
15f096f764 | ||
|
9dd41be608 | ||
|
17f8703c71 | ||
|
5edfd7b6f7 | ||
|
4371772ec7 | ||
|
8e4e08a828 | ||
|
401e6b5a57 | ||
|
01dfe83fcd | ||
|
177c5700ff | ||
|
5d401a4fbb | ||
|
c60ab97103 | ||
|
f3d7a90ac7 | ||
|
7b114f961a | ||
|
8caaf13c84 | ||
|
0218da1ee6 | ||
|
f4786755f1 | ||
|
b9b1fcb4e5 | ||
|
5bfda557bc | ||
|
6e53df403e | ||
|
4a7246ec6e | ||
|
f53a021c08 | ||
|
8fa95d06e1 | ||
|
b34bd9ffd2 | ||
|
fd1a7e34a8 | ||
|
973653a690 | ||
|
11e4dfe940 | ||
|
e7804d39b3 | ||
|
6cfbde43ab | ||
|
30421bf247 | ||
|
ab1b6b45a2 | ||
|
59366c04dd | ||
|
fac437c1ef | ||
|
797f0d92b3 | ||
|
75f3fa5144 | ||
|
6ea1b5c7c3 | ||
|
6a81cc0e83 | ||
|
d3e2c7c51a | ||
|
1c930a2eb9 | ||
|
fb1ea5b37c | ||
|
e82f344a2c | ||
|
10ba205f97 | ||
|
fbc8b97c25 | ||
|
c6a31b21e3 | ||
|
4aa8c4b79d | ||
|
04179ae833 | ||
|
205ecde505 | ||
|
7cb467c960 | ||
|
cbe7c69154 | ||
|
c93cbb614d | ||
|
42c570a6e2 | ||
|
aed47c1178 | ||
|
a1ac4784c8 | ||
|
d8b751d56b | ||
|
213eb2ae88 | ||
|
17b7265a9a | ||
|
3afdf9571e | ||
|
bb8a59d1db | ||
|
d26830d81e | ||
|
1f2c703ddc | ||
|
dd51a6e4d1 | ||
|
caa7597097 | ||
|
409996cb29 | ||
|
a15b37d177 | ||
|
a436ef2126 | ||
|
4c53e6e89d | ||
|
083eb98949 | ||
|
418951415c | ||
|
bac0d0a175 | ||
|
7bde7ebc30 | ||
|
7b7e6555c1 | ||
|
ea8565d0ca | ||
|
1ba2902618 | ||
|
e91e8d565a | ||
|
206e613ace | ||
|
9a88dcf33e | ||
|
9fc9e8972f | ||
|
4fe6703dd4 | ||
|
d613cbb68f | ||
|
be2247b36f | ||
|
66d1f49116 | ||
|
37d35bbbdb | ||
|
321d4ccc8e | ||
|
6e4338d13c | ||
|
677d15d8a7 | ||
|
a7e978dc06 | ||
|
67f5218a73 | ||
|
c2d1329b8b | ||
|
0530ec0651 | ||
|
8a6e18badc | ||
|
1a144a7070 | ||
|
e115432e11 | ||
|
145f6e2d53 | ||
|
9c62311b56 | ||
|
a150d91c48 | ||
|
4730a4e352 | ||
|
67c57be830 | ||
|
d98e72ca25 | ||
|
d81fa1e610 | ||
|
8c12de8b73 | ||
|
9b5218f85b | ||
|
a6cf5c7667 | ||
|
4fca73ffbd | ||
|
46a9023782 | ||
|
5fed443476 | ||
|
0fab0f207c | ||
|
ee67358ee6 | ||
|
cefd44304b | ||
|
2a88cdc76d | ||
|
4c39d37ad5 | ||
|
9b9865f717 | ||
|
3f0456322d | ||
|
16ac005fd6 | ||
|
003fcf446a | ||
|
39703120a4 | ||
|
656c0efc0d | ||
|
63f7c0ed69 | ||
|
5103463524 | ||
|
97254a1e3a | ||
|
9f171a01e8 | ||
|
6c9b6007a3 | ||
|
f4a7c4bd69 | ||
|
5abad18e51 | ||
|
57f70a6d35 | ||
|
cb78f3a707 | ||
|
b331db74ee | ||
|
8fb9ad3fe7 | ||
|
d4b5c55169 | ||
|
a41438d779 | ||
|
0895d8a813 | ||
|
89310d7b7c | ||
|
ca0e936f7c | ||
|
afe767e28a | ||
|
1481b011d9 | ||
|
5f1b968b60 | ||
|
5a729043ce | ||
|
2de0dfcef7 | ||
|
7cc83ed080 | ||
|
31ec03f8e5 | ||
|
0d095c4cf1 | ||
|
dbde09d008 | ||
|
43d33e21e6 | ||
|
c8cf43a255 | ||
|
b2d05672b1 | ||
|
4123c789e6 | ||
|
c65f3c7b68 | ||
|
41e3642e02 | ||
|
eae7602f80 | ||
|
fd12c73cf9 | ||
|
2ef9d70224 | ||
|
3861531fb7 | ||
|
aeff41ff41 | ||
|
b5c8283575 | ||
|
b31cdce073 | ||
|
6040aae12a | ||
|
6ba4afc5bb | ||
|
4707e2b02a | ||
|
08dd73fd72 | ||
|
83bf67b8ca | ||
|
b758ead371 | ||
|
357a0db03e | ||
|
6a3219a5e8 | ||
|
b7e7e82f85 | ||
|
e4c66c8b2b | ||
|
4d4f8b5ee8 | ||
|
5ab75f7eb9 | ||
|
8ff22bc737 | ||
|
1d65287fbb | ||
|
dc24993bf6 | ||
|
ef606d1365 | ||
|
e26ecec545 | ||
|
28e8aa9111 | ||
|
a8ff67e3b8 | ||
|
c311b92ae2 | ||
|
b04e22cd2c | ||
|
4903c70686 | ||
|
04aa963b9a | ||
|
7fdd2e81cf | ||
|
38778c8cac | ||
|
565851b113 | ||
|
cd216b0591 | ||
|
031e2acc63 | ||
|
b3afa6b0a4 | ||
|
40fc2b78d3 | ||
|
24e6c1ea36 | ||
|
275b4e2944 | ||
|
061c93cb7d | ||
|
3b91117724 | ||
|
eb4fac28c4 | ||
|
acb5fc6393 | ||
|
961ed728a1 | ||
|
678fcfc3a3 | ||
|
79d60e6b4a | ||
|
858b2ff6da | ||
|
ba3355c74c | ||
|
6e3028cf86 | ||
|
a28598630d | ||
|
36810b2d42 | ||
|
de9923f2e1 | ||
|
716b1ac528 | ||
|
77663d64a0 | ||
|
49540c7151 | ||
|
03710bc6ba | ||
|
489ae6c9b6 | ||
|
3b54eeb1c5 | ||
|
a9bd753e38 | ||
|
bf15580081 | ||
|
5b5a299b55 | ||
|
051b529c11 | ||
|
fa5fb815fb | ||
|
a4c0a61698 | ||
|
80d8a8190d | ||
|
e69a62c41a | ||
|
46183d4d43 | ||
|
4f2d3cc250 | ||
|
79daf543b0 | ||
|
5b83fb496c | ||
|
3dfe7c27cd | ||
|
e50538c634 | ||
|
92b6ecc4dd | ||
|
588ade0ab2 | ||
|
4375aefb49 | ||
|
c5cba7775a | ||
|
7cf8ec6d61 | ||
|
46cbd3ae7b | ||
|
678cf8a341 | ||
|
3a0f1f6197 | ||
|
902ff23a95 | ||
|
7e2e9aa836 | ||
|
491ef4559f | ||
|
b7bad0f585 | ||
|
ce248bc169 | ||
|
d599be7e7c | ||
|
5b1fc0e6c0 | ||
|
aeece6c201 | ||
|
d7a1b974cd | ||
|
0db1872bd2 | ||
|
4333bc7004 | ||
|
f6caac749e | ||
|
723303e1b6 | ||
|
0daf5bae96 | ||
|
6d59fe2a34 | ||
|
3ed0f005ee | ||
|
66b89ee817 | ||
|
c596c875a6 | ||
|
08c1a29383 | ||
|
ab078d1ea0 | ||
|
b028b09041 | ||
|
44c3eb7b35 | ||
|
f54052c320 | ||
|
d23a055e3b | ||
|
8f76f3184f | ||
|
619c4853fd | ||
|
58fb6ade7c | ||
|
f2c0626c68 | ||
|
3956d2bd63 | ||
|
1d851631d6 | ||
|
517e38e33f | ||
|
a2644a4ebf | ||
|
f7bf6a8080 | ||
|
681c1d9e9a | ||
|
9ab2e69f4f | ||
|
9af2ab57d1 | ||
|
f14e4dc0be | ||
|
39bd9641f4 | ||
|
177c283011 | ||
|
62ff1421e7 | ||
|
2577cbe1ac | ||
|
7542f065a1 | ||
|
aa2abb92ef | ||
|
efc00c1610 | ||
|
7585893cf0 | ||
|
4fd9019c25 | ||
|
f4990aaa95 | ||
|
834fa77385 | ||
|
3acd20c04d | ||
|
878859d7cc | ||
|
7038776ec5 | ||
|
a74001ef6a | ||
|
0bad1794c4 | ||
|
ceec72b869 | ||
|
5c1ed0ec06 | ||
|
ce00fc502b | ||
|
ab82d2af4f | ||
|
2eb3e1aef6 | ||
|
32cb0388cf | ||
|
30f9039a2c | ||
|
8bad63f72d | ||
|
aec4d89550 | ||
|
e8639a37af | ||
|
a92ab2cbe3 | ||
|
fb0bb6e7f3 | ||
|
8dbf870122 | ||
|
50dab48f25 | ||
|
e0c6354ff3 | ||
|
5b03d251fd | ||
|
9c4b69d1ae | ||
|
feba220fff | ||
|
f9efa3bf8f | ||
|
6ebf333982 | ||
|
2c7d07d606 | ||
|
b05185e7cc | ||
|
148547de95 | ||
|
fb6a97789e | ||
|
271f34c7e3 | ||
|
9309f877d1 | ||
|
88db5fb38e | ||
|
4ee0a1b8c5 | ||
|
7617d734e2 | ||
|
a15ae9badb | ||
|
97b6fed909 | ||
|
ab653afae9 | ||
|
df158a741a | ||
|
fb1371ebfa | ||
|
82d5494235 | ||
|
6b754723d9 | ||
|
e003113132 | ||
|
dda492ff87 | ||
|
2459699d27 | ||
|
989acec027 | ||
|
701ab8b24f | ||
|
ed6bfa5fa2 | ||
|
2d2899b68a | ||
|
2d8b80bfa9 | ||
|
c1cc560458 | ||
|
95fb562533 | ||
|
17853ac237 | ||
|
e064a8bbdb | ||
|
8b9e3f1b59 | ||
|
1d38739016 | ||
|
3bb46eaa6f | ||
|
3b1561a99b | ||
|
a61e5c7310 | ||
|
ef8d1ebc0e | ||
|
348b3d3738 | ||
|
3550aa10ba | ||
|
c29eadd9ee | ||
|
aecefaee8d | ||
|
3bab5ec3a7 | ||
|
3c908963ae | ||
|
cba176cdc9 | ||
|
cfdc854409 | ||
|
1d87f78088 | ||
|
63b01376d6 | ||
|
010e518adb | ||
|
6570784b46 | ||
|
63b31f41a7 | ||
|
96eca59afc | ||
|
25978bde8e | ||
|
7691d2b5db | ||
|
edb108322e | ||
|
af3936b68e | ||
|
e76c25f9d3 | ||
|
124f455be2 | ||
|
f679eefe0a |
5
.cargo/config.toml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
[target.aarch64-unknown-linux-gnu]
|
||||||
|
linker = "aarch64-linux-gnu-gcc"
|
||||||
|
|
||||||
|
[target.armv7-unknown-linux-gnueabihf]
|
||||||
|
linker = "arm-linux-gnueabihf-gcc"
|
@ -5,3 +5,9 @@ charset = utf-8
|
|||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.rs]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_size = 4
|
||||||
|
insert_final_newline = true
|
||||||
|
65
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
name: 问题反馈 / Bug report
|
||||||
|
title: "[BUG] "
|
||||||
|
description: 反馈你遇到的问题 / Report the issue you are experiencing
|
||||||
|
labels: ["bug"]
|
||||||
|
type: "Bug"
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## 在提交问题之前,请确认以下事项:
|
||||||
|
1. 请 **确保** 您已经查阅了 [Clash Verge Rev 官方文档](https://clash-verge-rev.github.io/guide/term.html) 以及 [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
||||||
|
2. 请 **确保** [已有的问题](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue) 中没有人提交过相似issue,否则请在已有的issue下进行讨论
|
||||||
|
3. 请 **务必** 给issue填写一个简洁明了的标题,以便他人快速检索
|
||||||
|
4. 请 **务必** 查看 [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) 版本更新日志
|
||||||
|
5. 请 **务必** 尝试 [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) 版本,确定问题是否仍然存在
|
||||||
|
6. 请 **务必** 按照模板规范详细描述问题以及尝试更新 Alpha 版本,否则issue将会被直接关闭
|
||||||
|
## Before submitting the issue, please make sure of the following checklist:
|
||||||
|
1. Please make sure you have read the [Clash Verge Rev official documentation](https://clash-verge-rev.github.io/guide/term.html) and [FAQ](https://clash-verge-rev.github.io/faq/windows.html)
|
||||||
|
2. Please make sure there is no similar issue in the [existing issues](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue), otherwise please discuss under the existing issue
|
||||||
|
3. Please be sure to fill in a concise and clear title for the issue so that others can quickly search
|
||||||
|
4. Please be sure to check out [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) version update log
|
||||||
|
5. Please be sure to try the [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) version to ensure that the problem still exists
|
||||||
|
6. Please describe the problem in detail according to the template specification and try to update the Alpha version, otherwise the issue will be closed
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: 问题描述 / Describe the bug
|
||||||
|
description: 详细清晰地描述你遇到的问题,并配合截图 / Describe the problem you encountered in detail and clearly, and provide screenshots
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: 软件版本 / Verge Version
|
||||||
|
description: 请提供Verge的具体版本,如果是alpha版本,请注明下载时间(精确到小时分钟) / Please provide the specific version of Verge. If it is an alpha version, please indicate the download time (accurate to hours and minutes)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: 复现步骤 / To Reproduce
|
||||||
|
description: 请提供复现问题的步骤 / Steps to reproduce the behavior
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: 操作系统 / OS
|
||||||
|
options:
|
||||||
|
- Windows
|
||||||
|
- Linux
|
||||||
|
- MacOS
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: 操作系统版本 / OS Version
|
||||||
|
description: 请提供你的操作系统版本,Linux请额外提供桌面环境及窗口系统 / Please provide your OS version, for Linux, please also provide the desktop environment and window system
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: 日志(勿上传日志文件,请粘贴日志内容) / Log (Do not upload the log file, paste the log content directly)
|
||||||
|
description: 请提供完整或相关部分的Debug日志(请在“软件左侧菜单”->“设置”->“日志等级”调整到debug,Verge错误请把“杂项设置”->“app日志等级”调整到debug,并重启Verge生效。日志文件在“软件左侧菜单”->“设置”->“日志目录”下) / Please provide a complete or relevant part of the Debug log (please adjust the "Log level" to debug in "Software left menu" -> "Settings" -> "Log level". If there is a Verge error, please adjust "Miscellaneous settings" -> "app log level" to debug, and restart Verge to take effect. The log file is under "Software left menu" -> "Settings" -> "Log directory")
|
||||||
|
validations:
|
||||||
|
required: true
|
4
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
contact_links:
|
||||||
|
- name: 讨论交流 / Communication
|
||||||
|
url: https://t.me/clash_verge_rev
|
||||||
|
about: 在 Telegram 群组中与其他用户讨论交流 / Communicate with other users in the Telegram group
|
47
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
name: 功能请求 / Feature request
|
||||||
|
title: "[Feature] "
|
||||||
|
description: 提出你的功能请求 / Propose your feature request
|
||||||
|
labels: ["enhancement"]
|
||||||
|
type: "Feature"
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## 在提交问题之前,请确认以下事项:
|
||||||
|
1. 请 **确保** 您已经查阅了 [Clash Verge Rev 官方文档](https://clash-verge-rev.github.io/guide/term.html) 确认软件不存在类似的功能
|
||||||
|
2. 请 **确保** [已有的问题](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue) 中没有人提交过相似issue,否则请在已有的issue下进行讨论
|
||||||
|
3. 请 **务必** 给issue填写一个简洁明了的标题,以便他人快速检索
|
||||||
|
4. 请 **务必** 先下载 [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) 版本测试,确保该功能还未实现
|
||||||
|
5. 请 **务必** 按照模板规范详细描述问题,否则issue将会被关闭
|
||||||
|
## Before submitting the issue, please make sure of the following checklist:
|
||||||
|
1. Please make sure you have read the [Clash Verge Rev official documentation](https://clash-verge-rev.github.io/guide/term.html) to confirm that the software does not have similar functions
|
||||||
|
2. Please make sure there is no similar issue in the [existing issues](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue), otherwise please discuss under the existing issue
|
||||||
|
3. Please be sure to fill in a concise and clear title for the issue so that others can quickly search
|
||||||
|
4. Please be sure to download the [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) version for testing to ensure that the function has not been implemented
|
||||||
|
5. Please describe the problem in detail according to the template specification, otherwise the issue will be closed
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: 功能描述 / Feature description
|
||||||
|
description: 详细清晰地描述你的功能请求 / A clear and concise description of what the feature is
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: 使用场景 / Use case
|
||||||
|
description: 请描述你的功能请求的使用场景 / Please describe the use case of your feature request
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: checkboxes
|
||||||
|
id: os-labels
|
||||||
|
attributes:
|
||||||
|
label: 适用系统 / Target OS
|
||||||
|
description: 请选择该功能适用的操作系统(至少选择一个) / Please select the operating system(s) for this feature request (select at least one)
|
||||||
|
options:
|
||||||
|
- label: windows
|
||||||
|
- label: macos
|
||||||
|
- label: linux
|
||||||
|
validations:
|
||||||
|
required: true
|
513
.github/workflows/alpha.yml
vendored
Normal file
@ -0,0 +1,513 @@
|
|||||||
|
name: Alpha Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
# UTC+8 0,6,12,18
|
||||||
|
- cron: "0 16,22,4,10 * * *"
|
||||||
|
permissions: write-all
|
||||||
|
env:
|
||||||
|
CARGO_INCREMENTAL: 0
|
||||||
|
RUST_BACKTRACE: short
|
||||||
|
concurrency:
|
||||||
|
# only allow per workflow per commit (and not pr) to run at a time
|
||||||
|
group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}"
|
||||||
|
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check_commit:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
should_run: ${{ steps.check.outputs.should_run }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- name: Check if version changed or src changed
|
||||||
|
id: check
|
||||||
|
run: |
|
||||||
|
# For manual workflow_dispatch, always run
|
||||||
|
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||||
|
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Store current version from package.json
|
||||||
|
CURRENT_VERSION=$(cat package.json | jq -r '.version')
|
||||||
|
echo "Current version: $CURRENT_VERSION"
|
||||||
|
|
||||||
|
# Get the previous commit's package.json version
|
||||||
|
git checkout HEAD~1 package.json
|
||||||
|
PREVIOUS_VERSION=$(cat package.json | jq -r '.version')
|
||||||
|
echo "Previous version: $PREVIOUS_VERSION"
|
||||||
|
|
||||||
|
# Reset back to current commit
|
||||||
|
git checkout HEAD package.json
|
||||||
|
|
||||||
|
# Check if version changed
|
||||||
|
if [ "$CURRENT_VERSION" != "$PREVIOUS_VERSION" ]; then
|
||||||
|
echo "Version changed from $PREVIOUS_VERSION to $CURRENT_VERSION"
|
||||||
|
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if src or src-tauri directories changed
|
||||||
|
CURRENT_SRC_HASH=$(git rev-parse HEAD:src)
|
||||||
|
PREVIOUS_SRC_HASH=$(git rev-parse HEAD~1:src 2>/dev/null || echo "")
|
||||||
|
CURRENT_TAURI_HASH=$(git rev-parse HEAD:src-tauri 2>/dev/null || echo "")
|
||||||
|
PREVIOUS_TAURI_HASH=$(git rev-parse HEAD~1:src-tauri 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
echo "Current src hash: $CURRENT_SRC_HASH"
|
||||||
|
echo "Previous src hash: $PREVIOUS_SRC_HASH"
|
||||||
|
echo "Current tauri hash: $CURRENT_TAURI_HASH"
|
||||||
|
echo "Previous tauri hash: $PREVIOUS_TAURI_HASH"
|
||||||
|
|
||||||
|
if [ "$CURRENT_SRC_HASH" != "$PREVIOUS_SRC_HASH" ] || [ "$CURRENT_TAURI_HASH" != "$PREVIOUS_TAURI_HASH" ]; then
|
||||||
|
echo "Source directories changed"
|
||||||
|
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "Version and source directories unchanged"
|
||||||
|
echo "should_run=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
delete_old_assets:
|
||||||
|
needs: check_commit
|
||||||
|
if: ${{ needs.check_commit.outputs.should_run == 'true' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Delete Old Alpha Release Assets
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
script: |
|
||||||
|
const releaseTag = 'alpha';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get the release by tag name
|
||||||
|
const { data: release } = await github.rest.repos.getReleaseByTag({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
tag: releaseTag
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Found release with ID: ${release.id}`);
|
||||||
|
|
||||||
|
// Delete each asset
|
||||||
|
if (release.assets && release.assets.length > 0) {
|
||||||
|
console.log(`Deleting ${release.assets.length} assets`);
|
||||||
|
|
||||||
|
for (const asset of release.assets) {
|
||||||
|
console.log(`Deleting asset: ${asset.name} (${asset.id})`);
|
||||||
|
await github.rest.repos.deleteReleaseAsset({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
asset_id: asset.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('All assets deleted successfully');
|
||||||
|
} else {
|
||||||
|
console.log('No assets found to delete');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error.status === 404) {
|
||||||
|
console.log('Release not found, nothing to delete');
|
||||||
|
} else {
|
||||||
|
console.error('Error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update_tag:
|
||||||
|
name: Update tag
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: delete_old_assets
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Fetch Alpha update logs
|
||||||
|
id: fetch_alpha_logs
|
||||||
|
run: |
|
||||||
|
# Check if UPDATELOG.md exists
|
||||||
|
if [ -f "UPDATELOG.md" ]; then
|
||||||
|
# Extract the section starting with ## and containing -alpha until the next ## or end of file
|
||||||
|
# ALPHA_LOGS=$(awk '/^## .*-alpha/{flag=1; print; next} /^## /{flag=0} flag' UPDATELOG.md)
|
||||||
|
ALPHA_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md)
|
||||||
|
|
||||||
|
if [ -n "$ALPHA_LOGS" ]; then
|
||||||
|
echo "Found alpha update logs"
|
||||||
|
echo "ALPHA_LOGS<<EOF" >> $GITHUB_ENV
|
||||||
|
echo "$ALPHA_LOGS" >> $GITHUB_ENV
|
||||||
|
echo "EOF" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "No alpha sections found in UPDATELOG.md"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "UPDATELOG.md file not found"
|
||||||
|
fi
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Set Env
|
||||||
|
run: |
|
||||||
|
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- run: |
|
||||||
|
# 检查 ALPHA_LOGS 是否存在,如果不存在则使用默认消息
|
||||||
|
if [ -z "$ALPHA_LOGS" ]; then
|
||||||
|
echo "No alpha logs found, using default message"
|
||||||
|
ALPHA_LOGS="More new features are now supported. Check for detailed changelog soon."
|
||||||
|
else
|
||||||
|
echo "Using found alpha logs"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 生成 release.txt 文件
|
||||||
|
cat > release.txt << EOF
|
||||||
|
$ALPHA_LOGS
|
||||||
|
|
||||||
|
## 我应该下载哪个版本?
|
||||||
|
|
||||||
|
### MacOS
|
||||||
|
- MacOS intel芯片: x64.dmg
|
||||||
|
- MacOS apple M芯片: aarch64.dmg
|
||||||
|
|
||||||
|
### Linux
|
||||||
|
- Linux 64位: amd64.deb/amd64.rpm
|
||||||
|
- Linux arm64 architecture: arm64.deb/aarch64.rpm
|
||||||
|
- Linux armv7架构: armhf.deb/armhfp.rpm
|
||||||
|
|
||||||
|
### Windows (不再支持Win7)
|
||||||
|
#### 正常版本(推荐)
|
||||||
|
- 64位: x64-setup.exe
|
||||||
|
- arm64架构: arm64-setup.exe
|
||||||
|
#### 便携版问题很多不再提供
|
||||||
|
#### 内置Webview2版(体积较大,仅在企业版系统或无法安装webview2时使用)
|
||||||
|
- 64位: x64_fixed_webview2-setup.exe
|
||||||
|
- arm64架构: arm64_fixed_webview2-setup.exe
|
||||||
|
|
||||||
|
### FAQ
|
||||||
|
|
||||||
|
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
||||||
|
|
||||||
|
### 稳定机场VPN推荐
|
||||||
|
- [狗狗加速](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||||
|
|
||||||
|
Created at ${{ env.BUILDTIME }}.
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: Upload Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: alpha
|
||||||
|
name: "Clash Verge Rev Alpha"
|
||||||
|
body_path: release.txt
|
||||||
|
prerelease: true
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
generate_release_notes: true
|
||||||
|
|
||||||
|
alpha:
|
||||||
|
needs: update_tag
|
||||||
|
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
|
||||||
|
- os: ubuntu-22.04
|
||||||
|
target: x86_64-unknown-linux-gnu
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Rust Stable
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Add Rust Target
|
||||||
|
run: rustup target add ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Rust Cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: src-tauri
|
||||||
|
cache-all-crates: true
|
||||||
|
cache-on-failure: true
|
||||||
|
|
||||||
|
- name: Install dependencies (ubuntu only)
|
||||||
|
if: matrix.os == 'ubuntu-22.04'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||||
|
|
||||||
|
- name: Install Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "22"
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
name: Install pnpm
|
||||||
|
with:
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Pnpm install and check
|
||||||
|
run: |
|
||||||
|
pnpm i
|
||||||
|
pnpm check ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Release Alpha Version
|
||||||
|
run: pnpm release-alpha-version
|
||||||
|
|
||||||
|
- name: Tauri build
|
||||||
|
uses: tauri-apps/tauri-action@v0
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
|
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||||
|
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||||
|
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||||
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
|
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||||
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
|
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 }}
|
||||||
|
|
||||||
|
alpha-for-linux-arm:
|
||||||
|
needs: update_tag
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: ubuntu-22.04
|
||||||
|
target: aarch64-unknown-linux-gnu
|
||||||
|
arch: arm64
|
||||||
|
- os: ubuntu-22.04
|
||||||
|
target: armv7-unknown-linux-gnueabihf
|
||||||
|
arch: armhf
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Rust Stable
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Add Rust Target
|
||||||
|
run: rustup target add ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Rust Cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: src-tauri
|
||||||
|
cache-all-crates: true
|
||||||
|
|
||||||
|
- name: Install Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "22"
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Pnpm install and check
|
||||||
|
run: |
|
||||||
|
pnpm i
|
||||||
|
pnpm check ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Release Alpha Version
|
||||||
|
run: pnpm release-alpha-version
|
||||||
|
|
||||||
|
- name: "Setup for linux"
|
||||||
|
run: |-
|
||||||
|
sudo ls -lR /etc/apt/
|
||||||
|
|
||||||
|
cat > /tmp/sources.list << EOF
|
||||||
|
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy main multiverse universe restricted
|
||||||
|
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy-security main multiverse universe restricted
|
||||||
|
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy-updates main multiverse universe restricted
|
||||||
|
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy-backports main multiverse universe restricted
|
||||||
|
|
||||||
|
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy main multiverse universe restricted
|
||||||
|
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security main multiverse universe restricted
|
||||||
|
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates main multiverse universe restricted
|
||||||
|
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-backports main multiverse universe restricted
|
||||||
|
EOF
|
||||||
|
|
||||||
|
sudo mv /etc/apt/sources.list /etc/apt/sources.list.default
|
||||||
|
sudo mv /tmp/sources.list /etc/apt/sources.list
|
||||||
|
|
||||||
|
sudo dpkg --add-architecture ${{ matrix.arch }}
|
||||||
|
sudo apt update
|
||||||
|
|
||||||
|
sudo apt install -y \
|
||||||
|
libxslt1.1:${{ matrix.arch }} \
|
||||||
|
libwebkit2gtk-4.1-dev:${{ matrix.arch }} \
|
||||||
|
libayatana-appindicator3-dev:${{ matrix.arch }} \
|
||||||
|
libssl-dev:${{ matrix.arch }} \
|
||||||
|
patchelf:${{ matrix.arch }} \
|
||||||
|
librsvg2-dev:${{ matrix.arch }}
|
||||||
|
|
||||||
|
- name: "Install aarch64 tools"
|
||||||
|
if: matrix.target == 'aarch64-unknown-linux-gnu'
|
||||||
|
run: |
|
||||||
|
sudo apt install -y \
|
||||||
|
gcc-aarch64-linux-gnu \
|
||||||
|
g++-aarch64-linux-gnu
|
||||||
|
|
||||||
|
- name: "Install armv7 tools"
|
||||||
|
if: matrix.target == 'armv7-unknown-linux-gnueabihf'
|
||||||
|
run: |
|
||||||
|
sudo apt install -y \
|
||||||
|
gcc-arm-linux-gnueabihf \
|
||||||
|
g++-arm-linux-gnueabihf
|
||||||
|
|
||||||
|
- name: Build for Linux
|
||||||
|
run: |
|
||||||
|
export PKG_CONFIG_ALLOW_CROSS=1
|
||||||
|
if [ "${{ matrix.target }}" == "aarch64-unknown-linux-gnu" ]; then
|
||||||
|
export PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/:$PKG_CONFIG_PATH
|
||||||
|
export PKG_CONFIG_SYSROOT_DIR=/usr/aarch64-linux-gnu/
|
||||||
|
elif [ "${{ matrix.target }}" == "armv7-unknown-linux-gnueabihf" ]; then
|
||||||
|
export PKG_CONFIG_PATH=/usr/lib/arm-linux-gnueabihf/pkgconfig/:$PKG_CONFIG_PATH
|
||||||
|
export PKG_CONFIG_SYSROOT_DIR=/usr/arm-linux-gnueabihf/
|
||||||
|
fi
|
||||||
|
pnpm build --target ${{ matrix.target }}
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Get Version
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install jq
|
||||||
|
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
|
||||||
|
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Upload Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: alpha
|
||||||
|
name: "Clash Verge Rev Alpha"
|
||||||
|
prerelease: true
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
files: |
|
||||||
|
src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
|
||||||
|
src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm
|
||||||
|
|
||||||
|
alpha-for-fixed-webview2:
|
||||||
|
needs: update_tag
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: windows-latest
|
||||||
|
target: x86_64-pc-windows-msvc
|
||||||
|
arch: x64
|
||||||
|
- os: windows-latest
|
||||||
|
target: aarch64-pc-windows-msvc
|
||||||
|
arch: arm64
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Add Rust Target
|
||||||
|
run: rustup target add ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Rust Cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: src-tauri
|
||||||
|
cache-all-crates: true
|
||||||
|
cache-on-failure: true
|
||||||
|
|
||||||
|
- name: Install Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "22"
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
name: Install pnpm
|
||||||
|
with:
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Pnpm install and check
|
||||||
|
run: |
|
||||||
|
pnpm i
|
||||||
|
pnpm check ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Release Alpha Version
|
||||||
|
run: pnpm release-alpha-version
|
||||||
|
|
||||||
|
- name: Download WebView2 Runtime
|
||||||
|
run: |
|
||||||
|
invoke-webrequest -uri https://github.com/westinyang/WebView2RuntimeArchive/releases/download/109.0.1518.78/Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab -outfile Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab
|
||||||
|
Expand .\Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab -F:* ./src-tauri
|
||||||
|
Remove-Item .\src-tauri\tauri.windows.conf.json
|
||||||
|
Rename-Item .\src-tauri\webview2.${{ matrix.arch }}.json tauri.windows.conf.json
|
||||||
|
|
||||||
|
- name: Tauri build
|
||||||
|
id: build
|
||||||
|
uses: tauri-apps/tauri-action@v0
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
|
with:
|
||||||
|
tauriScript: pnpm
|
||||||
|
args: --target ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Rename
|
||||||
|
run: |
|
||||||
|
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe"
|
||||||
|
foreach ($file in $files) {
|
||||||
|
$newName = $file.Name -replace "-setup\.exe$", "_fixed_webview2-setup.exe"
|
||||||
|
Rename-Item $file.FullName $newName
|
||||||
|
}
|
||||||
|
|
||||||
|
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*.nsis.zip"
|
||||||
|
foreach ($file in $files) {
|
||||||
|
$newName = $file.Name -replace "-setup\.nsis\.zip$", "_fixed_webview2-setup.nsis.zip"
|
||||||
|
Rename-Item $file.FullName $newName
|
||||||
|
}
|
||||||
|
|
||||||
|
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe.sig"
|
||||||
|
foreach ($file in $files) {
|
||||||
|
$newName = $file.Name -replace "-setup\.exe\.sig$", "_fixed_webview2-setup.exe.sig"
|
||||||
|
Rename-Item $file.FullName $newName
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Upload Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: alpha
|
||||||
|
name: "Clash Verge Rev Alpha"
|
||||||
|
prerelease: true
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
|
||||||
|
|
||||||
|
- name: Portable Bundle
|
||||||
|
run: pnpm portable-fixed-webview2 ${{ matrix.target }} --alpha
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
94
.github/workflows/dev.yml
vendored
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
name: Development Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
permissions: write-all
|
||||||
|
env:
|
||||||
|
CARGO_INCREMENTAL: 0
|
||||||
|
RUST_BACKTRACE: short
|
||||||
|
concurrency:
|
||||||
|
# only allow per workflow per commit (and not pr) to run at a time
|
||||||
|
group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}"
|
||||||
|
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
dev:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: windows-latest
|
||||||
|
target: x86_64-pc-windows-msvc
|
||||||
|
bundle: nsis
|
||||||
|
- os: macos-latest
|
||||||
|
target: aarch64-apple-darwin
|
||||||
|
bundle: dmg
|
||||||
|
- os: macos-latest
|
||||||
|
target: x86_64-apple-darwin
|
||||||
|
bundle: dmg
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Rust Stable
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Add Rust Target
|
||||||
|
run: rustup target add ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Rust Cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: src-tauri
|
||||||
|
cache-all-crates: true
|
||||||
|
cache-on-failure: true
|
||||||
|
|
||||||
|
- name: Install Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "20"
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
name: Install pnpm
|
||||||
|
with:
|
||||||
|
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:
|
||||||
|
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
|
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||||
|
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||||
|
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||||
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
|
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||||
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
|
with:
|
||||||
|
tauriScript: pnpm
|
||||||
|
args: --target ${{ matrix.target }} -b ${{ matrix.bundle }}
|
||||||
|
|
||||||
|
- name: Upload Artifacts
|
||||||
|
if: matrix.os == 'macos-latest'
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.target }}
|
||||||
|
path: src-tauri/target/${{ matrix.target }}/release/bundle/dmg/*.dmg
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Upload Artifacts
|
||||||
|
if: matrix.os == 'windows-latest'
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.target }}
|
||||||
|
path: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*.exe
|
||||||
|
if-no-files-found: error
|
400
.github/workflows/release.yml
vendored
@ -1,55 +1,375 @@
|
|||||||
name: Release Project
|
name: Release Build
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
workflow_dispatch:
|
||||||
tags:
|
permissions: write-all
|
||||||
- v*
|
env:
|
||||||
|
CARGO_INCREMENTAL: 0
|
||||||
|
RUST_BACKTRACE: short
|
||||||
|
concurrency:
|
||||||
|
# only allow per workflow per commit (and not pr) to run at a time
|
||||||
|
group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}"
|
||||||
|
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-tauri:
|
release:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
platform: [windows-latest]
|
include:
|
||||||
runs-on: ${{ matrix.platform }}
|
- 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
|
||||||
|
- os: ubuntu-22.04
|
||||||
|
target: x86_64-unknown-linux-gnu
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- name: Checkout Repository
|
||||||
- name: setup node
|
uses: actions/checkout@v4
|
||||||
uses: actions/setup-node@v1
|
|
||||||
|
- 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:
|
with:
|
||||||
node-version: 14
|
workspaces: src-tauri
|
||||||
- name: install Rust stable
|
cache-all-crates: true
|
||||||
uses: actions-rs/toolchain@v1
|
cache-on-failure: true
|
||||||
|
|
||||||
|
- name: Install dependencies (ubuntu only)
|
||||||
|
if: matrix.os == 'ubuntu-22.04'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||||
|
|
||||||
|
- name: Install Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
toolchain: stable
|
node-version: "22"
|
||||||
- name: Get yarn cache directory path
|
|
||||||
id: yarn-cache-dir-path
|
- uses: pnpm/action-setup@v4
|
||||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
name: Install pnpm
|
||||||
- uses: actions/cache@v2
|
|
||||||
id: yarn-cache
|
|
||||||
with:
|
with:
|
||||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
run_install: false
|
||||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
|
||||||
restore-keys: |
|
- name: Pnpm install and check
|
||||||
${{ runner.os }}-yarn-
|
run: |
|
||||||
- uses: actions/cache@v2
|
pnpm i
|
||||||
with:
|
pnpm check ${{ matrix.target }}
|
||||||
path: |
|
|
||||||
~/.cargo/bin/
|
- name: Tauri build
|
||||||
~/.cargo/registry/index/
|
uses: tauri-apps/tauri-action@v0
|
||||||
~/.cargo/registry/cache/
|
|
||||||
~/.cargo/git/db/
|
|
||||||
src-tauri/target/
|
|
||||||
src-tauri/WixTools/
|
|
||||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
- name: install app dependencies and build it
|
|
||||||
run: yarn && yarn run predev
|
|
||||||
- uses: tauri-apps/tauri-action@v0
|
|
||||||
env:
|
env:
|
||||||
|
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
|
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||||
|
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||||
|
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||||
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
|
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||||
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
with:
|
with:
|
||||||
tagName: v__VERSION__
|
tagName: v__VERSION__
|
||||||
releaseName: "Clash Verge v__VERSION__"
|
releaseName: "Clash Verge Rev v__VERSION__"
|
||||||
releaseBody: "This is a release."
|
releaseBody: "More new features are now supported."
|
||||||
releaseDraft: true
|
tauriScript: pnpm
|
||||||
prerelease: false
|
args: --target ${{ matrix.target }}
|
||||||
|
|
||||||
|
release-for-linux-arm:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: ubuntu-22.04
|
||||||
|
target: aarch64-unknown-linux-gnu
|
||||||
|
arch: arm64
|
||||||
|
- os: ubuntu-22.04
|
||||||
|
target: armv7-unknown-linux-gnueabihf
|
||||||
|
arch: armhf
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Rust Stable
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Add Rust Target
|
||||||
|
run: rustup target add ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Rust Cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: src-tauri
|
||||||
|
cache-all-crates: true
|
||||||
|
|
||||||
|
- name: Install Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "22"
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Pnpm install and check
|
||||||
|
run: |
|
||||||
|
pnpm i
|
||||||
|
pnpm check ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: "Setup for linux"
|
||||||
|
run: |-
|
||||||
|
sudo ls -lR /etc/apt/
|
||||||
|
|
||||||
|
cat > /tmp/sources.list << EOF
|
||||||
|
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy main multiverse universe restricted
|
||||||
|
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy-security main multiverse universe restricted
|
||||||
|
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy-updates main multiverse universe restricted
|
||||||
|
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy-backports main multiverse universe restricted
|
||||||
|
|
||||||
|
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy main multiverse universe restricted
|
||||||
|
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security main multiverse universe restricted
|
||||||
|
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates main multiverse universe restricted
|
||||||
|
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-backports main multiverse universe restricted
|
||||||
|
EOF
|
||||||
|
|
||||||
|
sudo mv /etc/apt/sources.list /etc/apt/sources.list.default
|
||||||
|
sudo mv /tmp/sources.list /etc/apt/sources.list
|
||||||
|
|
||||||
|
sudo dpkg --add-architecture ${{ matrix.arch }}
|
||||||
|
sudo apt update
|
||||||
|
|
||||||
|
sudo apt install -y \
|
||||||
|
libxslt1.1:${{ matrix.arch }} \
|
||||||
|
libwebkit2gtk-4.1-dev:${{ matrix.arch }} \
|
||||||
|
libayatana-appindicator3-dev:${{ matrix.arch }} \
|
||||||
|
libssl-dev:${{ matrix.arch }} \
|
||||||
|
patchelf:${{ matrix.arch }} \
|
||||||
|
librsvg2-dev:${{ matrix.arch }}
|
||||||
|
|
||||||
|
- name: "Install aarch64 tools"
|
||||||
|
if: matrix.target == 'aarch64-unknown-linux-gnu'
|
||||||
|
run: |
|
||||||
|
sudo apt install -y \
|
||||||
|
gcc-aarch64-linux-gnu \
|
||||||
|
g++-aarch64-linux-gnu
|
||||||
|
|
||||||
|
- name: "Install armv7 tools"
|
||||||
|
if: matrix.target == 'armv7-unknown-linux-gnueabihf'
|
||||||
|
run: |
|
||||||
|
sudo apt install -y \
|
||||||
|
gcc-arm-linux-gnueabihf \
|
||||||
|
g++-arm-linux-gnueabihf
|
||||||
|
|
||||||
|
- name: Build for Linux
|
||||||
|
run: |
|
||||||
|
export PKG_CONFIG_ALLOW_CROSS=1
|
||||||
|
if [ "${{ matrix.target }}" == "aarch64-unknown-linux-gnu" ]; then
|
||||||
|
export PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/:$PKG_CONFIG_PATH
|
||||||
|
export PKG_CONFIG_SYSROOT_DIR=/usr/aarch64-linux-gnu/
|
||||||
|
elif [ "${{ matrix.target }}" == "armv7-unknown-linux-gnueabihf" ]; then
|
||||||
|
export PKG_CONFIG_PATH=/usr/lib/arm-linux-gnueabihf/pkgconfig/:$PKG_CONFIG_PATH
|
||||||
|
export PKG_CONFIG_SYSROOT_DIR=/usr/arm-linux-gnueabihf/
|
||||||
|
fi
|
||||||
|
pnpm build --target ${{ matrix.target }}
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Get Version
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install jq
|
||||||
|
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
|
||||||
|
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Upload Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
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
|
||||||
|
src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm
|
||||||
|
|
||||||
|
release-for-fixed-webview2:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: windows-latest
|
||||||
|
target: x86_64-pc-windows-msvc
|
||||||
|
arch: x64
|
||||||
|
- os: windows-latest
|
||||||
|
target: aarch64-pc-windows-msvc
|
||||||
|
arch: arm64
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Add Rust Target
|
||||||
|
run: rustup target add ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Rust Cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: src-tauri
|
||||||
|
cache-all-crates: true
|
||||||
|
cache-on-failure: true
|
||||||
|
|
||||||
|
- name: Install Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "22"
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
name: Install pnpm
|
||||||
|
with:
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Pnpm install and check
|
||||||
|
run: |
|
||||||
|
pnpm i
|
||||||
|
pnpm check ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Download WebView2 Runtime
|
||||||
|
run: |
|
||||||
|
invoke-webrequest -uri https://github.com/westinyang/WebView2RuntimeArchive/releases/download/109.0.1518.78/Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab -outfile Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab
|
||||||
|
Expand .\Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab -F:* ./src-tauri
|
||||||
|
Remove-Item .\src-tauri\tauri.windows.conf.json
|
||||||
|
Rename-Item .\src-tauri\webview2.${{ matrix.arch }}.json tauri.windows.conf.json
|
||||||
|
|
||||||
|
- name: Tauri build
|
||||||
|
id: build
|
||||||
|
uses: tauri-apps/tauri-action@v0
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
|
with:
|
||||||
|
tauriScript: pnpm
|
||||||
|
args: --target ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Rename
|
||||||
|
run: |
|
||||||
|
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe"
|
||||||
|
foreach ($file in $files) {
|
||||||
|
$newName = $file.Name -replace "-setup\.exe$", "_fixed_webview2-setup.exe"
|
||||||
|
Rename-Item $file.FullName $newName
|
||||||
|
}
|
||||||
|
|
||||||
|
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*.nsis.zip"
|
||||||
|
foreach ($file in $files) {
|
||||||
|
$newName = $file.Name -replace "-setup\.nsis\.zip$", "_fixed_webview2-setup.nsis.zip"
|
||||||
|
Rename-Item $file.FullName $newName
|
||||||
|
}
|
||||||
|
|
||||||
|
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe.sig"
|
||||||
|
foreach ($file in $files) {
|
||||||
|
$newName = $file.Name -replace "-setup\.exe\.sig$", "_fixed_webview2-setup.exe.sig"
|
||||||
|
Rename-Item $file.FullName $newName
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Upload Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: v${{steps.build.outputs.appVersion}}
|
||||||
|
name: "Clash Verge Rev v${{steps.build.outputs.appVersion}}"
|
||||||
|
body: "More new features are now supported."
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
|
||||||
|
|
||||||
|
- name: Portable Bundle
|
||||||
|
run: pnpm portable-fixed-webview2 ${{ matrix.target }}
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
release-update:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [release, release-for-linux-arm]
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "22"
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
name: Install pnpm
|
||||||
|
with:
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Pnpm install
|
||||||
|
run: pnpm i
|
||||||
|
|
||||||
|
- name: Release updater file
|
||||||
|
run: pnpm updater
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
release-update-for-fixed-webview2:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [release-for-fixed-webview2]
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "22"
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
name: Install pnpm
|
||||||
|
with:
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Pnpm install
|
||||||
|
run: pnpm i
|
||||||
|
|
||||||
|
- name: Release updater file
|
||||||
|
run: pnpm updater-fixed-webview2
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
submit-to-winget:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [release-update]
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- 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: Submit to Winget
|
||||||
|
uses: vedantmgoyal9/winget-releaser@main
|
||||||
|
with:
|
||||||
|
identifier: ClashVergeRev.ClashVergeRev
|
||||||
|
version: ${{env.VERSION}}
|
||||||
|
release-tag: v${{env.VERSION}}
|
||||||
|
installers-regex: '_(arm64|x64|x86)-setup\.exe$'
|
||||||
|
token: ${{ secrets.WINGET_TOKEN }}
|
||||||
|
52
.github/workflows/updater.yml
vendored
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
name: Updater CI
|
||||||
|
|
||||||
|
on: workflow_dispatch
|
||||||
|
permissions: write-all
|
||||||
|
jobs:
|
||||||
|
release-update:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "22"
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
name: Install pnpm
|
||||||
|
with:
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Pnpm install
|
||||||
|
run: pnpm i
|
||||||
|
|
||||||
|
- name: Release updater file
|
||||||
|
run: pnpm updater
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
release-update-for-fixed-webview2:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "22"
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
name: Install pnpm
|
||||||
|
with:
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Pnpm install
|
||||||
|
run: pnpm i
|
||||||
|
|
||||||
|
- name: Release updater file
|
||||||
|
run: pnpm updater-fixed-webview2
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
8
.gitignore
vendored
@ -1,7 +1,11 @@
|
|||||||
node_modules
|
node_modules
|
||||||
|
.pnpm-store
|
||||||
.DS_Store
|
.DS_Store
|
||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
package-lock.json
|
update.json
|
||||||
yarn.lock
|
scripts/_env.sh
|
||||||
|
.vscode
|
||||||
|
.tool-versions
|
||||||
|
.idea
|
||||||
|
18
.husky/pre-commit
Normal file → Executable file
@ -1,4 +1,16 @@
|
|||||||
#!/bin/sh
|
#!/bin/bash
|
||||||
. "$(dirname "$0")/_/husky.sh"
|
|
||||||
|
|
||||||
yarn pretty-quick --staged
|
#pnpm pretty-quick --staged
|
||||||
|
|
||||||
|
# 运行 clippy fmt
|
||||||
|
cargo fmt --manifest-path ./src-tauri/Cargo.toml
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "rustfmt failed to format the code. Please fix the issues and try again."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
#git add .
|
||||||
|
|
||||||
|
# 允许提交
|
||||||
|
exit 0
|
||||||
|
13
.husky/pre-push
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 运行 clippy
|
||||||
|
# cargo clippy --manifest-path ./src-tauri/Cargo.toml --fix
|
||||||
|
|
||||||
|
# 如果 clippy 失败,阻止 push
|
||||||
|
# if [ $? -ne 0 ]; then
|
||||||
|
# echo "Clippy found issues in sub_crate. Please fix them before pushing."
|
||||||
|
# exit 1
|
||||||
|
# fi
|
||||||
|
|
||||||
|
# 允许 push
|
||||||
|
exit 0
|
1
.tool-versions
Normal file
@ -0,0 +1 @@
|
|||||||
|
nodejs 21.7.1
|
106
CONTRIBUTING.md
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
When you setup `Rust` environment, Only use toolchain with `Windows MSVC` , to change settings follow command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
rustup target add x86_64-pc-windows-msvc
|
||||||
|
rustup set default-host x86_64-pc-windows-msvc
|
||||||
|
```
|
||||||
|
|
||||||
|
### Install Node.js Package
|
||||||
|
|
||||||
|
After installing Rust and Node.js, install the necessary Node.js and Node Package Manager:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
npm install pnpm -g
|
||||||
|
```
|
||||||
|
|
||||||
|
### Install Dependencies
|
||||||
|
|
||||||
|
```shell
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Download the Mihomo Core 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 [Mihomo release](https://github.com/MetaCubeX/mihomo/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
|
||||||
|
|
||||||
|
To build this project:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
|
|
||||||
|
For a faster build, use the following command
|
||||||
|
|
||||||
|
```shell
|
||||||
|
pnpm build:fast
|
||||||
|
```
|
||||||
|
|
||||||
|
This uses Rust's fast-release profile which significantly reduces compilation time by disabling optimization and LTO. The resulting binary will be larger and less performant than the standard build, but it's useful for testing changes quickly.
|
||||||
|
|
||||||
|
The `Artifacts` will display in the `log` in the Terminal.
|
||||||
|
|
||||||
|
### Build clean
|
||||||
|
|
||||||
|
To clean rust build:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
pnpm clean
|
||||||
|
```
|
||||||
|
|
||||||
|
### Portable Version (Windows Only)
|
||||||
|
|
||||||
|
To package portable version after the build:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
pnpm portable
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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!
|
106
README.md
@ -1,59 +1,91 @@
|
|||||||
<h1 align="center">
|
<h1 align="center">
|
||||||
<img src="./src/assets/image/logo.png" alt="Clash" width="128" />
|
<img src="./src-tauri/icons/icon.png" alt="Clash" width="128" />
|
||||||
<br>
|
<br>
|
||||||
Clash Verge
|
Continuation of <a href="https://github.com/zzzgydi/clash-verge">Clash Verge</a>
|
||||||
<br>
|
<br>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<h3 align="center">
|
<h3 align="center">
|
||||||
A <a href="https://github.com/Dreamacro/clash">Clash</a> GUI based on <a href="https://github.com/tauri-apps/tauri">tauri</a>.
|
A Clash Meta GUI based on <a href="https://github.com/tauri-apps/tauri">Tauri</a>.
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
|
## Preview
|
||||||
|
|
||||||
|
| Dark | Light |
|
||||||
|
| -------------------------------- | --------------------------------- |
|
||||||
|
|  |  |
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
请到发布页面下载对应的安装包:[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).
|
||||||
|
|
||||||
|
### 安装说明和常见问题,请到[文档页](https://clash-verge-rev.github.io/)查看:[Doc](https://clash-verge-rev.github.io/)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TG 频道: [@clash_verge_rev](https://t.me/clash_verge_re)
|
||||||
|
|
||||||
|
## Promotion
|
||||||
|
|
||||||
|
[狗狗加速 —— 技术流机场 Doggygo VPN](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||||
|
|
||||||
|
- 高性能海外机场,免费试用,优惠套餐,解锁流媒体,全球首家支持 Hysteria 协议。
|
||||||
|
- 使用 Clash Verge 专属邀请链接注册送 3 天,每天 1G 流量免费试用:[点此注册](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||||
|
- Clash Verge 专属 8 折优惠码: verge20 (仅有 500 份)
|
||||||
|
- 优惠套餐每月仅需 15.8 元,160G 流量,年付 8 折
|
||||||
|
- 海外团队,无跑路风险,高达 50% 返佣
|
||||||
|
- 集群负载均衡设计,高速专线(兼容老客户端),极低延迟,无视晚高峰,4K 秒开
|
||||||
|
- 全球首家 Hysteria 协议机场,现已上线更快的 `Hysteria2` 协议(Clash Verge 客户端最佳搭配)
|
||||||
|
- 解锁流媒体及 ChatGPT
|
||||||
|
- 官网:[https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
Now it's no different from the others, even fewer. (WIP)
|
- 基于性能强劲的 Rust 和 Tauri 2 框架
|
||||||
|
- 内置[Clash.Meta(mihomo)](https://github.com/MetaCubeX/mihomo)内核,并支持切换 `Alpha` 版本内核。
|
||||||
|
- 简洁美观的用户界面,支持自定义主题颜色、代理组/托盘图标以及 `CSS Injection`。
|
||||||
|
- 配置文件管理和增强(Merge 和 Script),配置文件语法提示。
|
||||||
|
- 系统代理和守卫、`TUN(虚拟网卡)` 模式。
|
||||||
|
- 可视化节点和规则编辑
|
||||||
|
- WebDav 配置备份和同步
|
||||||
|
|
||||||
|
### FAQ
|
||||||
|
|
||||||
|
Refer to [Doc FAQ Page](https://clash-verge-rev.github.io/faq/windows.html)
|
||||||
|
|
||||||
|
### Donation
|
||||||
|
|
||||||
|
[捐助Clash Verge Rev的开发](https://github.com/sponsors/clash-verge-rev)
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
You should install Rust and Nodejs. Then install tauri cli and packages.
|
See [CONTRIBUTING.md](./CONTRIBUTING.md) for more details.
|
||||||
|
|
||||||
|
To run the development server, execute the following commands after all prerequisites for **Tauri** are installed:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
cargo install tauri-cli --git https://github.com/tauri-apps/tauri
|
pnpm i
|
||||||
|
pnpm run check
|
||||||
yarn install
|
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/en/docs/api/config#tauri.bundle.externalBin).
|
|
||||||
|
|
||||||
```shell
|
|
||||||
yarn run predev
|
|
||||||
```
|
|
||||||
|
|
||||||
Then run
|
|
||||||
|
|
||||||
```shell
|
|
||||||
yarn dev
|
|
||||||
```
|
|
||||||
|
|
||||||
## Todos
|
|
||||||
|
|
||||||
> This keng is a little big...
|
|
||||||
|
|
||||||
## Screenshots
|
|
||||||
|
|
||||||
<div align="center">
|
|
||||||
<img src="./docs/demo1.png" alt="demo1" width="42%" />
|
|
||||||
<img src="./docs/demo2.png" alt="demo2" width="42%" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## Disclaimer
|
|
||||||
|
|
||||||
This is a learning project for Rust practice.
|
|
||||||
|
|
||||||
## Contributions
|
## Contributions
|
||||||
|
|
||||||
PR welcome!
|
Issue and PR welcome!
|
||||||
|
|
||||||
|
## Acknowledgement
|
||||||
|
|
||||||
|
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!
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
GPL-3.0 License
|
GPL-3.0 License. See [License here](./LICENSE) for details.
|
||||||
|
1573
UPDATELOG.md
Normal file
BIN
docs/demo1.png
Before Width: | Height: | Size: 52 KiB |
BIN
docs/demo2.png
Before Width: | Height: | Size: 73 KiB |
BIN
docs/preview_dark.png
Normal file
After Width: | Height: | Size: 314 KiB |
BIN
docs/preview_light.png
Normal file
After Width: | Height: | Size: 274 KiB |
130
package.json
@ -1,49 +1,113 @@
|
|||||||
{
|
{
|
||||||
"name": "clash-verge",
|
"name": "clash-verge",
|
||||||
"version": "0.0.1",
|
"version": "2.2.3",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0-only",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "cargo tauri dev",
|
"dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev -- --profile fast-dev",
|
||||||
"build": "cargo tauri build",
|
"dev:diff": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev -- --profile fast-dev",
|
||||||
|
"build": "cross-env NODE_OPTIONS='--max-old-space-size=4096' tauri build",
|
||||||
|
"build:fast": "cross-env NODE_OPTIONS='--max-old-space-size=4096' tauri build -- --profile fast-release",
|
||||||
|
"tauri": "tauri",
|
||||||
"web:dev": "vite",
|
"web:dev": "vite",
|
||||||
"web:build": "tsc && vite build",
|
"web:build": "tsc --noEmit && vite build",
|
||||||
"web:serve": "vite preview",
|
"web:serve": "vite preview",
|
||||||
"predev": "node scripts/pre-dev.mjs",
|
"check": "node scripts/check.mjs",
|
||||||
"prepare": "husky install"
|
"updater": "node scripts/updater.mjs",
|
||||||
|
"updater-fixed-webview2": "node scripts/updater-fixed-webview2.mjs",
|
||||||
|
"portable": "node scripts/portable.mjs",
|
||||||
|
"portable-fixed-webview2": "node scripts/portable-fixed-webview2.mjs",
|
||||||
|
"fix-alpha-version": "node scripts/fix-alpha_version.mjs",
|
||||||
|
"release-version": "node scripts/release_version.mjs",
|
||||||
|
"release-alpha-version": "node scripts/release-alpha_version.mjs",
|
||||||
|
"prepare": "husky",
|
||||||
|
"clippy": "cargo clippy --manifest-path ./src-tauri/Cargo.toml"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.7.0",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@emotion/styled": "^11.6.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@mui/icons-material": "^5.2.1",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@mui/material": "^5.2.3",
|
"@emotion/react": "^11.14.0",
|
||||||
"@tauri-apps/api": "^1.0.0-beta.8",
|
"@emotion/styled": "^11.14.0",
|
||||||
"axios": "^0.24.0",
|
"@juggle/resize-observer": "^3.4.0",
|
||||||
"dayjs": "^1.10.7",
|
"@mui/icons-material": "^6.4.8",
|
||||||
"react": "^17.0.0",
|
"@mui/lab": "6.0.0-beta.25",
|
||||||
"react-dom": "^17.0.0",
|
"@mui/material": "^6.4.8",
|
||||||
"react-router-dom": "^6.0.2",
|
"@mui/x-data-grid": "^7.28.0",
|
||||||
"react-virtuoso": "^2.3.1",
|
"@tauri-apps/api": "2.2.0",
|
||||||
"recoil": "^0.5.2",
|
"@tauri-apps/plugin-clipboard-manager": "^2.2.2",
|
||||||
"swr": "^1.1.2-beta.0"
|
"@tauri-apps/plugin-dialog": "^2.2.0",
|
||||||
|
"@tauri-apps/plugin-fs": "^2.2.0",
|
||||||
|
"@tauri-apps/plugin-global-shortcut": "^2.2.0",
|
||||||
|
"@tauri-apps/plugin-notification": "^2.2.2",
|
||||||
|
"@tauri-apps/plugin-process": "^2.2.0",
|
||||||
|
"@tauri-apps/plugin-shell": "2.2.0",
|
||||||
|
"@tauri-apps/plugin-updater": "2.3.0",
|
||||||
|
"@types/d3-shape": "^3.1.7",
|
||||||
|
"@types/json-schema": "^7.0.15",
|
||||||
|
"ahooks": "^3.8.4",
|
||||||
|
"axios": "^1.8.3",
|
||||||
|
"cli-color": "^2.0.4",
|
||||||
|
"d3-shape": "^3.2.0",
|
||||||
|
"dayjs": "1.11.13",
|
||||||
|
"foxact": "^0.2.44",
|
||||||
|
"glob": "^11.0.1",
|
||||||
|
"i18next": "^24.2.3",
|
||||||
|
"js-base64": "^3.7.7",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
|
"monaco-editor": "^0.52.2",
|
||||||
|
"monaco-yaml": "^5.3.1",
|
||||||
|
"nanoid": "^5.1.5",
|
||||||
|
"peggy": "^4.2.0",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-error-boundary": "^4.1.2",
|
||||||
|
"react-hook-form": "^7.54.2",
|
||||||
|
"react-i18next": "^13.5.0",
|
||||||
|
"react-markdown": "^9.1.0",
|
||||||
|
"react-monaco-editor": "^0.56.2",
|
||||||
|
"react-router-dom": "^6.30.0",
|
||||||
|
"react-transition-group": "^4.4.5",
|
||||||
|
"react-virtuoso": "^4.12.5",
|
||||||
|
"recharts": "^2.15.1",
|
||||||
|
"sockette": "^2.0.6",
|
||||||
|
"swr": "^2.3.3",
|
||||||
|
"tar": "^7.4.3",
|
||||||
|
"types-pac": "^1.0.3",
|
||||||
|
"zustand": "^5.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^1.0.0-beta.10",
|
"@actions/github": "^6.0.0",
|
||||||
"@types/react": "^17.0.0",
|
"@tauri-apps/cli": "2.2.7",
|
||||||
"@types/react-dom": "^17.0.0",
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@vitejs/plugin-react": "^1.1.1",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"adm-zip": "^0.5.9",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"fs-extra": "^10.0.0",
|
"@types/react": "^18.3.18",
|
||||||
"husky": "^7.0.0",
|
"@types/react-dom": "^18.3.5",
|
||||||
"node-fetch": "^3.1.0",
|
"@types/react-transition-group": "^4.4.12",
|
||||||
"pretty-quick": "^3.1.3",
|
"@vitejs/plugin-legacy": "^6.0.2",
|
||||||
"sass": "^1.44.0",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"typescript": "^4.5.2",
|
"adm-zip": "^0.5.16",
|
||||||
"vite": "^2.7.1"
|
"cross-env": "^7.0.3",
|
||||||
|
"https-proxy-agent": "^7.0.6",
|
||||||
|
"husky": "^9.1.7",
|
||||||
|
"meta-json-schema": "^1.19.3",
|
||||||
|
"node-fetch": "^3.3.2",
|
||||||
|
"prettier": "^3.5.3",
|
||||||
|
"pretty-quick": "^4.1.1",
|
||||||
|
"sass": "^1.86.0",
|
||||||
|
"terser": "^5.39.0",
|
||||||
|
"typescript": "^5.8.2",
|
||||||
|
"vite": "^6.2.2",
|
||||||
|
"vite-plugin-monaco-editor": "^1.1.0",
|
||||||
|
"vite-plugin-svgr": "^4.3.0"
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
"semi": true,
|
"semi": true,
|
||||||
"singleQuote": false,
|
"singleQuote": false,
|
||||||
"endOfLine": "lf"
|
"endOfLine": "lf"
|
||||||
}
|
},
|
||||||
|
"type": "module",
|
||||||
|
"packageManager": "pnpm@9.13.2"
|
||||||
}
|
}
|
||||||
|
6288
pnpm-lock.yaml
generated
Normal file
567
scripts/check.mjs
Normal file
@ -0,0 +1,567 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import fsp from "fs/promises";
|
||||||
|
import zlib from "zlib";
|
||||||
|
import { extract } from "tar";
|
||||||
|
import path from "path";
|
||||||
|
import AdmZip from "adm-zip";
|
||||||
|
import fetch from "node-fetch";
|
||||||
|
import { HttpsProxyAgent } from "https-proxy-agent";
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
import { log_info, log_debug, log_error, log_success } from "./utils.mjs";
|
||||||
|
import { glob } from "glob";
|
||||||
|
|
||||||
|
const cwd = process.cwd();
|
||||||
|
const TEMP_DIR = path.join(cwd, "node_modules/.verge");
|
||||||
|
const FORCE = process.argv.includes("--force");
|
||||||
|
|
||||||
|
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",
|
||||||
|
"riscv64gc-unknown-linux-gnu": "linux",
|
||||||
|
"loongarch64-unknown-linux-gnu": "linux",
|
||||||
|
};
|
||||||
|
const ARCH_MAP = {
|
||||||
|
"x86_64-pc-windows-msvc": "x64",
|
||||||
|
"i686-pc-windows-msvc": "ia32",
|
||||||
|
"aarch64-pc-windows-msvc": "arm64",
|
||||||
|
"x86_64-apple-darwin": "x64",
|
||||||
|
"aarch64-apple-darwin": "arm64",
|
||||||
|
"x86_64-unknown-linux-gnu": "x64",
|
||||||
|
"i686-unknown-linux-gnu": "ia32",
|
||||||
|
"aarch64-unknown-linux-gnu": "arm64",
|
||||||
|
"armv7-unknown-linux-gnueabihf": "arm",
|
||||||
|
"riscv64gc-unknown-linux-gnu": "riscv64",
|
||||||
|
"loongarch64-unknown-linux-gnu": "loong64",
|
||||||
|
};
|
||||||
|
|
||||||
|
const 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;
|
||||||
|
|
||||||
|
const SIDECAR_HOST = target
|
||||||
|
? target
|
||||||
|
: execSync("rustc -vV")
|
||||||
|
.toString()
|
||||||
|
.match(/(?<=host: ).+(?=\s*)/g)[0];
|
||||||
|
|
||||||
|
/* ======= 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;
|
||||||
|
|
||||||
|
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-compatible",
|
||||||
|
"darwin-arm64": "mihomo-darwin-arm64",
|
||||||
|
"linux-x64": "mihomo-linux-amd64-compatible",
|
||||||
|
"linux-ia32": "mihomo-linux-386",
|
||||||
|
"linux-arm64": "mihomo-linux-arm64",
|
||||||
|
"linux-arm": "mihomo-linux-armv7",
|
||||||
|
"linux-riscv64": "mihomo-linux-riscv64",
|
||||||
|
"linux-loong64": "mihomo-linux-loong64",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch the latest alpha release version from the version.txt file
|
||||||
|
async function getLatestAlphaVersion() {
|
||||||
|
const options = {};
|
||||||
|
|
||||||
|
const httpProxy =
|
||||||
|
process.env.HTTP_PROXY ||
|
||||||
|
process.env.http_proxy ||
|
||||||
|
process.env.HTTPS_PROXY ||
|
||||||
|
process.env.https_proxy;
|
||||||
|
|
||||||
|
if (httpProxy) {
|
||||||
|
options.agent = new HttpsProxyAgent(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
|
||||||
|
log_info(`Latest alpha version: ${META_ALPHA_VERSION}`);
|
||||||
|
} catch (error) {
|
||||||
|
log_error("Error fetching latest alpha version:", error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ======= 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;
|
||||||
|
|
||||||
|
const META_MAP = {
|
||||||
|
"win32-x64": "mihomo-windows-amd64-compatible",
|
||||||
|
"win32-ia32": "mihomo-windows-386",
|
||||||
|
"win32-arm64": "mihomo-windows-arm64",
|
||||||
|
"darwin-x64": "mihomo-darwin-amd64-compatible",
|
||||||
|
"darwin-arm64": "mihomo-darwin-arm64",
|
||||||
|
"linux-x64": "mihomo-linux-amd64-compatible",
|
||||||
|
"linux-ia32": "mihomo-linux-386",
|
||||||
|
"linux-arm64": "mihomo-linux-arm64",
|
||||||
|
"linux-arm": "mihomo-linux-armv7",
|
||||||
|
"linux-riscv64": "mihomo-linux-riscv64",
|
||||||
|
"linux-loong64": "mihomo-linux-loong64",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch the latest release version from the version.txt file
|
||||||
|
async function getLatestReleaseVersion() {
|
||||||
|
const options = {};
|
||||||
|
|
||||||
|
const httpProxy =
|
||||||
|
process.env.HTTP_PROXY ||
|
||||||
|
process.env.http_proxy ||
|
||||||
|
process.env.HTTPS_PROXY ||
|
||||||
|
process.env.https_proxy;
|
||||||
|
|
||||||
|
if (httpProxy) {
|
||||||
|
options.agent = new HttpsProxyAgent(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
|
||||||
|
log_info(`Latest release version: ${META_VERSION}`);
|
||||||
|
} catch (error) {
|
||||||
|
log_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}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* core info
|
||||||
|
*/
|
||||||
|
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}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: "verge-mihomo-alpha",
|
||||||
|
targetFile: `verge-mihomo-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: "verge-mihomo",
|
||||||
|
targetFile: `verge-mihomo-${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 fsp.mkdir(sidecarDir, { recursive: true });
|
||||||
|
if (!FORCE && fs.existsSync(sidecarPath)) return;
|
||||||
|
|
||||||
|
const tempDir = path.join(TEMP_DIR, name);
|
||||||
|
const tempZip = path.join(tempDir, zipFile);
|
||||||
|
const tempExe = path.join(tempDir, exeFile);
|
||||||
|
|
||||||
|
await fsp.mkdir(tempDir, { recursive: true });
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(tempZip)) {
|
||||||
|
await downloadFile(downloadURL, tempZip);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zipFile.endsWith(".zip")) {
|
||||||
|
const zip = new AdmZip(tempZip);
|
||||||
|
zip.getEntries().forEach((entry) => {
|
||||||
|
log_debug(`"${name}" entry name`, entry.entryName);
|
||||||
|
});
|
||||||
|
zip.extractAllTo(tempDir, true);
|
||||||
|
await fsp.rename(tempExe, sidecarPath);
|
||||||
|
log_success(`unzip finished: "${name}"`);
|
||||||
|
} else if (zipFile.endsWith(".tgz")) {
|
||||||
|
// tgz
|
||||||
|
await fsp.mkdir(tempDir, { recursive: true });
|
||||||
|
await extract({
|
||||||
|
cwd: tempDir,
|
||||||
|
file: tempZip,
|
||||||
|
//strip: 1, // 可能需要根据实际的 .tgz 文件结构调整
|
||||||
|
});
|
||||||
|
const files = await fsp.readdir(tempDir);
|
||||||
|
log_debug(`"${name}" files in tempDir:`, files);
|
||||||
|
const extractedFile = files.find((file) => file.startsWith("虚空终端-"));
|
||||||
|
if (extractedFile) {
|
||||||
|
const extractedFilePath = path.join(tempDir, extractedFile);
|
||||||
|
await fsp.rename(extractedFilePath, sidecarPath);
|
||||||
|
log_success(`"${name}" file renamed to "${sidecarPath}"`);
|
||||||
|
execSync(`chmod 755 ${sidecarPath}`);
|
||||||
|
log_success(`chmod binary finished: "${name}"`);
|
||||||
|
} 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) => {
|
||||||
|
log_error(`"${name}" gz failed:`, error.message);
|
||||||
|
reject(error);
|
||||||
|
};
|
||||||
|
readStream
|
||||||
|
.pipe(zlib.createGunzip().on("error", onError))
|
||||||
|
.pipe(writeStream)
|
||||||
|
.on("finish", () => {
|
||||||
|
execSync(`chmod 755 ${sidecarPath}`);
|
||||||
|
log_success(`chmod binary finished: "${name}"`);
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.on("error", onError);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// 需要删除文件
|
||||||
|
await fsp.rm(sidecarPath, { recursive: true, force: true });
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
// delete temp dir
|
||||||
|
await fsp.rm(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveSetDnsScript = () =>
|
||||||
|
resolveResource({
|
||||||
|
file: "set_dns.sh",
|
||||||
|
localPath: path.join(cwd, "scripts/set_dns.sh"),
|
||||||
|
});
|
||||||
|
const resolveUnSetDnsScript = () =>
|
||||||
|
resolveResource({
|
||||||
|
file: "unset_dns.sh",
|
||||||
|
localPath: path.join(cwd, "scripts/unset_dns.sh"),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* download the file to the resources dir
|
||||||
|
*/
|
||||||
|
async function resolveResource(binInfo) {
|
||||||
|
const { file, downloadURL, localPath } = binInfo;
|
||||||
|
|
||||||
|
const resDir = path.join(cwd, "src-tauri/resources");
|
||||||
|
const targetPath = path.join(resDir, file);
|
||||||
|
|
||||||
|
if (!FORCE && fs.existsSync(targetPath)) return;
|
||||||
|
|
||||||
|
if (downloadURL) {
|
||||||
|
await fsp.mkdir(resDir, { recursive: true });
|
||||||
|
await downloadFile(downloadURL, targetPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localPath) {
|
||||||
|
await fs.copyFile(localPath, targetPath, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error("Error copying file:", err);
|
||||||
|
} else {
|
||||||
|
console.log("File was copied successfully");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
log_debug(`copy file finished: "${localPath}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
log_success(`${file} finished`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* download file and save to `path`
|
||||||
|
*/ async function downloadFile(url, path) {
|
||||||
|
const options = {};
|
||||||
|
|
||||||
|
const httpProxy =
|
||||||
|
process.env.HTTP_PROXY ||
|
||||||
|
process.env.http_proxy ||
|
||||||
|
process.env.HTTPS_PROXY ||
|
||||||
|
process.env.https_proxy;
|
||||||
|
|
||||||
|
if (httpProxy) {
|
||||||
|
options.agent = new HttpsProxyAgent(httpProxy);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
...options,
|
||||||
|
method: "GET",
|
||||||
|
headers: { "Content-Type": "application/octet-stream" },
|
||||||
|
});
|
||||||
|
const buffer = await response.arrayBuffer();
|
||||||
|
await fsp.writeFile(path, new Uint8Array(buffer));
|
||||||
|
|
||||||
|
log_success(`download finished: ${url}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 fsp.mkdir(pluginDir, { recursive: true });
|
||||||
|
await fsp.mkdir(tempDir, { recursive: true });
|
||||||
|
if (!FORCE && fs.existsSync(pluginPath)) return;
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(tempZip)) {
|
||||||
|
await downloadFile(url, tempZip);
|
||||||
|
}
|
||||||
|
const zip = new AdmZip(tempZip);
|
||||||
|
zip.getEntries().forEach((entry) => {
|
||||||
|
log_debug(`"SimpleSC" entry name`, entry.entryName);
|
||||||
|
});
|
||||||
|
zip.extractAllTo(tempDir, true);
|
||||||
|
await fsp.cp(tempDll, pluginPath, { recursive: true, force: true });
|
||||||
|
log_success(`unzip finished: "SimpleSC"`);
|
||||||
|
} finally {
|
||||||
|
await fsp.rm(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// service chmod
|
||||||
|
const resolveServicePermission = async () => {
|
||||||
|
const serviceExecutables = [
|
||||||
|
"clash-verge-service*",
|
||||||
|
"install-service*",
|
||||||
|
"uninstall-service*",
|
||||||
|
];
|
||||||
|
const resDir = path.join(cwd, "src-tauri/resources");
|
||||||
|
for (let f of serviceExecutables) {
|
||||||
|
// 使用glob模块来处理通配符
|
||||||
|
const files = glob.sync(path.join(resDir, f));
|
||||||
|
for (let filePath of files) {
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
execSync(`chmod 755 ${filePath}`);
|
||||||
|
log_success(`chmod finished: "${filePath}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 在 resolveResource 函数后添加新函数
|
||||||
|
async function resolveLocales() {
|
||||||
|
const srcLocalesDir = path.join(cwd, "src/locales");
|
||||||
|
const targetLocalesDir = path.join(cwd, "src-tauri/resources/locales");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 确保目标目录存在
|
||||||
|
await fsp.mkdir(targetLocalesDir, { recursive: true });
|
||||||
|
|
||||||
|
// 读取所有语言文件
|
||||||
|
const files = await fsp.readdir(srcLocalesDir);
|
||||||
|
|
||||||
|
// 复制每个文件
|
||||||
|
for (const file of files) {
|
||||||
|
const srcPath = path.join(srcLocalesDir, file);
|
||||||
|
const targetPath = path.join(targetLocalesDir, file);
|
||||||
|
|
||||||
|
await fsp.copyFile(srcPath, targetPath);
|
||||||
|
log_success(`Copied locale file: ${file}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
log_success("All locale files copied successfully");
|
||||||
|
} catch (err) {
|
||||||
|
log_error("Error copying locale files:", err.message);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* main
|
||||||
|
*/
|
||||||
|
const SERVICE_URL = `https://github.com/clash-verge-rev/clash-verge-service/releases/download/${SIDECAR_HOST}`;
|
||||||
|
|
||||||
|
const resolveService = () => {
|
||||||
|
let ext = platform === "win32" ? ".exe" : "";
|
||||||
|
let suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
|
||||||
|
resolveResource({
|
||||||
|
file: "clash-verge-service" + suffix + ext,
|
||||||
|
downloadURL: `${SERVICE_URL}/clash-verge-service${ext}`,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveInstall = () => {
|
||||||
|
let ext = platform === "win32" ? ".exe" : "";
|
||||||
|
let suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
|
||||||
|
resolveResource({
|
||||||
|
file: "install-service" + suffix + ext,
|
||||||
|
downloadURL: `${SERVICE_URL}/install-service${ext}`,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveUninstall = () => {
|
||||||
|
let ext = platform === "win32" ? ".exe" : "";
|
||||||
|
let suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
|
||||||
|
|
||||||
|
resolveResource({
|
||||||
|
file: "uninstall-service" + suffix + ext,
|
||||||
|
downloadURL: `${SERVICE_URL}/uninstall-service${ext}`,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
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 resolveWinSysproxy = () =>
|
||||||
|
resolveResource({
|
||||||
|
file: "sysproxy.exe",
|
||||||
|
downloadURL: `https://github.com/clash-verge-rev/sysproxy/releases/download/${arch}/sysproxy.exe`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const tasks = [
|
||||||
|
// { name: "clash", func: resolveClash, retry: 5 },
|
||||||
|
{
|
||||||
|
name: "verge-mihomo-alpha",
|
||||||
|
func: () =>
|
||||||
|
getLatestAlphaVersion().then(() => resolveSidecar(clashMetaAlpha())),
|
||||||
|
retry: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "verge-mihomo",
|
||||||
|
func: () =>
|
||||||
|
getLatestReleaseVersion().then(() => resolveSidecar(clashMeta())),
|
||||||
|
retry: 5,
|
||||||
|
},
|
||||||
|
{ name: "plugin", func: resolvePlugin, retry: 5, winOnly: true },
|
||||||
|
{ name: "service", func: resolveService, retry: 5 },
|
||||||
|
{ name: "install", func: resolveInstall, retry: 5 },
|
||||||
|
{ name: "uninstall", func: resolveUninstall, retry: 5 },
|
||||||
|
{ name: "mmdb", func: resolveMmdb, retry: 5 },
|
||||||
|
{ name: "geosite", func: resolveGeosite, retry: 5 },
|
||||||
|
{ name: "geoip", func: resolveGeoIP, retry: 5 },
|
||||||
|
{
|
||||||
|
name: "enableLoopback",
|
||||||
|
func: resolveEnableLoopback,
|
||||||
|
retry: 5,
|
||||||
|
winOnly: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "service_chmod",
|
||||||
|
func: resolveServicePermission,
|
||||||
|
retry: 5,
|
||||||
|
unixOnly: platform === "linux" || platform === "darwin",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "windows-sysproxy",
|
||||||
|
func: resolveWinSysproxy,
|
||||||
|
retry: 5,
|
||||||
|
winOnly: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "set_dns_script",
|
||||||
|
func: resolveSetDnsScript,
|
||||||
|
retry: 5,
|
||||||
|
macosOnly: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unset_dns_script",
|
||||||
|
func: resolveUnSetDnsScript,
|
||||||
|
retry: 5,
|
||||||
|
macosOnly: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "locales",
|
||||||
|
func: resolveLocales,
|
||||||
|
retry: 2,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
async function runTask() {
|
||||||
|
const task = tasks.shift();
|
||||||
|
if (!task) return;
|
||||||
|
if (task.unixOnly && platform === "win32") return runTask();
|
||||||
|
if (task.winOnly && platform !== "win32") return runTask();
|
||||||
|
if (task.macosOnly && platform !== "darwin") return runTask();
|
||||||
|
if (task.linuxOnly && platform !== "linux") return runTask();
|
||||||
|
|
||||||
|
for (let i = 0; i < task.retry; i++) {
|
||||||
|
try {
|
||||||
|
await task.func();
|
||||||
|
break;
|
||||||
|
} catch (err) {
|
||||||
|
log_error(`task::${task.name} try ${i} ==`, err.message);
|
||||||
|
if (i === task.retry - 1) throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return runTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
runTask();
|
56
scripts/fix-alpha_version.mjs
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { exec } from "child_process";
|
||||||
|
import { promisify } from "util";
|
||||||
|
import fs from "fs/promises";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为Alpha版本重命名版本号
|
||||||
|
*/
|
||||||
|
const execPromise = promisify(exec);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标准输出HEAD hash
|
||||||
|
*/
|
||||||
|
async function getLatestCommitHash() {
|
||||||
|
try {
|
||||||
|
const { stdout } = await execPromise("git rev-parse HEAD");
|
||||||
|
const commitHash = stdout.trim();
|
||||||
|
// 格式化,只截取前7位字符
|
||||||
|
const formathash = commitHash.substring(0, 7);
|
||||||
|
console.log(`Found the latest commit hash code: ${commitHash}`);
|
||||||
|
return formathash;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("pnpm run fix-alpha-version ERROR", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string 传入格式化后的hash
|
||||||
|
* 将新的版本号写入文件 package.json
|
||||||
|
*/
|
||||||
|
async function updatePackageVersion(newVersion) {
|
||||||
|
// 获取内容根目录
|
||||||
|
const _dirname = process.cwd();
|
||||||
|
const packageJsonPath = path.join(_dirname, "package.json");
|
||||||
|
try {
|
||||||
|
// 读取文件
|
||||||
|
const data = await fs.readFile(packageJsonPath, "utf8");
|
||||||
|
const packageJson = JSON.parse(data);
|
||||||
|
// 获取键值替换
|
||||||
|
let result = packageJson.version.replace("alpha", newVersion);
|
||||||
|
console.log("[INFO]: Current version is: ", result);
|
||||||
|
packageJson.version = result;
|
||||||
|
// 写入版本号
|
||||||
|
await fs.writeFile(
|
||||||
|
packageJsonPath,
|
||||||
|
JSON.stringify(packageJson, null, 2),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
console.log(`[INFO]: Alpha version update to: ${newVersion}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("pnpm run fix-alpha-version ERROR", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newVersion = await getLatestCommitHash();
|
||||||
|
updatePackageVersion(newVersion).catch(console.error);
|
103
scripts/portable-fixed-webview2.mjs
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import fsp from "fs/promises";
|
||||||
|
import path from "path";
|
||||||
|
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",
|
||||||
|
"i686-pc-windows-msvc": "x86",
|
||||||
|
"aarch64-pc-windows-msvc": "arm64",
|
||||||
|
};
|
||||||
|
|
||||||
|
const PROCESS_MAP = {
|
||||||
|
x64: "x64",
|
||||||
|
ia32: "x86",
|
||||||
|
arm64: "arm64",
|
||||||
|
};
|
||||||
|
const arch = target ? ARCH_MAP[target] : PROCESS_MAP[process.arch];
|
||||||
|
/// Script for ci
|
||||||
|
/// 打包绿色版/便携版 (only Windows)
|
||||||
|
async function resolvePortable() {
|
||||||
|
if (process.platform !== "win32") return;
|
||||||
|
|
||||||
|
const releaseDir = target
|
||||||
|
? `./src-tauri/target/${target}/release`
|
||||||
|
: `./src-tauri/target/release`;
|
||||||
|
|
||||||
|
const configDir = path.join(releaseDir, ".config");
|
||||||
|
|
||||||
|
if (!fs.existsSync(releaseDir)) {
|
||||||
|
throw new Error("could not found the release dir");
|
||||||
|
}
|
||||||
|
|
||||||
|
await fsp.mkdir(configDir, { recursive: true });
|
||||||
|
if (!fs.existsSync(path.join(configDir, "PORTABLE"))) {
|
||||||
|
await fsp.writeFile(path.join(configDir, "PORTABLE"), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
const zip = new AdmZip();
|
||||||
|
|
||||||
|
zip.addLocalFile(path.join(releaseDir, "Clash Verge.exe"));
|
||||||
|
zip.addLocalFile(path.join(releaseDir, "verge-mihomo.exe"));
|
||||||
|
zip.addLocalFile(path.join(releaseDir, "verge-mihomo-alpha.exe"));
|
||||||
|
zip.addLocalFolder(path.join(releaseDir, "resources"), "resources");
|
||||||
|
zip.addLocalFolder(
|
||||||
|
path.join(
|
||||||
|
releaseDir,
|
||||||
|
`Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${arch}`,
|
||||||
|
),
|
||||||
|
`Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${arch}`,
|
||||||
|
);
|
||||||
|
zip.addLocalFolder(configDir, ".config");
|
||||||
|
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
const packageJson = require("../package.json");
|
||||||
|
const { version } = packageJson;
|
||||||
|
|
||||||
|
const zipFile = `Clash.Verge_${version}_${arch}_fixed_webview2_portable.zip`;
|
||||||
|
zip.writeZip(zipFile);
|
||||||
|
|
||||||
|
console.log("[INFO]: create portable zip successfully");
|
||||||
|
|
||||||
|
// push release assets
|
||||||
|
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 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,
|
||||||
|
});
|
||||||
|
|
||||||
|
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({
|
||||||
|
...options,
|
||||||
|
release_id: release.id,
|
||||||
|
name: zipFile,
|
||||||
|
data: zip.toBuffer(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
resolvePortable().catch(console.error);
|
52
scripts/portable.mjs
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import AdmZip from "adm-zip";
|
||||||
|
import { createRequire } from "module";
|
||||||
|
import fsp from "fs/promises";
|
||||||
|
|
||||||
|
const target = process.argv.slice(2)[0];
|
||||||
|
const ARCH_MAP = {
|
||||||
|
"x86_64-pc-windows-msvc": "x64",
|
||||||
|
"aarch64-pc-windows-msvc": "arm64",
|
||||||
|
};
|
||||||
|
|
||||||
|
const PROCESS_MAP = {
|
||||||
|
x64: "x64",
|
||||||
|
arm64: "arm64",
|
||||||
|
};
|
||||||
|
const arch = target ? ARCH_MAP[target] : PROCESS_MAP[process.arch];
|
||||||
|
/// Script for ci
|
||||||
|
/// 打包绿色版/便携版 (only Windows)
|
||||||
|
async function resolvePortable() {
|
||||||
|
if (process.platform !== "win32") return;
|
||||||
|
|
||||||
|
const releaseDir = target
|
||||||
|
? `./src-tauri/target/${target}/release`
|
||||||
|
: `./src-tauri/target/release`;
|
||||||
|
const configDir = path.join(releaseDir, ".config");
|
||||||
|
|
||||||
|
if (!fs.existsSync(releaseDir)) {
|
||||||
|
throw new Error("could not found the release dir");
|
||||||
|
}
|
||||||
|
|
||||||
|
await fsp.mkdir(configDir, { recursive: true });
|
||||||
|
if (!fs.existsSync(path.join(configDir, "PORTABLE"))) {
|
||||||
|
await fsp.writeFile(path.join(configDir, "PORTABLE"), "");
|
||||||
|
}
|
||||||
|
const zip = new AdmZip();
|
||||||
|
|
||||||
|
zip.addLocalFile(path.join(releaseDir, "clash-verge.exe"));
|
||||||
|
zip.addLocalFile(path.join(releaseDir, "verge-mihomo.exe"));
|
||||||
|
zip.addLocalFile(path.join(releaseDir, "verge-mihomo-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}_${arch}_portable.zip`;
|
||||||
|
zip.writeZip(zipFile);
|
||||||
|
console.log("[INFO]: create portable zip successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
resolvePortable().catch(console.error);
|
@ -1,104 +0,0 @@
|
|||||||
import fs from "fs-extra";
|
|
||||||
import path from "path";
|
|
||||||
import AdmZip from "adm-zip";
|
|
||||||
import fetch from "node-fetch";
|
|
||||||
import { execSync } from "child_process";
|
|
||||||
|
|
||||||
const cwd = process.cwd();
|
|
||||||
|
|
||||||
const CLASH_URL_PREFIX =
|
|
||||||
"https://github.com/Dreamacro/clash/releases/download/premium/";
|
|
||||||
const CLASH_LATEST_DATE = "2021.12.07";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get the correct clash release infomation
|
|
||||||
*/
|
|
||||||
function resolveClash() {
|
|
||||||
const { platform, arch } = process;
|
|
||||||
|
|
||||||
let name = "";
|
|
||||||
|
|
||||||
// todo
|
|
||||||
if (platform === "win32" && arch === "x64") {
|
|
||||||
name = `clash-windows-386`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!name) {
|
|
||||||
throw new Error("todo");
|
|
||||||
}
|
|
||||||
|
|
||||||
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}`;
|
|
||||||
|
|
||||||
return { url, zip, exefile, zipfile };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get the sidecar bin
|
|
||||||
*/
|
|
||||||
async function resolveSidecar() {
|
|
||||||
const sidecarDir = path.join(cwd, "src-tauri", "sidecar");
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
if (!(await fs.pathExists(sidecarDir))) await fs.mkdir(sidecarDir);
|
|
||||||
if (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);
|
|
||||||
|
|
||||||
if (!(await fs.pathExists(tempDir))) await fs.mkdir(tempDir);
|
|
||||||
if (!(await fs.pathExists(tempZip))) await downloadFile(binInfo.url, tempZip);
|
|
||||||
|
|
||||||
// Todo: support gz
|
|
||||||
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);
|
|
||||||
|
|
||||||
// delete temp dir
|
|
||||||
await fs.remove(tempDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get the Country.mmdb (not required)
|
|
||||||
*/
|
|
||||||
async function resolveMmdb() {
|
|
||||||
const url =
|
|
||||||
"https://github.com/Dreamacro/maxmind-geoip/releases/latest/download/Country.mmdb";
|
|
||||||
|
|
||||||
const resPath = path.join(cwd, "src-tauri", "resources", "Country.mmdb");
|
|
||||||
if (await fs.pathExists(resPath)) return;
|
|
||||||
await downloadFile(url, resPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* download file and save to `path`
|
|
||||||
*/
|
|
||||||
async function downloadFile(url, path) {
|
|
||||||
console.log(`[INFO]: downloading from "${url}"`);
|
|
||||||
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: "GET",
|
|
||||||
headers: { "Content-Type": "application/octet-stream" },
|
|
||||||
});
|
|
||||||
const buffer = await response.arrayBuffer();
|
|
||||||
await fs.writeFile(path, new Uint8Array(buffer));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// main
|
|
||||||
resolveSidecar();
|
|
||||||
resolveMmdb();
|
|
96
scripts/release-alpha_version.mjs
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import fs from "fs/promises";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新 package.json 文件中的版本号
|
||||||
|
*/
|
||||||
|
async function updatePackageVersion() {
|
||||||
|
const _dirname = process.cwd();
|
||||||
|
const packageJsonPath = path.join(_dirname, "package.json");
|
||||||
|
try {
|
||||||
|
const data = await fs.readFile(packageJsonPath, "utf8");
|
||||||
|
const packageJson = JSON.parse(data);
|
||||||
|
|
||||||
|
let result = packageJson.version;
|
||||||
|
if (!result.includes("alpha")) {
|
||||||
|
result = `${result}-alpha`;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[INFO]: Current package.json version is: ", result);
|
||||||
|
packageJson.version = result;
|
||||||
|
await fs.writeFile(
|
||||||
|
packageJsonPath,
|
||||||
|
JSON.stringify(packageJson, null, 2),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
console.log(`[INFO]: package.json version updated to: ${result}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating package.json version:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新 Cargo.toml 文件中的版本号
|
||||||
|
*/
|
||||||
|
async function updateCargoVersion() {
|
||||||
|
const _dirname = process.cwd();
|
||||||
|
const cargoTomlPath = path.join(_dirname, "src-tauri", "Cargo.toml");
|
||||||
|
try {
|
||||||
|
const data = await fs.readFile(cargoTomlPath, "utf8");
|
||||||
|
const lines = data.split("\n");
|
||||||
|
|
||||||
|
const updatedLines = lines.map((line) => {
|
||||||
|
if (line.startsWith("version =")) {
|
||||||
|
const versionMatch = line.match(/version\s*=\s*"([^"]+)"/);
|
||||||
|
if (versionMatch && !versionMatch[1].includes("alpha")) {
|
||||||
|
const newVersion = `${versionMatch[1]}-alpha`;
|
||||||
|
return line.replace(versionMatch[1], newVersion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return line;
|
||||||
|
});
|
||||||
|
|
||||||
|
await fs.writeFile(cargoTomlPath, updatedLines.join("\n"), "utf8");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating Cargo.toml version:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新 tauri.conf.json 文件中的版本号
|
||||||
|
*/
|
||||||
|
async function updateTauriConfigVersion() {
|
||||||
|
const _dirname = process.cwd();
|
||||||
|
const tauriConfigPath = path.join(_dirname, "src-tauri", "tauri.conf.json");
|
||||||
|
try {
|
||||||
|
const data = await fs.readFile(tauriConfigPath, "utf8");
|
||||||
|
const tauriConfig = JSON.parse(data);
|
||||||
|
|
||||||
|
let version = tauriConfig.version;
|
||||||
|
if (!version.includes("alpha")) {
|
||||||
|
version = `${version}-alpha`;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[INFO]: Current tauri.conf.json version is: ", version);
|
||||||
|
tauriConfig.version = version;
|
||||||
|
await fs.writeFile(
|
||||||
|
tauriConfigPath,
|
||||||
|
JSON.stringify(tauriConfig, null, 2),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
console.log(`[INFO]: tauri.conf.json version updated to: ${version}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating tauri.conf.json version:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主函数,依次更新所有文件的版本号
|
||||||
|
*/
|
||||||
|
async function main() {
|
||||||
|
await updatePackageVersion();
|
||||||
|
await updateCargoVersion();
|
||||||
|
await updateTauriConfigVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error);
|
197
scripts/release_version.mjs
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
import fs from "fs/promises";
|
||||||
|
import path from "path";
|
||||||
|
import { program } from "commander";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证版本号格式
|
||||||
|
* @param {string} version
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function isValidVersion(version) {
|
||||||
|
return /^v?\d+\.\d+\.\d+(-(alpha|beta|rc)(\.\d+)?)?$/i.test(version);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标准化版本号(确保v前缀可选)
|
||||||
|
* @param {string} version
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function normalizeVersion(version) {
|
||||||
|
return version.startsWith("v") ? version : `v${version}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新 package.json 文件中的版本号
|
||||||
|
* @param {string} newVersion 新版本号
|
||||||
|
*/
|
||||||
|
async function updatePackageVersion(newVersion) {
|
||||||
|
const _dirname = process.cwd();
|
||||||
|
const packageJsonPath = path.join(_dirname, "package.json");
|
||||||
|
try {
|
||||||
|
const data = await fs.readFile(packageJsonPath, "utf8");
|
||||||
|
const packageJson = JSON.parse(data);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"[INFO]: Current package.json version is: ",
|
||||||
|
packageJson.version,
|
||||||
|
);
|
||||||
|
packageJson.version = newVersion.startsWith("v")
|
||||||
|
? newVersion.slice(1)
|
||||||
|
: newVersion;
|
||||||
|
await fs.writeFile(
|
||||||
|
packageJsonPath,
|
||||||
|
JSON.stringify(packageJson, null, 2),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
`[INFO]: package.json version updated to: ${packageJson.version}`,
|
||||||
|
);
|
||||||
|
return packageJson.version;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating package.json version:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新 Cargo.toml 文件中的版本号
|
||||||
|
* @param {string} newVersion 新版本号
|
||||||
|
*/
|
||||||
|
async function updateCargoVersion(newVersion) {
|
||||||
|
const _dirname = process.cwd();
|
||||||
|
const cargoTomlPath = path.join(_dirname, "src-tauri", "Cargo.toml");
|
||||||
|
try {
|
||||||
|
const data = await fs.readFile(cargoTomlPath, "utf8");
|
||||||
|
const lines = data.split("\n");
|
||||||
|
|
||||||
|
const versionWithoutV = newVersion.startsWith("v")
|
||||||
|
? newVersion.slice(1)
|
||||||
|
: newVersion;
|
||||||
|
|
||||||
|
const updatedLines = lines.map((line) => {
|
||||||
|
if (line.trim().startsWith("version =")) {
|
||||||
|
return line.replace(
|
||||||
|
/version\s*=\s*"[^"]+"/,
|
||||||
|
`version = "${versionWithoutV}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return line;
|
||||||
|
});
|
||||||
|
|
||||||
|
await fs.writeFile(cargoTomlPath, updatedLines.join("\n"), "utf8");
|
||||||
|
console.log(`[INFO]: Cargo.toml version updated to: ${versionWithoutV}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating Cargo.toml version:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新 tauri.conf.json 文件中的版本号
|
||||||
|
* @param {string} newVersion 新版本号
|
||||||
|
*/
|
||||||
|
async function updateTauriConfigVersion(newVersion) {
|
||||||
|
const _dirname = process.cwd();
|
||||||
|
const tauriConfigPath = path.join(_dirname, "src-tauri", "tauri.conf.json");
|
||||||
|
try {
|
||||||
|
const data = await fs.readFile(tauriConfigPath, "utf8");
|
||||||
|
const tauriConfig = JSON.parse(data);
|
||||||
|
|
||||||
|
const versionWithoutV = newVersion.startsWith("v")
|
||||||
|
? newVersion.slice(1)
|
||||||
|
: newVersion;
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"[INFO]: Current tauri.conf.json version is: ",
|
||||||
|
tauriConfig.version,
|
||||||
|
);
|
||||||
|
tauriConfig.version = versionWithoutV;
|
||||||
|
await fs.writeFile(
|
||||||
|
tauriConfigPath,
|
||||||
|
JSON.stringify(tauriConfig, null, 2),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
`[INFO]: tauri.conf.json version updated to: ${versionWithoutV}`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating tauri.conf.json version:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前版本号(从package.json)
|
||||||
|
*/
|
||||||
|
async function getCurrentVersion() {
|
||||||
|
const _dirname = process.cwd();
|
||||||
|
const packageJsonPath = path.join(_dirname, "package.json");
|
||||||
|
try {
|
||||||
|
const data = await fs.readFile(packageJsonPath, "utf8");
|
||||||
|
const packageJson = JSON.parse(data);
|
||||||
|
return packageJson.version;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error getting current version:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主函数,更新所有文件的版本号
|
||||||
|
* @param {string} versionArg 版本参数(可以是标签或完整版本号)
|
||||||
|
*/
|
||||||
|
async function main(versionArg) {
|
||||||
|
if (!versionArg) {
|
||||||
|
console.error("Error: Version argument is required");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let newVersion;
|
||||||
|
const validTags = ["alpha", "beta", "rc"];
|
||||||
|
|
||||||
|
// 判断参数是标签还是完整版本号
|
||||||
|
if (validTags.includes(versionArg.toLowerCase())) {
|
||||||
|
// 标签模式:在当前版本基础上添加标签
|
||||||
|
const currentVersion = await getCurrentVersion();
|
||||||
|
const baseVersion = currentVersion.replace(
|
||||||
|
/-(alpha|beta|rc)(\.\d+)?$/i,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
newVersion = `${baseVersion}-${versionArg.toLowerCase()}`;
|
||||||
|
} else {
|
||||||
|
// 完整版本号模式
|
||||||
|
if (!isValidVersion(versionArg)) {
|
||||||
|
console.error(
|
||||||
|
"Error: Invalid version format. Expected format: vX.X.X or vX.X.X-tag (e.g. v2.2.3 or v2.2.3-alpha)",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
newVersion = normalizeVersion(versionArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[INFO]: Updating versions to: ${newVersion}`);
|
||||||
|
await updatePackageVersion(newVersion);
|
||||||
|
await updateCargoVersion(newVersion);
|
||||||
|
await updateTauriConfigVersion(newVersion);
|
||||||
|
console.log("[SUCCESS]: All version updates completed successfully!");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[ERROR]: Failed to update versions:", error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example:
|
||||||
|
// pnpm release-version 2.2.3-alpha
|
||||||
|
// 设置命令行界面
|
||||||
|
program
|
||||||
|
.name("pnpm release-version")
|
||||||
|
.description(
|
||||||
|
"Update project version numbers. Can add tag (alpha/beta/rc) or set full version (e.g. v2.2.3 or v2.2.3-alpha)",
|
||||||
|
)
|
||||||
|
.argument(
|
||||||
|
"<version>",
|
||||||
|
"version tag (alpha/beta/rc) or full version (e.g. v2.2.3 or v2.2.3-alpha)",
|
||||||
|
)
|
||||||
|
.action(main)
|
||||||
|
.parse(process.argv);
|
66
scripts/set_dns.sh
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 验证IPv4地址格式
|
||||||
|
function is_valid_ipv4() {
|
||||||
|
local ip=$1
|
||||||
|
local IFS='.'
|
||||||
|
local -a octets
|
||||||
|
|
||||||
|
[[ ! $ip =~ ^([0-9]+\.){3}[0-9]+$ ]] && return 1
|
||||||
|
read -r -a octets <<<"$ip"
|
||||||
|
[ "${#octets[@]}" -ne 4 ] && return 1
|
||||||
|
|
||||||
|
for octet in "${octets[@]}"; do
|
||||||
|
if ! [[ "$octet" =~ ^[0-9]+$ ]] || ((octet < 0 || octet > 255)); then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# 验证IPv6地址格式
|
||||||
|
function is_valid_ipv6() {
|
||||||
|
local ip=$1
|
||||||
|
if [[ ! $ip =~ ^([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4}$ ]] &&
|
||||||
|
[[ ! $ip =~ ^(([0-9a-fA-F]{0,4}:){0,7}:|(:[0-9a-fA-F]{0,4}:){0,6}:[0-9a-fA-F]{0,4})$ ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# 验证IP地址是否为有效的IPv4或IPv6
|
||||||
|
function is_valid_ip() {
|
||||||
|
is_valid_ipv4 "$1" || is_valid_ipv6 "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 检查参数
|
||||||
|
[ $# -lt 1 ] && echo "Usage: $0 <IP address>" && exit 1
|
||||||
|
! is_valid_ip "$1" && echo "$1 is not a valid IP address." && exit 1
|
||||||
|
|
||||||
|
# 获取网络接口和硬件端口
|
||||||
|
nic=$(route -n get default | grep "interface" | awk '{print $2}')
|
||||||
|
hardware_port=$(networksetup -listallhardwareports | awk -v dev="$nic" '
|
||||||
|
/Hardware Port:/{port=$0; gsub("Hardware Port: ", "", port)}
|
||||||
|
/Device: /{if ($2 == dev) {print port; exit}}
|
||||||
|
')
|
||||||
|
|
||||||
|
# 获取当前DNS设置
|
||||||
|
original_dns=$(networksetup -getdnsservers "$hardware_port")
|
||||||
|
|
||||||
|
# 检查当前DNS设置是否有效
|
||||||
|
is_valid_dns=false
|
||||||
|
for ip in $original_dns; do
|
||||||
|
ip=$(echo "$ip" | tr -d '[:space:]')
|
||||||
|
if [ -n "$ip" ] && (is_valid_ipv4 "$ip" || is_valid_ipv6 "$ip"); then
|
||||||
|
is_valid_dns=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# 更新DNS设置
|
||||||
|
if [ "$is_valid_dns" = false ]; then
|
||||||
|
echo "empty" >.original_dns.txt
|
||||||
|
else
|
||||||
|
echo "$original_dns" >.original_dns.txt
|
||||||
|
fi
|
||||||
|
networksetup -setdnsservers "$hardware_port" "$1"
|
20
scripts/unset_dns.sh
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
nic=$(route -n get default | grep "interface" | awk '{print $2}')
|
||||||
|
|
||||||
|
hardware_port=$(networksetup -listallhardwareports | awk -v dev="$nic" '
|
||||||
|
/Hardware Port:/{
|
||||||
|
port=$0; gsub("Hardware Port: ", "", port)
|
||||||
|
}
|
||||||
|
/Device: /{
|
||||||
|
if ($2 == dev) {
|
||||||
|
print port;
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
')
|
||||||
|
|
||||||
|
if [ -f .original_dns.txt ]; then
|
||||||
|
original_dns=$(cat .original_dns.txt)
|
||||||
|
networksetup -setdnsservers "$hardware_port" $original_dns
|
||||||
|
rm -rf .original_dns.txt
|
||||||
|
fi
|
84
scripts/updatelog.mjs
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import fsp from "fs/promises";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
const UPDATE_LOG = "UPDATELOG.md";
|
||||||
|
|
||||||
|
// parse the UPDATELOG.md
|
||||||
|
export async function resolveUpdateLog(tag) {
|
||||||
|
const cwd = process.cwd();
|
||||||
|
|
||||||
|
const reTitle = /^## v[\d\.]+/;
|
||||||
|
const reEnd = /^---/;
|
||||||
|
|
||||||
|
const file = path.join(cwd, UPDATE_LOG);
|
||||||
|
|
||||||
|
if (!fs.existsSync(file)) {
|
||||||
|
throw new Error("could not found UPDATELOG.md");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await fsp.readFile(file, "utf-8");
|
||||||
|
|
||||||
|
const map = {};
|
||||||
|
let p = "";
|
||||||
|
|
||||||
|
data.split("\n").forEach((line) => {
|
||||||
|
if (reTitle.test(line)) {
|
||||||
|
p = line.slice(3).trim();
|
||||||
|
if (!map[p]) {
|
||||||
|
map[p] = [];
|
||||||
|
} else {
|
||||||
|
throw new Error(`Tag ${p} dup`);
|
||||||
|
}
|
||||||
|
} else if (reEnd.test(line)) {
|
||||||
|
p = "";
|
||||||
|
} else if (p) {
|
||||||
|
map[p].push(line);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!map[tag]) {
|
||||||
|
throw new Error(`could not found "${tag}" in UPDATELOG.md`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return map[tag].join("\n").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolveUpdateLogDefault() {
|
||||||
|
const cwd = process.cwd();
|
||||||
|
const file = path.join(cwd, UPDATE_LOG);
|
||||||
|
|
||||||
|
if (!fs.existsSync(file)) {
|
||||||
|
throw new Error("could not found UPDATELOG.md");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await fsp.readFile(file, "utf-8");
|
||||||
|
|
||||||
|
const reTitle = /^## v[\d\.]+/;
|
||||||
|
const reEnd = /^---/;
|
||||||
|
|
||||||
|
let isCapturing = false;
|
||||||
|
let content = [];
|
||||||
|
let firstTag = "";
|
||||||
|
|
||||||
|
for (const line of data.split("\n")) {
|
||||||
|
if (reTitle.test(line) && !isCapturing) {
|
||||||
|
isCapturing = true;
|
||||||
|
firstTag = line.slice(3).trim();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCapturing) {
|
||||||
|
if (reEnd.test(line)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
content.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!firstTag) {
|
||||||
|
throw new Error("could not found any version tag in UPDATELOG.md");
|
||||||
|
}
|
||||||
|
|
||||||
|
return content.join("\n").trim();
|
||||||
|
}
|
157
scripts/updater-fixed-webview2.mjs
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
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-fixed-webview2.json";
|
||||||
|
const UPDATE_JSON_PROXY = "update-fixed-webview2-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: {
|
||||||
|
"windows-x86_64": { signature: "", url: "" },
|
||||||
|
"windows-aarch64": { signature: "", url: "" },
|
||||||
|
"windows-x86": { signature: "", url: "" },
|
||||||
|
"windows-i686": { signature: "", url: "" },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const promises = latestRelease.assets.map(async (asset) => {
|
||||||
|
const { name, browser_download_url } = asset;
|
||||||
|
|
||||||
|
// win64 url
|
||||||
|
if (name.endsWith("x64_fixed_webview2-setup.nsis.zip")) {
|
||||||
|
updateData.platforms["windows-x86_64"].url = browser_download_url;
|
||||||
|
}
|
||||||
|
// win64 signature
|
||||||
|
if (name.endsWith("x64_fixed_webview2-setup.nsis.zip.sig")) {
|
||||||
|
const sig = await getSignature(browser_download_url);
|
||||||
|
updateData.platforms["windows-x86_64"].signature = sig;
|
||||||
|
}
|
||||||
|
|
||||||
|
// win32 url
|
||||||
|
if (name.endsWith("x86_fixed_webview2-setup.nsis.zip")) {
|
||||||
|
updateData.platforms["windows-x86"].url = browser_download_url;
|
||||||
|
updateData.platforms["windows-i686"].url = browser_download_url;
|
||||||
|
}
|
||||||
|
// win32 signature
|
||||||
|
if (name.endsWith("x86_fixed_webview2-setup.nsis.zip.sig")) {
|
||||||
|
const sig = await getSignature(browser_download_url);
|
||||||
|
updateData.platforms["windows-x86"].signature = sig;
|
||||||
|
updateData.platforms["windows-i686"].signature = sig;
|
||||||
|
}
|
||||||
|
|
||||||
|
// win arm url
|
||||||
|
if (name.endsWith("arm64_fixed_webview2-setup.nsis.zip")) {
|
||||||
|
updateData.platforms["windows-aarch64"].url = browser_download_url;
|
||||||
|
}
|
||||||
|
// win arm signature
|
||||||
|
if (name.endsWith("arm64_fixed_webview2-setup.nsis.zip.sig")) {
|
||||||
|
const sig = await getSignature(browser_download_url);
|
||||||
|
updateData.platforms["windows-aarch64"].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://download.clashverge.dev/" + 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);
|
323
scripts/updater.mjs
Normal file
@ -0,0 +1,323 @@
|
|||||||
|
import fetch from "node-fetch";
|
||||||
|
import { getOctokit, context } from "@actions/github";
|
||||||
|
import { resolveUpdateLog, resolveUpdateLogDefault } from "./updatelog.mjs";
|
||||||
|
|
||||||
|
// Add stable update JSON filenames
|
||||||
|
const UPDATE_TAG_NAME = "updater";
|
||||||
|
const UPDATE_JSON_FILE = "update.json";
|
||||||
|
const UPDATE_JSON_PROXY = "update-proxy.json";
|
||||||
|
// Add alpha update JSON filenames
|
||||||
|
const ALPHA_TAG_NAME = "updater-alpha";
|
||||||
|
const ALPHA_UPDATE_JSON_FILE = "update.json";
|
||||||
|
const ALPHA_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);
|
||||||
|
|
||||||
|
// Fetch all tags using pagination
|
||||||
|
let allTags = [];
|
||||||
|
let page = 1;
|
||||||
|
const perPage = 100;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { data: pageTags } = await github.rest.repos.listTags({
|
||||||
|
...options,
|
||||||
|
per_page: perPage,
|
||||||
|
page: page,
|
||||||
|
});
|
||||||
|
|
||||||
|
allTags = allTags.concat(pageTags);
|
||||||
|
|
||||||
|
// Break if we received fewer tags than requested (last page)
|
||||||
|
if (pageTags.length < perPage) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
page++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tags = allTags;
|
||||||
|
console.log(`Retrieved ${tags.length} tags in total`);
|
||||||
|
|
||||||
|
// More flexible tag detection with regex patterns
|
||||||
|
const stableTagRegex = /^v\d+\.\d+\.\d+$/; // Matches vX.Y.Z format
|
||||||
|
// const preReleaseRegex = /^v\d+\.\d+\.\d+-(alpha|beta|rc|pre)/i; // Matches vX.Y.Z-alpha/beta/rc format
|
||||||
|
const preReleaseRegex = /^(alpha|beta|rc|pre)$/i; // Matches exact alpha/beta/rc/pre tags
|
||||||
|
|
||||||
|
// Get the latest stable tag and pre-release tag
|
||||||
|
const stableTag = tags.find((t) => stableTagRegex.test(t.name));
|
||||||
|
const preReleaseTag = tags.find((t) => preReleaseRegex.test(t.name));
|
||||||
|
|
||||||
|
console.log("All tags:", tags.map((t) => t.name).join(", "));
|
||||||
|
console.log("Stable tag:", stableTag ? stableTag.name : "None found");
|
||||||
|
console.log(
|
||||||
|
"Pre-release tag:",
|
||||||
|
preReleaseTag ? preReleaseTag.name : "None found",
|
||||||
|
);
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
// Process stable release
|
||||||
|
if (stableTag) {
|
||||||
|
await processRelease(github, options, stableTag, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process pre-release if found
|
||||||
|
if (preReleaseTag) {
|
||||||
|
await processRelease(github, options, preReleaseTag, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process a release (stable or alpha) and generate update files
|
||||||
|
async function processRelease(github, options, tag, isAlpha) {
|
||||||
|
if (!tag) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data: release } = await github.rest.repos.getReleaseByTag({
|
||||||
|
...options,
|
||||||
|
tag: tag.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateData = {
|
||||||
|
name: tag.name,
|
||||||
|
notes: await resolveUpdateLog(tag.name).catch(() =>
|
||||||
|
resolveUpdateLogDefault().catch(() => "No changelog available"),
|
||||||
|
),
|
||||||
|
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-x86": { signature: "", url: "" },
|
||||||
|
"linux-i686": { signature: "", url: "" },
|
||||||
|
"linux-aarch64": { signature: "", url: "" },
|
||||||
|
"linux-armv7": { signature: "", url: "" },
|
||||||
|
"windows-x86_64": { signature: "", url: "" },
|
||||||
|
"windows-aarch64": { signature: "", url: "" },
|
||||||
|
"windows-x86": { signature: "", url: "" },
|
||||||
|
"windows-i686": { signature: "", url: "" },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const promises = release.assets.map(async (asset) => {
|
||||||
|
const { name, browser_download_url } = asset;
|
||||||
|
|
||||||
|
// Process all the platform URL and signature data
|
||||||
|
// win64 url
|
||||||
|
if (name.endsWith("x64-setup.exe")) {
|
||||||
|
updateData.platforms.win64.url = browser_download_url;
|
||||||
|
updateData.platforms["windows-x86_64"].url = browser_download_url;
|
||||||
|
}
|
||||||
|
// win64 signature
|
||||||
|
if (name.endsWith("x64-setup.exe.sig")) {
|
||||||
|
const sig = await getSignature(browser_download_url);
|
||||||
|
updateData.platforms.win64.signature = sig;
|
||||||
|
updateData.platforms["windows-x86_64"].signature = sig;
|
||||||
|
}
|
||||||
|
|
||||||
|
// win32 url
|
||||||
|
if (name.endsWith("x86-setup.exe")) {
|
||||||
|
updateData.platforms["windows-x86"].url = browser_download_url;
|
||||||
|
updateData.platforms["windows-i686"].url = browser_download_url;
|
||||||
|
}
|
||||||
|
// win32 signature
|
||||||
|
if (name.endsWith("x86-setup.exe.sig")) {
|
||||||
|
const sig = await getSignature(browser_download_url);
|
||||||
|
updateData.platforms["windows-x86"].signature = sig;
|
||||||
|
updateData.platforms["windows-i686"].signature = sig;
|
||||||
|
}
|
||||||
|
|
||||||
|
// win arm url
|
||||||
|
if (name.endsWith("arm64-setup.exe")) {
|
||||||
|
updateData.platforms["windows-aarch64"].url = browser_download_url;
|
||||||
|
}
|
||||||
|
// win arm signature
|
||||||
|
if (name.endsWith("arm64-setup.exe.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;
|
||||||
|
// 使linux可以检查更新
|
||||||
|
updateData.platforms.linux.url = browser_download_url;
|
||||||
|
updateData.platforms["linux-x86_64"].url = browser_download_url;
|
||||||
|
updateData.platforms["linux-x86"].url = browser_download_url;
|
||||||
|
updateData.platforms["linux-i686"].url = browser_download_url;
|
||||||
|
updateData.platforms["linux-aarch64"].url = browser_download_url;
|
||||||
|
updateData.platforms["linux-armv7"].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;
|
||||||
|
updateData.platforms.linux.signature = sig;
|
||||||
|
updateData.platforms["linux-x86_64"].signature = sig;
|
||||||
|
updateData.platforms["linux-x86"].url = browser_download_url;
|
||||||
|
updateData.platforms["linux-i686"].url = browser_download_url;
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generate a proxy update file for accelerated GitHub resources
|
||||||
|
const updateDataNew = JSON.parse(JSON.stringify(updateData));
|
||||||
|
|
||||||
|
Object.entries(updateDataNew.platforms).forEach(([key, value]) => {
|
||||||
|
if (value.url) {
|
||||||
|
updateDataNew.platforms[key].url =
|
||||||
|
"https://download.clashverge.dev/" + value.url;
|
||||||
|
} else {
|
||||||
|
console.log(`[Error]: updateDataNew.platforms.${key} is null`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get the appropriate updater release based on isAlpha flag
|
||||||
|
const releaseTag = isAlpha ? ALPHA_TAG_NAME : UPDATE_TAG_NAME;
|
||||||
|
console.log(
|
||||||
|
`Processing ${isAlpha ? "alpha" : "stable"} release:`,
|
||||||
|
releaseTag,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
let updateRelease;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try to get the existing release
|
||||||
|
const response = await github.rest.repos.getReleaseByTag({
|
||||||
|
...options,
|
||||||
|
tag: releaseTag,
|
||||||
|
});
|
||||||
|
updateRelease = response.data;
|
||||||
|
console.log(
|
||||||
|
`Found existing ${releaseTag} release with ID: ${updateRelease.id}`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
// If release doesn't exist, create it
|
||||||
|
if (error.status === 404) {
|
||||||
|
console.log(
|
||||||
|
`Release with tag ${releaseTag} not found, creating new release...`,
|
||||||
|
);
|
||||||
|
const createResponse = await github.rest.repos.createRelease({
|
||||||
|
...options,
|
||||||
|
tag_name: releaseTag,
|
||||||
|
name: isAlpha
|
||||||
|
? "Auto-update Alpha Channel"
|
||||||
|
: "Auto-update Stable Channel",
|
||||||
|
body: `This release contains the update information for ${isAlpha ? "alpha" : "stable"} channel.`,
|
||||||
|
prerelease: isAlpha,
|
||||||
|
});
|
||||||
|
updateRelease = createResponse.data;
|
||||||
|
console.log(
|
||||||
|
`Created new ${releaseTag} release with ID: ${updateRelease.id}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// If it's another error, throw it
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// File names based on release type
|
||||||
|
const jsonFile = isAlpha ? ALPHA_UPDATE_JSON_FILE : UPDATE_JSON_FILE;
|
||||||
|
const proxyFile = isAlpha ? ALPHA_UPDATE_JSON_PROXY : UPDATE_JSON_PROXY;
|
||||||
|
|
||||||
|
// Delete existing assets with these names
|
||||||
|
for (let asset of updateRelease.assets) {
|
||||||
|
if (asset.name === jsonFile) {
|
||||||
|
await github.rest.repos.deleteReleaseAsset({
|
||||||
|
...options,
|
||||||
|
asset_id: asset.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (asset.name === proxyFile) {
|
||||||
|
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: jsonFile,
|
||||||
|
data: JSON.stringify(updateData, null, 2),
|
||||||
|
});
|
||||||
|
|
||||||
|
await github.rest.repos.uploadReleaseAsset({
|
||||||
|
...options,
|
||||||
|
release_id: updateRelease.id,
|
||||||
|
name: proxyFile,
|
||||||
|
data: JSON.stringify(updateDataNew, null, 2),
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Successfully uploaded ${isAlpha ? "alpha" : "stable"} update files to ${releaseTag}`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`Failed to process ${isAlpha ? "alpha" : "stable"} release:`,
|
||||||
|
error.message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error.status === 404) {
|
||||||
|
console.log(`Release not found for tag: ${tag.name}, skipping...`);
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
`Failed to get release for tag: ${tag.name}`,
|
||||||
|
error.message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
11
scripts/utils.mjs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import clc from "cli-color";
|
||||||
|
|
||||||
|
export const log_success = (msg, ...optionalParams) =>
|
||||||
|
console.log(clc.green(msg), ...optionalParams);
|
||||||
|
export const log_error = (msg, ...optionalParams) =>
|
||||||
|
console.log(clc.red(msg), ...optionalParams);
|
||||||
|
export const log_info = (msg, ...optionalParams) =>
|
||||||
|
console.log(clc.bgBlue(msg), ...optionalParams);
|
||||||
|
var debugMsg = clc.xterm(245);
|
||||||
|
export const log_debug = (msg, ...optionalParams) =>
|
||||||
|
console.log(debugMsg(msg), ...optionalParams);
|
1
src-tauri/.clippy.toml
Normal file
@ -0,0 +1 @@
|
|||||||
|
avoid-breaking-exported-api = true
|
4
src-tauri/.gitignore
vendored
@ -1,6 +1,8 @@
|
|||||||
# Generated by Cargo
|
# Generated by Cargo
|
||||||
# will have compiled files and executables
|
# will have compiled files and executables
|
||||||
/target/
|
/target/
|
||||||
|
gen/
|
||||||
WixTools
|
WixTools
|
||||||
resources/Country.mmdb
|
resources
|
||||||
sidecar
|
sidecar
|
||||||
|
|
||||||
|
8479
src-tauri/Cargo.lock
generated
139
src-tauri/Cargo.toml
Normal file → Executable file
@ -1,34 +1,137 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "app"
|
name = "clash-verge"
|
||||||
version = "0.1.0"
|
version = "2.2.3"
|
||||||
description = "clash verge"
|
description = "clash verge"
|
||||||
authors = ["zzzgydi"]
|
authors = ["zzzgydi", "wonfen", "MystiPanda"]
|
||||||
license = "GPL-3.0"
|
license = "GPL-3.0-only"
|
||||||
repository = ""
|
repository = "https://github.com/clash-verge-rev/clash-verge-rev.git"
|
||||||
default-run = "app"
|
default-run = "clash-verge"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
|
[package.metadata.bundle]
|
||||||
|
identifier = "io.github.clash-verge-rev.clash-verge-rev"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "1.0.0-beta.4" }
|
tauri-build = { version = "2.1.0", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
dirs = "4.0.0"
|
|
||||||
chrono = "0.4.19"
|
|
||||||
serde_json = "1.0"
|
|
||||||
serde_yaml = "0.8"
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
|
||||||
tauri = { version = "1.0.0-beta.8", features = ["api-all", "system-tray"] }
|
|
||||||
reqwest = { version = "0.11", features = ["json"] }
|
|
||||||
tokio = { version = "1", features = ["full"] }
|
|
||||||
log = "0.4.14"
|
|
||||||
log4rs = "1.0.0"
|
|
||||||
warp = "0.3"
|
warp = "0.3"
|
||||||
|
anyhow = "1.0.97"
|
||||||
|
dirs = "6.0"
|
||||||
|
open = "5.3"
|
||||||
|
log = "0.4"
|
||||||
|
dunce = "1.0"
|
||||||
|
log4rs = "1"
|
||||||
|
nanoid = "0.4"
|
||||||
|
chrono = "0.4.40"
|
||||||
|
sysinfo = "0.34"
|
||||||
|
boa_engine = "0.20.0"
|
||||||
|
serde_json = "1.0"
|
||||||
|
serde_yaml = "0.9"
|
||||||
|
once_cell = "1.21.3"
|
||||||
port_scanner = "0.1.5"
|
port_scanner = "0.1.5"
|
||||||
|
delay_timer = "0.11.6"
|
||||||
|
parking_lot = "0.12"
|
||||||
|
percent-encoding = "2.3.1"
|
||||||
|
tokio = { version = "1.44", features = [
|
||||||
|
"rt-multi-thread",
|
||||||
|
"macros",
|
||||||
|
"time",
|
||||||
|
"sync",
|
||||||
|
] }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
reqwest = { version = "0.12", features = ["json", "rustls-tls", "cookies"] }
|
||||||
|
regex = "1.11.1"
|
||||||
|
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs", rev = "3d748b5" }
|
||||||
|
image = "0.25.6"
|
||||||
|
imageproc = "0.25.0"
|
||||||
|
tauri = { version = "2.4.0", features = [
|
||||||
|
"protocol-asset",
|
||||||
|
"devtools",
|
||||||
|
"tray-icon",
|
||||||
|
"image-ico",
|
||||||
|
"image-png",
|
||||||
|
] }
|
||||||
|
network-interface = { version = "2.0.1", features = ["serde"] }
|
||||||
|
tauri-plugin-shell = "2.2.0"
|
||||||
|
tauri-plugin-dialog = "2.2.0"
|
||||||
|
tauri-plugin-fs = "2.2.0"
|
||||||
|
tauri-plugin-process = "2.2.0"
|
||||||
|
tauri-plugin-clipboard-manager = "2.2.2"
|
||||||
|
tauri-plugin-deep-link = "2.2.0"
|
||||||
|
tauri-plugin-devtools = "2.0.0"
|
||||||
|
zip = "2.5.0"
|
||||||
|
reqwest_dav = "0.1.15"
|
||||||
|
aes-gcm = { version = "0.10.3", features = ["std"] }
|
||||||
|
base64 = "0.22.1"
|
||||||
|
getrandom = "0.3.2"
|
||||||
|
tokio-tungstenite = "0.26.2"
|
||||||
|
futures = "0.3"
|
||||||
|
sys-locale = "0.3.2"
|
||||||
|
async-trait = "0.1.88"
|
||||||
|
mihomo_api = { path = "src_crates/crate_mihomo_api" }
|
||||||
|
ab_glyph = "0.2.29"
|
||||||
|
tungstenite = "0.26.2"
|
||||||
|
libc = "0.2"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
winreg = { version = "0.10", features = ["transactions"] }
|
runas = "=1.2.0"
|
||||||
|
deelevate = "0.2.0"
|
||||||
|
winreg = "0.55.0"
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
|
users = "0.11.0"
|
||||||
|
|
||||||
|
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||||
|
tauri-plugin-autostart = "2.2.0"
|
||||||
|
tauri-plugin-global-shortcut = "2.2.0"
|
||||||
|
tauri-plugin-updater = "2.6.1"
|
||||||
|
tauri-plugin-window-state = "2.2.1"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["custom-protocol"]
|
default = ["custom-protocol"]
|
||||||
custom-protocol = ["tauri/custom-protocol"]
|
custom-protocol = ["tauri/custom-protocol"]
|
||||||
|
verge-dev = []
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
panic = "abort"
|
||||||
|
codegen-units = 1
|
||||||
|
lto = true
|
||||||
|
opt-level = "s"
|
||||||
|
strip = true
|
||||||
|
|
||||||
|
[profile.dev]
|
||||||
|
incremental = true
|
||||||
|
|
||||||
|
[profile.fast-release]
|
||||||
|
inherits = "release" # 继承 release 的配置
|
||||||
|
panic = "abort" # 与 release 相同
|
||||||
|
codegen-units = 256 # 增加编译单元,提升编译速度
|
||||||
|
lto = false # 禁用 LTO,提升编译速度
|
||||||
|
opt-level = 0 # 禁用优化,大幅提升编译速度
|
||||||
|
debug = true # 保留调试信息
|
||||||
|
strip = false # 不剥离符号,保留调试信息
|
||||||
|
|
||||||
|
[profile.fast-dev]
|
||||||
|
inherits = "dev" # 继承 dev 的配置
|
||||||
|
codegen-units = 256 # 增加编译单元,提升编译速度
|
||||||
|
opt-level = 0 # 禁用优化,进一步提升编译速度
|
||||||
|
incremental = true # 启用增量编译
|
||||||
|
debug = true # 保留调试信息
|
||||||
|
strip = false # 不剥离符号,保留调试信息
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "app_lib"
|
||||||
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = "3.19.1"
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
members = ["src_crates/crate_mihomo_api"]
|
||||||
|
|
||||||
|
# [patch.crates-io]
|
||||||
|
# bitflags = { git = "https://github.com/bitflags/bitflags", rev = "2.9.0" }
|
||||||
|
# zerocopy = { git = "https://github.com/google/zerocopy", rev = "v0.8.24" }
|
||||||
|
# tungstenite = { git = "https://github.com/snapview/tungstenite-rs", rev = "v0.26.2" }
|
||||||
|
BIN
src-tauri/assets/fonts/SF-Pro.ttf
Executable file
21
src-tauri/capabilities/desktop.json
Executable file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"identifier": "desktop-capability",
|
||||||
|
"platforms": ["macOS", "windows", "linux"],
|
||||||
|
"webviews": ["main"],
|
||||||
|
"windows": ["main"],
|
||||||
|
"permissions": [
|
||||||
|
"global-shortcut:default",
|
||||||
|
"updater:default",
|
||||||
|
"dialog:default",
|
||||||
|
"dialog:allow-ask",
|
||||||
|
"dialog:allow-message",
|
||||||
|
"updater:default",
|
||||||
|
"updater:allow-check",
|
||||||
|
"updater:allow-download-and-install",
|
||||||
|
"process:allow-restart",
|
||||||
|
"deep-link:default",
|
||||||
|
"window-state:default",
|
||||||
|
"window-state:default",
|
||||||
|
"autostart:default"
|
||||||
|
]
|
||||||
|
}
|
83
src-tauri/capabilities/migrated.json
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
{
|
||||||
|
"identifier": "migrated",
|
||||||
|
"description": "permissions that were migrated from v1",
|
||||||
|
"local": true,
|
||||||
|
"windows": ["main"],
|
||||||
|
"permissions": [
|
||||||
|
"core:default",
|
||||||
|
"fs:allow-read-file",
|
||||||
|
"fs:allow-exists",
|
||||||
|
{
|
||||||
|
"identifier": "fs:scope",
|
||||||
|
"allow": ["$APPDATA/**", "$RESOURCE/../**", "**"]
|
||||||
|
},
|
||||||
|
"fs:allow-write-file",
|
||||||
|
{
|
||||||
|
"identifier": "fs:scope",
|
||||||
|
"allow": ["$APPDATA/**", "$RESOURCE/../**", "**"]
|
||||||
|
},
|
||||||
|
"fs:allow-app-read",
|
||||||
|
"fs:allow-app-read-recursive",
|
||||||
|
"fs:allow-appcache-read",
|
||||||
|
"fs:allow-appcache-read-recursive",
|
||||||
|
"fs:allow-appconfig-read",
|
||||||
|
"fs:allow-appconfig-read-recursive",
|
||||||
|
"core:window:allow-create",
|
||||||
|
"core:window:allow-center",
|
||||||
|
"core:window:allow-request-user-attention",
|
||||||
|
"core:window:allow-set-resizable",
|
||||||
|
"core:window:allow-set-maximizable",
|
||||||
|
"core:window:allow-set-minimizable",
|
||||||
|
"core:window:allow-set-closable",
|
||||||
|
"core:window:allow-set-title",
|
||||||
|
"core:window:allow-maximize",
|
||||||
|
"core:window:allow-unmaximize",
|
||||||
|
"core:window:allow-minimize",
|
||||||
|
"core:window:allow-unminimize",
|
||||||
|
"core:window:allow-show",
|
||||||
|
"core:window:allow-hide",
|
||||||
|
"core:window:allow-close",
|
||||||
|
"core:window:allow-set-decorations",
|
||||||
|
"core:window:allow-set-always-on-top",
|
||||||
|
"core:window:allow-set-content-protected",
|
||||||
|
"core:window:allow-set-size",
|
||||||
|
"core:window:allow-set-min-size",
|
||||||
|
"core:window:allow-set-max-size",
|
||||||
|
"core:window:allow-set-position",
|
||||||
|
"core:window:allow-set-fullscreen",
|
||||||
|
"core:window:allow-set-focus",
|
||||||
|
"core:window:allow-set-icon",
|
||||||
|
"core:window:allow-set-skip-taskbar",
|
||||||
|
"core:window:allow-set-cursor-grab",
|
||||||
|
"core:window:allow-set-cursor-visible",
|
||||||
|
"core:window:allow-set-cursor-icon",
|
||||||
|
"core:window:allow-set-cursor-position",
|
||||||
|
"core:window:allow-set-ignore-cursor-events",
|
||||||
|
"core:window:allow-start-dragging",
|
||||||
|
"core:window:allow-maximize",
|
||||||
|
"core:window:allow-toggle-maximize",
|
||||||
|
"core:window:allow-unmaximize",
|
||||||
|
"core:window:allow-minimize",
|
||||||
|
"core:window:allow-unminimize",
|
||||||
|
"core:window:allow-set-maximizable",
|
||||||
|
"core:window:allow-set-minimizable",
|
||||||
|
"core:webview:allow-print",
|
||||||
|
"shell:allow-execute",
|
||||||
|
"shell:allow-open",
|
||||||
|
"shell:allow-kill",
|
||||||
|
"shell:allow-spawn",
|
||||||
|
"shell:allow-stdin-write",
|
||||||
|
"dialog:allow-open",
|
||||||
|
"global-shortcut:allow-is-registered",
|
||||||
|
"global-shortcut:allow-register",
|
||||||
|
"global-shortcut:allow-register-all",
|
||||||
|
"global-shortcut:allow-unregister",
|
||||||
|
"global-shortcut:allow-unregister-all",
|
||||||
|
"process:allow-restart",
|
||||||
|
"process:allow-exit",
|
||||||
|
"clipboard-manager:allow-read-text",
|
||||||
|
"clipboard-manager:allow-write-text",
|
||||||
|
"shell:default",
|
||||||
|
"dialog:default"
|
||||||
|
]
|
||||||
|
}
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 6.3 KiB |
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 8.6 KiB |
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 74 KiB |
BIN
src-tauri/icons/tray-icon-mono.ico
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
src-tauri/icons/tray-icon-sys-mono.ico
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
src-tauri/icons/tray-icon-sys.ico
Normal file
After Width: | Height: | Size: 47 KiB |
BIN
src-tauri/icons/tray-icon-tun-mono.ico
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
src-tauri/icons/tray-icon-tun.ico
Normal file
After Width: | Height: | Size: 54 KiB |
BIN
src-tauri/icons/tray-icon.ico
Normal file
After Width: | Height: | Size: 47 KiB |
BIN
src-tauri/images/background.png
Normal file
After Width: | Height: | Size: 22 KiB |
10
src-tauri/packages/linux/clash-verge.desktop
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Categories={{{categories}}}
|
||||||
|
Comment={{{comment}}}
|
||||||
|
Exec={{{exec}}} %u
|
||||||
|
StartupWMClass={{{exec}}}
|
||||||
|
Icon={{{icon}}}
|
||||||
|
Name={{{name}}}
|
||||||
|
Terminal=false
|
||||||
|
Type=Application
|
||||||
|
MimeType=x-scheme-handler/clash;
|
4
src-tauri/packages/linux/post-install.sh
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
chmod +x /usr/bin/install-service
|
||||||
|
chmod +x /usr/bin/uninstall-service
|
||||||
|
chmod +x /usr/bin/clash-verge-service
|
2
src-tauri/packages/linux/pre-remove.sh
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
/usr/bin/uninstall-service
|
14
src-tauri/packages/macos/entitlements.plist
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?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>com.apple.security.app-sandbox</key>
|
||||||
|
<false/>
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>io.github.clash-verge-rev.clash-verge-rev</string>
|
||||||
|
</array>
|
||||||
|
<key>com.apple.security.inherit</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
1057
src-tauri/packages/windows/installer.nsi
Normal file
@ -1,7 +0,0 @@
|
|||||||
# Default Config For Clash Core
|
|
||||||
|
|
||||||
mixed-port: 7890
|
|
||||||
log-level: info
|
|
||||||
allow-lan: false
|
|
||||||
external-controller: 127.0.0.1:9090
|
|
||||||
secret: ""
|
|
@ -1,3 +0,0 @@
|
|||||||
# Profiles Config for Clash Verge
|
|
||||||
|
|
||||||
current: 0
|
|
@ -1,6 +0,0 @@
|
|||||||
# Defaulf Config For Clash Verge
|
|
||||||
|
|
||||||
theme_mode: light
|
|
||||||
enable_self_startup: false
|
|
||||||
enable_system_proxy: false
|
|
||||||
system_proxy_bypass: 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>
|
|
@ -1,6 +1,6 @@
|
|||||||
max_width = 100
|
max_width = 100
|
||||||
hard_tabs = false
|
hard_tabs = false
|
||||||
tab_spaces = 2
|
tab_spaces = 4
|
||||||
newline_style = "Auto"
|
newline_style = "Auto"
|
||||||
use_small_heuristics = "Default"
|
use_small_heuristics = "Default"
|
||||||
reorder_imports = true
|
reorder_imports = true
|
||||||
|
216
src-tauri/src/cmd/app.rs
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
use super::CmdResult;
|
||||||
|
use crate::{
|
||||||
|
feat, logging,
|
||||||
|
utils::{dirs, logging::Type},
|
||||||
|
wrap_err,
|
||||||
|
};
|
||||||
|
use tauri::Manager;
|
||||||
|
|
||||||
|
/// 打开应用程序所在目录
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn open_app_dir() -> CmdResult<()> {
|
||||||
|
let app_dir = wrap_err!(dirs::app_home_dir())?;
|
||||||
|
wrap_err!(open::that(app_dir))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 打开核心所在目录
|
||||||
|
#[tauri::command]
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 打开日志目录
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn open_logs_dir() -> CmdResult<()> {
|
||||||
|
let log_dir = wrap_err!(dirs::app_logs_dir())?;
|
||||||
|
wrap_err!(open::that(log_dir))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 打开网页链接
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn open_web_url(url: String) -> CmdResult<()> {
|
||||||
|
wrap_err!(open::that(url))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 打开/关闭开发者工具
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn open_devtools(app_handle: tauri::AppHandle) {
|
||||||
|
if let Some(window) = app_handle.get_webview_window("main") {
|
||||||
|
if !window.is_devtools_open() {
|
||||||
|
window.open_devtools();
|
||||||
|
} else {
|
||||||
|
window.close_devtools();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 退出应用
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn exit_app() {
|
||||||
|
feat::quit(Some(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 重启应用
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn restart_app() -> CmdResult<()> {
|
||||||
|
feat::restart_app();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取便携版标识
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_portable_flag() -> CmdResult<bool> {
|
||||||
|
Ok(*dirs::PORTABLE_FLAG.get().unwrap_or(&false))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取应用目录
|
||||||
|
#[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 get_auto_launch_status() -> CmdResult<bool> {
|
||||||
|
use crate::core::sysopt::Sysopt;
|
||||||
|
wrap_err!(Sysopt::global().get_launch_status())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 下载图标缓存
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String> {
|
||||||
|
let icon_cache_dir = wrap_err!(dirs::app_home_dir())?.join("icons").join("cache");
|
||||||
|
let icon_path = icon_cache_dir.join(&name);
|
||||||
|
|
||||||
|
// 如果文件已存在,直接返回路径
|
||||||
|
if icon_path.exists() {
|
||||||
|
return Ok(icon_path.to_string_lossy().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保缓存目录存在
|
||||||
|
if !icon_cache_dir.exists() {
|
||||||
|
let _ = std::fs::create_dir_all(&icon_cache_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用临时文件名来下载
|
||||||
|
let temp_path = icon_cache_dir.join(format!("{}.downloading", &name));
|
||||||
|
|
||||||
|
// 下载文件到临时位置
|
||||||
|
let response = wrap_err!(reqwest::get(&url).await)?;
|
||||||
|
|
||||||
|
// 检查内容类型是否为图片
|
||||||
|
let content_type = response
|
||||||
|
.headers()
|
||||||
|
.get(reqwest::header::CONTENT_TYPE)
|
||||||
|
.and_then(|v| v.to_str().ok())
|
||||||
|
.unwrap_or("");
|
||||||
|
|
||||||
|
let is_image = content_type.starts_with("image/");
|
||||||
|
|
||||||
|
// 获取响应内容
|
||||||
|
let content = wrap_err!(response.bytes().await)?;
|
||||||
|
|
||||||
|
// 检查内容是否为HTML (针对CDN错误页面)
|
||||||
|
let is_html = content.len() > 15
|
||||||
|
&& (content.starts_with(b"<!DOCTYPE html")
|
||||||
|
|| content.starts_with(b"<html")
|
||||||
|
|| content.starts_with(b"<?xml"));
|
||||||
|
|
||||||
|
// 只有当内容确实是图片时才保存
|
||||||
|
if is_image && !is_html {
|
||||||
|
{
|
||||||
|
let mut file = match std::fs::File::create(&temp_path) {
|
||||||
|
Ok(file) => file,
|
||||||
|
Err(_) => {
|
||||||
|
if icon_path.exists() {
|
||||||
|
return Ok(icon_path.to_string_lossy().to_string());
|
||||||
|
} else {
|
||||||
|
return Err("Failed to create temporary file".into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
wrap_err!(std::io::copy(&mut content.as_ref(), &mut file))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 再次检查目标文件是否已存在,避免重命名覆盖其他线程已完成的文件
|
||||||
|
if !icon_path.exists() {
|
||||||
|
match std::fs::rename(&temp_path, &icon_path) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(_) => {
|
||||||
|
let _ = std::fs::remove_file(&temp_path);
|
||||||
|
if icon_path.exists() {
|
||||||
|
return Ok(icon_path.to_string_lossy().to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let _ = std::fs::remove_file(&temp_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(icon_path.to_string_lossy().to_string())
|
||||||
|
} else {
|
||||||
|
let _ = std::fs::remove_file(&temp_path);
|
||||||
|
Err(format!("下载的内容不是有效图片: {}", url))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct IconInfo {
|
||||||
|
name: String,
|
||||||
|
previous_t: String,
|
||||||
|
current_t: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 复制图标文件
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult<String> {
|
||||||
|
use std::{fs, path::Path};
|
||||||
|
|
||||||
|
let file_path = Path::new(&path);
|
||||||
|
|
||||||
|
let icon_dir = wrap_err!(dirs::app_home_dir())?.join("icons");
|
||||||
|
if !icon_dir.exists() {
|
||||||
|
let _ = fs::create_dir_all(&icon_dir);
|
||||||
|
}
|
||||||
|
let ext = match file_path.extension() {
|
||||||
|
Some(e) => e.to_string_lossy().to_string(),
|
||||||
|
None => "ico".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let dest_path = icon_dir.join(format!(
|
||||||
|
"{0}-{1}.{ext}",
|
||||||
|
icon_info.name, icon_info.current_t
|
||||||
|
));
|
||||||
|
if file_path.exists() {
|
||||||
|
if icon_info.previous_t.trim() != "" {
|
||||||
|
fs::remove_file(
|
||||||
|
icon_dir.join(format!("{0}-{1}.png", icon_info.name, icon_info.previous_t)),
|
||||||
|
)
|
||||||
|
.unwrap_or_default();
|
||||||
|
fs::remove_file(
|
||||||
|
icon_dir.join(format!("{0}-{1}.ico", icon_info.name, icon_info.previous_t)),
|
||||||
|
)
|
||||||
|
.unwrap_or_default();
|
||||||
|
}
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Cmd,
|
||||||
|
true,
|
||||||
|
"Copying icon file path: {:?} -> file dist: {:?}",
|
||||||
|
path,
|
||||||
|
dest_path
|
||||||
|
);
|
||||||
|
match fs::copy(file_path, &dest_path) {
|
||||||
|
Ok(_) => Ok(dest_path.to_string_lossy().to_string()),
|
||||||
|
Err(err) => Err(err.to_string()),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err("file not found".to_string())
|
||||||
|
}
|
||||||
|
}
|
223
src-tauri/src/cmd/clash.rs
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
use super::CmdResult;
|
||||||
|
use crate::{config::*, core::*, feat, module::mihomo::MihomoManager, wrap_err};
|
||||||
|
use serde_yaml::Mapping;
|
||||||
|
|
||||||
|
/// 复制Clash环境变量
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn copy_clash_env() -> CmdResult {
|
||||||
|
feat::copy_clash_env();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取Clash信息
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_clash_info() -> CmdResult<ClashInfo> {
|
||||||
|
Ok(Config::clash().latest().get_client_info())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 修改Clash配置
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn patch_clash_config(payload: Mapping) -> CmdResult {
|
||||||
|
wrap_err!(feat::patch_clash(payload).await)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 修改Clash模式
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn patch_clash_mode(payload: String) -> CmdResult {
|
||||||
|
feat::change_clash_mode(payload);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 切换Clash核心
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>> {
|
||||||
|
log::info!(target: "app", "changing core to {clash_core}");
|
||||||
|
|
||||||
|
match CoreManager::global()
|
||||||
|
.change_core(Some(clash_core.clone()))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => {
|
||||||
|
log::info!(target: "app", "core changed to {clash_core}");
|
||||||
|
handle::Handle::notice_message("config_core::change_success", &clash_core);
|
||||||
|
handle::Handle::refresh_clash();
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
let error_msg = err.to_string();
|
||||||
|
log::error!(target: "app", "failed to change core: {error_msg}");
|
||||||
|
handle::Handle::notice_message("config_core::change_error", &error_msg);
|
||||||
|
Ok(Some(error_msg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 重启核心
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn restart_core() -> CmdResult {
|
||||||
|
wrap_err!(CoreManager::global().restart_core().await)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取代理延迟
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn clash_api_get_proxy_delay(
|
||||||
|
name: String,
|
||||||
|
url: Option<String>,
|
||||||
|
timeout: i32,
|
||||||
|
) -> CmdResult<serde_json::Value> {
|
||||||
|
MihomoManager::global()
|
||||||
|
.test_proxy_delay(&name, url, timeout)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试URL延迟
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn test_delay(url: String) -> CmdResult<u32> {
|
||||||
|
Ok(feat::test_delay(url).await.unwrap_or(10000u32))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 保存DNS配置到单独文件
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
|
||||||
|
use crate::utils::dirs;
|
||||||
|
use serde_yaml;
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
// 获取DNS配置文件路径
|
||||||
|
let dns_path = dirs::app_home_dir()
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.join("dns_config.yaml");
|
||||||
|
|
||||||
|
// 保存DNS配置到文件
|
||||||
|
let yaml_str = serde_yaml::to_string(&dns_config).map_err(|e| e.to_string())?;
|
||||||
|
fs::write(&dns_path, yaml_str).map_err(|e| e.to_string())?;
|
||||||
|
log::info!(target: "app", "DNS config saved to {:?}", dns_path);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 应用或撤销DNS配置
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn apply_dns_config(apply: bool) -> CmdResult {
|
||||||
|
use crate::{
|
||||||
|
config::Config,
|
||||||
|
core::{handle, CoreManager},
|
||||||
|
utils::dirs,
|
||||||
|
};
|
||||||
|
use tauri::async_runtime;
|
||||||
|
|
||||||
|
// 使用spawn来处理异步操作
|
||||||
|
async_runtime::spawn(async move {
|
||||||
|
if apply {
|
||||||
|
// 读取DNS配置文件
|
||||||
|
let dns_path = match dirs::app_home_dir() {
|
||||||
|
Ok(path) => path.join("dns_config.yaml"),
|
||||||
|
Err(e) => {
|
||||||
|
log::error!(target: "app", "Failed to get home dir: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if !dns_path.exists() {
|
||||||
|
log::warn!(target: "app", "DNS config file not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dns_yaml = match std::fs::read_to_string(&dns_path) {
|
||||||
|
Ok(content) => content,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!(target: "app", "Failed to read DNS config: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 解析DNS配置并创建patch
|
||||||
|
let patch_config = match serde_yaml::from_str::<serde_yaml::Mapping>(&dns_yaml) {
|
||||||
|
Ok(config) => {
|
||||||
|
let mut patch = serde_yaml::Mapping::new();
|
||||||
|
patch.insert("dns".into(), config.into());
|
||||||
|
patch
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!(target: "app", "Failed to parse DNS config: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
log::info!(target: "app", "Applying DNS config from file");
|
||||||
|
|
||||||
|
// 重新生成配置,确保DNS配置被正确应用
|
||||||
|
// 这里不调用patch_clash以避免将DNS配置写入config.yaml
|
||||||
|
Config::runtime()
|
||||||
|
.latest()
|
||||||
|
.patch_config(patch_config.clone());
|
||||||
|
|
||||||
|
// 首先重新生成配置
|
||||||
|
if let Err(err) = Config::generate().await {
|
||||||
|
log::error!(target: "app", "Failed to regenerate config with DNS: {}", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 然后应用新配置
|
||||||
|
if let Err(err) = CoreManager::global().update_config().await {
|
||||||
|
log::error!(target: "app", "Failed to apply config with DNS: {}", err);
|
||||||
|
} else {
|
||||||
|
log::info!(target: "app", "DNS config successfully applied");
|
||||||
|
handle::Handle::refresh_clash();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 当关闭DNS设置时,不需要对配置进行任何修改
|
||||||
|
// 直接重新生成配置,让enhance函数自动跳过DNS配置的加载
|
||||||
|
log::info!(target: "app", "DNS settings disabled, regenerating config");
|
||||||
|
|
||||||
|
// 重新生成配置
|
||||||
|
if let Err(err) = Config::generate().await {
|
||||||
|
log::error!(target: "app", "Failed to regenerate config: {}", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用新配置
|
||||||
|
match CoreManager::global().update_config().await {
|
||||||
|
Ok(_) => {
|
||||||
|
log::info!(target: "app", "Config regenerated successfully");
|
||||||
|
handle::Handle::refresh_clash();
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log::error!(target: "app", "Failed to apply regenerated config: {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 检查DNS配置文件是否存在
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn check_dns_config_exists() -> CmdResult<bool> {
|
||||||
|
use crate::utils::dirs;
|
||||||
|
|
||||||
|
let dns_path = dirs::app_home_dir()
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.join("dns_config.yaml");
|
||||||
|
|
||||||
|
Ok(dns_path.exists())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取DNS配置文件内容
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_dns_config_content() -> CmdResult<String> {
|
||||||
|
use crate::utils::dirs;
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
let dns_path = dirs::app_home_dir()
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.join("dns_config.yaml");
|
||||||
|
|
||||||
|
if !dns_path.exists() {
|
||||||
|
return Err("DNS config file not found".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = fs::read_to_string(&dns_path).map_err(|e| e.to_string())?;
|
||||||
|
Ok(content)
|
||||||
|
}
|
9
src-tauri/src/cmd/lightweight.rs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
use crate::module::lightweight;
|
||||||
|
|
||||||
|
use super::CmdResult;
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn entry_lightweight_mode() -> CmdResult {
|
||||||
|
lightweight::entry_lightweight_mode();
|
||||||
|
Ok(())
|
||||||
|
}
|
1297
src-tauri/src/cmd/media_unlock_checker.rs
Normal file
38
src-tauri/src/cmd/mod.rs
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
// Common result type used by command functions
|
||||||
|
pub type CmdResult<T = ()> = Result<T, String>;
|
||||||
|
|
||||||
|
// Command modules
|
||||||
|
pub mod app;
|
||||||
|
pub mod clash;
|
||||||
|
pub mod lightweight;
|
||||||
|
pub mod media_unlock_checker;
|
||||||
|
pub mod network;
|
||||||
|
pub mod profile;
|
||||||
|
pub mod proxy;
|
||||||
|
pub mod runtime;
|
||||||
|
pub mod save_profile;
|
||||||
|
pub mod service;
|
||||||
|
pub mod system;
|
||||||
|
pub mod uwp;
|
||||||
|
pub mod validate;
|
||||||
|
pub mod verge;
|
||||||
|
pub mod webdav;
|
||||||
|
|
||||||
|
// Re-export all command functions for backwards compatibility
|
||||||
|
pub use app::*;
|
||||||
|
pub use clash::*;
|
||||||
|
pub use lightweight::*;
|
||||||
|
pub use media_unlock_checker::*;
|
||||||
|
pub use network::*;
|
||||||
|
pub use profile::*;
|
||||||
|
pub use proxy::*;
|
||||||
|
pub use runtime::*;
|
||||||
|
pub use save_profile::*;
|
||||||
|
pub use service::*;
|
||||||
|
pub use system::*;
|
||||||
|
pub use uwp::*;
|
||||||
|
pub use validate::*;
|
||||||
|
pub use verge::*;
|
||||||
|
pub use webdav::*;
|
63
src-tauri/src/cmd/network.rs
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
use super::CmdResult;
|
||||||
|
use crate::wrap_err;
|
||||||
|
use network_interface::NetworkInterface;
|
||||||
|
use serde_yaml::Mapping;
|
||||||
|
use sysproxy::{Autoproxy, Sysproxy};
|
||||||
|
|
||||||
|
/// get the system proxy
|
||||||
|
#[tauri::command]
|
||||||
|
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 system proxy
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_auto_proxy() -> CmdResult<Mapping> {
|
||||||
|
let current = wrap_err!(Autoproxy::get_auto_proxy())?;
|
||||||
|
|
||||||
|
let mut map = Mapping::new();
|
||||||
|
map.insert("enable".into(), current.enable.into());
|
||||||
|
map.insert("url".into(), current.url.into());
|
||||||
|
|
||||||
|
Ok(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取网络接口列表
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_network_interfaces() -> Vec<String> {
|
||||||
|
use sysinfo::Networks;
|
||||||
|
let mut result = Vec::new();
|
||||||
|
let networks = Networks::new_with_refreshed_list();
|
||||||
|
for (interface_name, _) in &networks {
|
||||||
|
result.push(interface_name.clone());
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取网络接口详细信息
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_network_interfaces_info() -> CmdResult<Vec<NetworkInterface>> {
|
||||||
|
use network_interface::{NetworkInterface, NetworkInterfaceConfig};
|
||||||
|
|
||||||
|
let names = get_network_interfaces();
|
||||||
|
let interfaces = wrap_err!(NetworkInterface::show())?;
|
||||||
|
|
||||||
|
let mut result = Vec::new();
|
||||||
|
|
||||||
|
for interface in interfaces {
|
||||||
|
if names.contains(&interface.name) {
|
||||||
|
result.push(interface);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
244
src-tauri/src/cmd/profile.rs
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
use super::CmdResult;
|
||||||
|
use crate::{
|
||||||
|
config::{Config, IProfiles, PrfItem, PrfOption},
|
||||||
|
core::{handle, tray::Tray, CoreManager},
|
||||||
|
feat, logging, ret_err,
|
||||||
|
utils::{dirs, help, logging::Type},
|
||||||
|
wrap_err,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 获取配置文件列表
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_profiles() -> CmdResult<IProfiles> {
|
||||||
|
let _ = Tray::global().update_menu();
|
||||||
|
Ok(Config::profiles().data().clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 增强配置文件
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn enhance_profiles() -> CmdResult {
|
||||||
|
wrap_err!(feat::enhance_profiles().await)?;
|
||||||
|
handle::Handle::refresh_clash();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 导入配置文件
|
||||||
|
#[tauri::command]
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 重新排序配置文件
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
|
||||||
|
wrap_err!(Config::profiles().data().reorder(active_id, over_id))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 创建配置文件
|
||||||
|
#[tauri::command]
|
||||||
|
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<bool> {
|
||||||
|
logging!(info, Type::Cmd, true, "开始修改配置文件");
|
||||||
|
|
||||||
|
// 保存当前配置,以便在验证失败时恢复
|
||||||
|
let current_profile = Config::profiles().latest().current.clone();
|
||||||
|
logging!(info, Type::Cmd, true, "当前配置: {:?}", current_profile);
|
||||||
|
|
||||||
|
// 如果要切换配置,先检查目标配置文件是否有语法错误
|
||||||
|
if let Some(new_profile) = profiles.current.as_ref() {
|
||||||
|
if current_profile.as_ref() != Some(new_profile) {
|
||||||
|
logging!(info, Type::Cmd, true, "正在切换到新配置: {}", new_profile);
|
||||||
|
|
||||||
|
// 获取目标配置文件路径
|
||||||
|
let profiles_config = Config::profiles();
|
||||||
|
let profiles_data = profiles_config.latest();
|
||||||
|
let config_file_result = match profiles_data.get_item(new_profile) {
|
||||||
|
Ok(item) => {
|
||||||
|
if let Some(file) = &item.file {
|
||||||
|
let path = dirs::app_profiles_dir().map(|dir| dir.join(file));
|
||||||
|
path.ok()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
logging!(error, Type::Cmd, true, "获取目标配置信息失败: {}", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 如果获取到文件路径,检查YAML语法
|
||||||
|
if let Some(file_path) = config_file_result {
|
||||||
|
if !file_path.exists() {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Cmd,
|
||||||
|
true,
|
||||||
|
"目标配置文件不存在: {}",
|
||||||
|
file_path.display()
|
||||||
|
);
|
||||||
|
handle::Handle::notice_message(
|
||||||
|
"config_validate::file_not_found",
|
||||||
|
format!("{}", file_path.display()),
|
||||||
|
);
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
match std::fs::read_to_string(&file_path) {
|
||||||
|
Ok(content) => match serde_yaml::from_str::<serde_yaml::Value>(&content) {
|
||||||
|
Ok(_) => {
|
||||||
|
logging!(info, Type::Cmd, true, "目标配置文件语法正确");
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
let error_msg = format!(" {}", err);
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Cmd,
|
||||||
|
true,
|
||||||
|
"目标配置文件存在YAML语法错误:{}",
|
||||||
|
error_msg
|
||||||
|
);
|
||||||
|
handle::Handle::notice_message(
|
||||||
|
"config_validate::yaml_syntax_error",
|
||||||
|
&error_msg,
|
||||||
|
);
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
let error_msg = format!("无法读取目标配置文件: {}", err);
|
||||||
|
logging!(error, Type::Cmd, true, "{}", error_msg);
|
||||||
|
handle::Handle::notice_message(
|
||||||
|
"config_validate::file_read_error",
|
||||||
|
&error_msg,
|
||||||
|
);
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新profiles配置
|
||||||
|
logging!(info, Type::Cmd, true, "正在更新配置草稿");
|
||||||
|
let _ = Config::profiles().draft().patch_config(profiles);
|
||||||
|
|
||||||
|
// 更新配置并进行验证
|
||||||
|
match CoreManager::global().update_config().await {
|
||||||
|
Ok((true, _)) => {
|
||||||
|
logging!(info, Type::Cmd, true, "配置更新成功");
|
||||||
|
handle::Handle::refresh_clash();
|
||||||
|
let _ = Tray::global().update_tooltip();
|
||||||
|
Config::profiles().apply();
|
||||||
|
wrap_err!(Config::profiles().data().save_file())?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
Ok((false, error_msg)) => {
|
||||||
|
logging!(warn, Type::Cmd, true, "配置验证失败: {}", error_msg);
|
||||||
|
Config::profiles().discard();
|
||||||
|
// 如果验证失败,恢复到之前的配置
|
||||||
|
if let Some(prev_profile) = current_profile {
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Cmd,
|
||||||
|
true,
|
||||||
|
"尝试恢复到之前的配置: {}",
|
||||||
|
prev_profile
|
||||||
|
);
|
||||||
|
let restore_profiles = IProfiles {
|
||||||
|
current: Some(prev_profile),
|
||||||
|
items: None,
|
||||||
|
};
|
||||||
|
// 静默恢复,不触发验证
|
||||||
|
wrap_err!({ Config::profiles().draft().patch_config(restore_profiles) })?;
|
||||||
|
Config::profiles().apply();
|
||||||
|
wrap_err!(Config::profiles().data().save_file())?;
|
||||||
|
logging!(info, Type::Cmd, true, "成功恢复到之前的配置");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送验证错误通知
|
||||||
|
handle::Handle::notice_message("config_validate::error", &error_msg);
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
logging!(warn, Type::Cmd, true, "更新过程发生错误: {}", e);
|
||||||
|
Config::profiles().discard();
|
||||||
|
handle::Handle::notice_message("config_validate::boot_error", e.to_string());
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 根据profile name修改profiles
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn patch_profiles_config_by_profile_index(
|
||||||
|
_app_handle: tauri::AppHandle,
|
||||||
|
profile_index: String,
|
||||||
|
) -> CmdResult<bool> {
|
||||||
|
logging!(info, Type::Cmd, true, "切换配置到: {}", profile_index);
|
||||||
|
|
||||||
|
let profiles = IProfiles {
|
||||||
|
current: Some(profile_index),
|
||||||
|
items: None,
|
||||||
|
};
|
||||||
|
patch_profiles_config(profiles).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 修改某个profile item的
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
|
||||||
|
wrap_err!(Config::profiles().data().patch_item(index, profile))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 查看配置文件
|
||||||
|
#[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)
|
||||||
|
}
|
24
src-tauri/src/cmd/proxy.rs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
use super::CmdResult;
|
||||||
|
use crate::module::mihomo::MihomoManager;
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_proxies() -> CmdResult<serde_json::Value> {
|
||||||
|
let mannager = MihomoManager::global();
|
||||||
|
|
||||||
|
mannager
|
||||||
|
.refresh_proxies()
|
||||||
|
.await
|
||||||
|
.map(|_| mannager.get_proxies())
|
||||||
|
.or_else(|_| Ok(mannager.get_proxies()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
|
||||||
|
let mannager = MihomoManager::global();
|
||||||
|
|
||||||
|
mannager
|
||||||
|
.refresh_providers_proxies()
|
||||||
|
.await
|
||||||
|
.map(|_| mannager.get_providers_proxies())
|
||||||
|
.or_else(|_| Ok(mannager.get_providers_proxies()))
|
||||||
|
}
|
36
src-tauri/src/cmd/runtime.rs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
use super::CmdResult;
|
||||||
|
use crate::{config::*, wrap_err};
|
||||||
|
use anyhow::Context;
|
||||||
|
use serde_yaml::Mapping;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
/// 获取运行时配置
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_runtime_config() -> CmdResult<Option<Mapping>> {
|
||||||
|
Ok(Config::runtime().latest().config.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取运行时YAML配置
|
||||||
|
#[tauri::command]
|
||||||
|
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")
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取运行时存在的键
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_runtime_exists() -> CmdResult<Vec<String>> {
|
||||||
|
Ok(Config::runtime().latest().exists_keys.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取运行时日志
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String)>>> {
|
||||||
|
Ok(Config::runtime().latest().chain_logs.clone())
|
||||||
|
}
|
116
src-tauri/src/cmd/save_profile.rs
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
use super::CmdResult;
|
||||||
|
use crate::{config::*, core::*, utils::dirs, wrap_err};
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
/// 保存profiles的配置
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdResult {
|
||||||
|
if file_data.is_none() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在异步操作前完成所有文件操作
|
||||||
|
let (file_path, original_content, is_merge_file) = {
|
||||||
|
let profiles = Config::profiles();
|
||||||
|
let profiles_guard = profiles.latest();
|
||||||
|
let item = wrap_err!(profiles_guard.get_item(&index))?;
|
||||||
|
// 确定是否为merge类型文件
|
||||||
|
let is_merge = item.itype.as_ref().is_some_and(|t| t == "merge");
|
||||||
|
let content = wrap_err!(item.read_file())?;
|
||||||
|
let path = item.file.clone().ok_or("file field is null")?;
|
||||||
|
let profiles_dir = wrap_err!(dirs::app_profiles_dir())?;
|
||||||
|
(profiles_dir.join(path), content, is_merge)
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存新的配置文件
|
||||||
|
wrap_err!(fs::write(&file_path, file_data.clone().unwrap()))?;
|
||||||
|
|
||||||
|
let file_path_str = file_path.to_string_lossy().to_string();
|
||||||
|
println!(
|
||||||
|
"[cmd配置save] 开始验证配置文件: {}, 是否为merge文件: {}",
|
||||||
|
file_path_str, is_merge_file
|
||||||
|
);
|
||||||
|
|
||||||
|
// 对于 merge 文件,只进行语法验证,不进行后续内核验证
|
||||||
|
if is_merge_file {
|
||||||
|
println!("[cmd配置save] 检测到merge文件,只进行语法验证");
|
||||||
|
match CoreManager::global()
|
||||||
|
.validate_config_file(&file_path_str, Some(true))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok((true, _)) => {
|
||||||
|
println!("[cmd配置save] merge文件语法验证通过");
|
||||||
|
// 成功后尝试更新整体配置
|
||||||
|
if let Err(e) = CoreManager::global().update_config().await {
|
||||||
|
println!("[cmd配置save] 更新整体配置时发生错误: {}", e);
|
||||||
|
log::warn!(target: "app", "更新整体配置时发生错误: {}", e);
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Ok((false, error_msg)) => {
|
||||||
|
println!("[cmd配置save] merge文件语法验证失败: {}", error_msg);
|
||||||
|
// 恢复原始配置文件
|
||||||
|
wrap_err!(fs::write(&file_path, original_content))?;
|
||||||
|
// 发送合并文件专用错误通知
|
||||||
|
let result = (false, error_msg.clone());
|
||||||
|
crate::cmd::validate::handle_yaml_validation_notice(&result, "合并配置文件");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("[cmd配置save] 验证过程发生错误: {}", e);
|
||||||
|
// 恢复原始配置文件
|
||||||
|
wrap_err!(fs::write(&file_path, original_content))?;
|
||||||
|
return Err(e.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 非merge文件使用完整验证流程
|
||||||
|
match CoreManager::global()
|
||||||
|
.validate_config_file(&file_path_str, None)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok((true, _)) => {
|
||||||
|
println!("[cmd配置save] 验证成功");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Ok((false, error_msg)) => {
|
||||||
|
println!("[cmd配置save] 验证失败: {}", error_msg);
|
||||||
|
// 恢复原始配置文件
|
||||||
|
wrap_err!(fs::write(&file_path, original_content))?;
|
||||||
|
|
||||||
|
// 智能判断错误类型
|
||||||
|
let is_script_error = file_path_str.ends_with(".js")
|
||||||
|
|| error_msg.contains("Script syntax error")
|
||||||
|
|| error_msg.contains("Script must contain a main function")
|
||||||
|
|| error_msg.contains("Failed to read script file");
|
||||||
|
|
||||||
|
if error_msg.contains("YAML syntax error")
|
||||||
|
|| error_msg.contains("Failed to read file:")
|
||||||
|
|| (!file_path_str.ends_with(".js") && !is_script_error)
|
||||||
|
{
|
||||||
|
// 普通YAML错误使用YAML通知处理
|
||||||
|
println!("[cmd配置save] YAML配置文件验证失败,发送通知");
|
||||||
|
let result = (false, error_msg.clone());
|
||||||
|
crate::cmd::validate::handle_yaml_validation_notice(&result, "YAML配置文件");
|
||||||
|
} else if is_script_error {
|
||||||
|
// 脚本错误使用专门的通知处理
|
||||||
|
println!("[cmd配置save] 脚本文件验证失败,发送通知");
|
||||||
|
let result = (false, error_msg.clone());
|
||||||
|
crate::cmd::validate::handle_script_validation_notice(&result, "脚本文件");
|
||||||
|
} else {
|
||||||
|
// 普通配置错误使用一般通知
|
||||||
|
println!("[cmd配置save] 其他类型验证失败,发送一般通知");
|
||||||
|
handle::Handle::notice_message("config_validate::error", &error_msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("[cmd配置save] 验证过程发生错误: {}", e);
|
||||||
|
// 恢复原始配置文件
|
||||||
|
wrap_err!(fs::write(&file_path, original_content))?;
|
||||||
|
Err(e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
40
src-tauri/src/cmd/service.rs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
use super::CmdResult;
|
||||||
|
use crate::{
|
||||||
|
core::{service, CoreManager},
|
||||||
|
utils::i18n::t,
|
||||||
|
};
|
||||||
|
|
||||||
|
async fn execute_service_operation(
|
||||||
|
service_op: impl std::future::Future<Output = Result<(), impl ToString + std::fmt::Debug>>,
|
||||||
|
op_type: &str,
|
||||||
|
) -> CmdResult {
|
||||||
|
if service_op.await.is_err() {
|
||||||
|
let emsg = format!("{} {} failed", op_type, "Service");
|
||||||
|
return Err(t(emsg.as_str()));
|
||||||
|
}
|
||||||
|
if CoreManager::global().restart_core().await.is_err() {
|
||||||
|
let emsg = format!("{} {} failed", "Restart", "Core");
|
||||||
|
return Err(t(emsg.as_str()));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn install_service() -> CmdResult {
|
||||||
|
execute_service_operation(service::install_service(), "Install").await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn uninstall_service() -> CmdResult {
|
||||||
|
execute_service_operation(service::uninstall_service(), "Uninstall").await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn reinstall_service() -> CmdResult {
|
||||||
|
execute_service_operation(service::reinstall_service(), "Reinstall").await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn repair_service() -> CmdResult {
|
||||||
|
execute_service_operation(service::force_reinstall_service(), "Repair").await
|
||||||
|
}
|
94
src-tauri/src/cmd/system.rs
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
use super::CmdResult;
|
||||||
|
use crate::{
|
||||||
|
core::{handle, CoreManager},
|
||||||
|
module::sysinfo::PlatformSpecification,
|
||||||
|
};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use std::{
|
||||||
|
sync::atomic::{AtomicI64, Ordering},
|
||||||
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
|
};
|
||||||
|
use tauri_plugin_clipboard_manager::ClipboardExt;
|
||||||
|
|
||||||
|
// 存储应用启动时间的全局变量
|
||||||
|
static APP_START_TIME: Lazy<AtomicI64> = Lazy::new(|| {
|
||||||
|
// 获取当前系统时间,转换为毫秒级时间戳
|
||||||
|
let now = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_millis() as i64;
|
||||||
|
|
||||||
|
AtomicI64::new(now)
|
||||||
|
});
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn export_diagnostic_info() -> CmdResult<()> {
|
||||||
|
let sysinfo = PlatformSpecification::new_async().await;
|
||||||
|
let info = format!("{:?}", sysinfo);
|
||||||
|
|
||||||
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||||
|
let cliboard = app_handle.clipboard();
|
||||||
|
if cliboard.write_text(info).is_err() {
|
||||||
|
log::error!(target: "app", "Failed to write to clipboard");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_system_info() -> CmdResult<String> {
|
||||||
|
let sysinfo = PlatformSpecification::new_async().await;
|
||||||
|
let info = format!("{:?}", sysinfo);
|
||||||
|
Ok(info)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取当前内核运行模式
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_running_mode() -> Result<String, String> {
|
||||||
|
Ok(CoreManager::global().get_running_mode().await.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取应用的运行时间(毫秒)
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_app_uptime() -> CmdResult<i64> {
|
||||||
|
let start_time = APP_START_TIME.load(Ordering::Relaxed);
|
||||||
|
let now = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_millis() as i64;
|
||||||
|
|
||||||
|
Ok(now - start_time)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 检查应用是否以管理员身份运行
|
||||||
|
#[tauri::command]
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub fn is_admin() -> CmdResult<bool> {
|
||||||
|
use deelevate::{PrivilegeLevel, Token};
|
||||||
|
|
||||||
|
let result = Token::with_current_process()
|
||||||
|
.and_then(|token| token.privilege_level())
|
||||||
|
.map(|level| level != PrivilegeLevel::NotPrivileged)
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 非Windows平台检测是否以管理员身份运行
|
||||||
|
#[tauri::command]
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
pub fn is_admin() -> CmdResult<bool> {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
Ok(unsafe { libc::geteuid() } == 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
Ok(unsafe { libc::geteuid() } == 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
|
||||||
|
{
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
28
src-tauri/src/cmd/uwp.rs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
use super::CmdResult;
|
||||||
|
|
||||||
|
/// Platform-specific implementation for UWP functionality
|
||||||
|
#[cfg(windows)]
|
||||||
|
mod platform {
|
||||||
|
use super::CmdResult;
|
||||||
|
use crate::{core::win_uwp, wrap_err};
|
||||||
|
|
||||||
|
pub async fn invoke_uwp_tool() -> CmdResult {
|
||||||
|
wrap_err!(win_uwp::invoke_uwptools().await)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stub implementation for non-Windows platforms
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
mod platform {
|
||||||
|
use super::CmdResult;
|
||||||
|
|
||||||
|
pub async fn invoke_uwp_tool() -> CmdResult {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Command exposed to Tauri
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn invoke_uwp_tool() -> CmdResult {
|
||||||
|
platform::invoke_uwp_tool().await
|
||||||
|
}
|
104
src-tauri/src/cmd/validate.rs
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
use super::CmdResult;
|
||||||
|
use crate::core::*;
|
||||||
|
|
||||||
|
/// 发送脚本验证通知消息
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn script_validate_notice(status: String, msg: String) -> CmdResult {
|
||||||
|
handle::Handle::notice_message(&status, &msg);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 处理脚本验证相关的所有消息通知
|
||||||
|
/// 统一通知接口,保持消息类型一致性
|
||||||
|
pub fn handle_script_validation_notice(result: &(bool, String), file_type: &str) {
|
||||||
|
if !result.0 {
|
||||||
|
let error_msg = &result.1;
|
||||||
|
|
||||||
|
// 根据错误消息内容判断错误类型
|
||||||
|
let status = if error_msg.starts_with("File not found:") {
|
||||||
|
"config_validate::file_not_found"
|
||||||
|
} else if error_msg.starts_with("Failed to read script file:") {
|
||||||
|
"config_validate::script_error"
|
||||||
|
} else if error_msg.starts_with("Script syntax error:") {
|
||||||
|
"config_validate::script_syntax_error"
|
||||||
|
} else if error_msg == "Script must contain a main function" {
|
||||||
|
"config_validate::script_missing_main"
|
||||||
|
} else {
|
||||||
|
// 如果是其他类型错误,作为一般脚本错误处理
|
||||||
|
"config_validate::script_error"
|
||||||
|
};
|
||||||
|
|
||||||
|
log::warn!(target: "app", "{} 验证失败: {}", file_type, error_msg);
|
||||||
|
handle::Handle::notice_message(status, error_msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 验证指定脚本文件
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn validate_script_file(file_path: String) -> CmdResult<bool> {
|
||||||
|
log::info!(target: "app", "验证脚本文件: {}", file_path);
|
||||||
|
|
||||||
|
match CoreManager::global()
|
||||||
|
.validate_config_file(&file_path, None)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(result) => {
|
||||||
|
handle_script_validation_notice(&result, "脚本文件");
|
||||||
|
Ok(result.0) // 返回验证结果布尔值
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let error_msg = e.to_string();
|
||||||
|
log::error!(target: "app", "验证脚本文件过程发生错误: {}", error_msg);
|
||||||
|
handle::Handle::notice_message("config_validate::process_terminated", &error_msg);
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 处理YAML验证相关的所有消息通知
|
||||||
|
/// 统一通知接口,保持消息类型一致性
|
||||||
|
pub fn handle_yaml_validation_notice(result: &(bool, String), file_type: &str) {
|
||||||
|
if !result.0 {
|
||||||
|
let error_msg = &result.1;
|
||||||
|
println!("[通知] 处理{}验证错误: {}", file_type, error_msg);
|
||||||
|
|
||||||
|
// 检查是否为merge文件
|
||||||
|
let is_merge_file = file_type.contains("合并");
|
||||||
|
|
||||||
|
// 根据错误消息内容判断错误类型
|
||||||
|
let status = if error_msg.starts_with("File not found:") {
|
||||||
|
"config_validate::file_not_found"
|
||||||
|
} else if error_msg.starts_with("Failed to read file:") {
|
||||||
|
"config_validate::yaml_read_error"
|
||||||
|
} else if error_msg.starts_with("YAML syntax error:") {
|
||||||
|
if is_merge_file {
|
||||||
|
"config_validate::merge_syntax_error"
|
||||||
|
} else {
|
||||||
|
"config_validate::yaml_syntax_error"
|
||||||
|
}
|
||||||
|
} else if error_msg.contains("mapping values are not allowed") {
|
||||||
|
if is_merge_file {
|
||||||
|
"config_validate::merge_mapping_error"
|
||||||
|
} else {
|
||||||
|
"config_validate::yaml_mapping_error"
|
||||||
|
}
|
||||||
|
} else if error_msg.contains("did not find expected key") {
|
||||||
|
if is_merge_file {
|
||||||
|
"config_validate::merge_key_error"
|
||||||
|
} else {
|
||||||
|
"config_validate::yaml_key_error"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果是其他类型错误,根据文件类型作为一般错误处理
|
||||||
|
if is_merge_file {
|
||||||
|
"config_validate::merge_error"
|
||||||
|
} else {
|
||||||
|
"config_validate::yaml_error"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
log::warn!(target: "app", "{} 验证失败: {}", file_type, error_msg);
|
||||||
|
println!("[通知] 发送通知: status={}, msg={}", status, error_msg);
|
||||||
|
handle::Handle::notice_message(status, error_msg);
|
||||||
|
}
|
||||||
|
}
|
16
src-tauri/src/cmd/verge.rs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
use super::CmdResult;
|
||||||
|
use crate::{config::*, feat, wrap_err};
|
||||||
|
|
||||||
|
/// 获取Verge配置
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_verge_config() -> CmdResult<IVergeResponse> {
|
||||||
|
let verge = Config::verge();
|
||||||
|
let verge_data = verge.data().clone();
|
||||||
|
Ok(IVergeResponse::from(verge_data))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 修改Verge配置
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn patch_verge_config(payload: IVerge) -> CmdResult {
|
||||||
|
wrap_err!(feat::patch_verge(payload, false).await)
|
||||||
|
}
|
46
src-tauri/src/cmd/webdav.rs
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
use super::CmdResult;
|
||||||
|
use crate::{config::*, core, feat, wrap_err};
|
||||||
|
use reqwest_dav::list_cmd::ListFile;
|
||||||
|
|
||||||
|
/// 保存 WebDAV 配置
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn save_webdav_config(url: String, username: String, password: String) -> CmdResult<()> {
|
||||||
|
let patch = IVerge {
|
||||||
|
webdav_url: Some(url),
|
||||||
|
webdav_username: Some(username),
|
||||||
|
webdav_password: Some(password),
|
||||||
|
..IVerge::default()
|
||||||
|
};
|
||||||
|
Config::verge().draft().patch_config(patch.clone());
|
||||||
|
Config::verge().apply();
|
||||||
|
Config::verge()
|
||||||
|
.data()
|
||||||
|
.save_file()
|
||||||
|
.map_err(|err| err.to_string())?;
|
||||||
|
core::backup::WebDavClient::global().reset();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 创建 WebDAV 备份并上传
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn create_webdav_backup() -> CmdResult<()> {
|
||||||
|
wrap_err!(feat::create_backup_and_upload_webdav().await)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 列出 WebDAV 上的备份文件
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn list_webdav_backup() -> CmdResult<Vec<ListFile>> {
|
||||||
|
wrap_err!(feat::list_wevdav_backup().await)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 删除 WebDAV 上的备份文件
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn delete_webdav_backup(filename: String) -> CmdResult<()> {
|
||||||
|
wrap_err!(feat::delete_webdav_backup(filename).await)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从 WebDAV 恢复备份文件
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn restore_webdav_backup(filename: String) -> CmdResult<()> {
|
||||||
|
wrap_err!(feat::restore_webdav_backup(filename).await)
|
||||||
|
}
|
@ -1,2 +0,0 @@
|
|||||||
pub mod profile;
|
|
||||||
pub mod some;
|
|
@ -1,190 +0,0 @@
|
|||||||
use crate::{
|
|
||||||
config::{ProfileItem, ProfilesConfig},
|
|
||||||
events::state::{ClashInfoState, ProfileLock},
|
|
||||||
utils::{
|
|
||||||
app_home_dir,
|
|
||||||
clash::put_clash_profile,
|
|
||||||
config::{read_profiles, save_profiles},
|
|
||||||
fetch::fetch_profile,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
use std::fs::File;
|
|
||||||
use std::io::Write;
|
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
|
||||||
use tauri::State;
|
|
||||||
|
|
||||||
/// Import the profile from url
|
|
||||||
/// and save to `profiles.yaml`
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn import_profile(url: String, lock: State<'_, ProfileLock>) -> Result<(), String> {
|
|
||||||
let result = match fetch_profile(&url).await {
|
|
||||||
Some(r) => r,
|
|
||||||
None => {
|
|
||||||
log::error!("failed to fetch profile from `{}`", url);
|
|
||||||
return Err(format!("failed to fetch profile from `{}`", url));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// get lock
|
|
||||||
if lock.0.lock().is_err() {
|
|
||||||
return Err(format!("can not get file lock"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// save the profile file
|
|
||||||
let path = app_home_dir().join("profiles").join(&result.file);
|
|
||||||
let file_data = result.data.as_bytes();
|
|
||||||
File::create(path).unwrap().write(file_data).unwrap();
|
|
||||||
|
|
||||||
// update `profiles.yaml`
|
|
||||||
let mut profiles = read_profiles();
|
|
||||||
let mut items = profiles.items.unwrap_or(vec![]);
|
|
||||||
|
|
||||||
let now = SystemTime::now()
|
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.unwrap()
|
|
||||||
.as_secs();
|
|
||||||
|
|
||||||
items.push(ProfileItem {
|
|
||||||
name: Some(result.name),
|
|
||||||
file: Some(result.file),
|
|
||||||
mode: Some(format!("rule")),
|
|
||||||
url: Some(url),
|
|
||||||
selected: Some(vec![]),
|
|
||||||
extra: Some(result.extra),
|
|
||||||
updated: Some(now as usize),
|
|
||||||
});
|
|
||||||
profiles.items = Some(items);
|
|
||||||
save_profiles(&profiles)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update the profile
|
|
||||||
/// and save to `profiles.yaml`
|
|
||||||
/// http request firstly
|
|
||||||
/// then acquire the lock of `profiles.yaml`
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn update_profile(index: usize, lock: State<'_, ProfileLock>) -> Result<(), String> {
|
|
||||||
// get lock
|
|
||||||
if lock.0.lock().is_err() {
|
|
||||||
return Err(format!("can not get file lock"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// update `profiles.yaml`
|
|
||||||
let mut profiles = read_profiles();
|
|
||||||
let mut items = profiles.items.unwrap_or(vec![]);
|
|
||||||
|
|
||||||
if index >= items.len() {
|
|
||||||
return Err(format!("the index out of bound"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let url = match &items[index].url {
|
|
||||||
Some(u) => u,
|
|
||||||
None => return Err(format!("invalid url")),
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = match fetch_profile(&url).await {
|
|
||||||
Some(r) => r,
|
|
||||||
None => {
|
|
||||||
log::error!("failed to fetch profile from `{}`", url);
|
|
||||||
return Err(format!("failed to fetch profile from `{}`", url));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let now = SystemTime::now()
|
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.unwrap()
|
|
||||||
.as_secs() as usize;
|
|
||||||
|
|
||||||
// update file
|
|
||||||
let file_path = &items[index].file.as_ref().unwrap();
|
|
||||||
let file_path = app_home_dir().join("profiles").join(file_path);
|
|
||||||
let file_data = result.data.as_bytes();
|
|
||||||
File::create(file_path).unwrap().write(file_data).unwrap();
|
|
||||||
|
|
||||||
items[index].name = Some(result.name);
|
|
||||||
items[index].extra = Some(result.extra);
|
|
||||||
items[index].updated = Some(now);
|
|
||||||
profiles.items = Some(items);
|
|
||||||
save_profiles(&profiles)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// get all profiles from `profiles.yaml`
|
|
||||||
/// do not acquire the lock of ProfileLock
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn get_profiles() -> Result<ProfilesConfig, String> {
|
|
||||||
Ok(read_profiles())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// patch the profile config
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn set_profiles(
|
|
||||||
index: usize,
|
|
||||||
profile: ProfileItem,
|
|
||||||
lock: State<'_, ProfileLock>,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
// get lock
|
|
||||||
if lock.0.lock().is_err() {
|
|
||||||
return Err(format!("can not get file lock"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut profiles = read_profiles();
|
|
||||||
let mut items = profiles.items.unwrap_or(vec![]);
|
|
||||||
|
|
||||||
if index >= items.len() {
|
|
||||||
return Err(format!("the index out of bound"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if profile.name.is_some() {
|
|
||||||
items[index].name = profile.name;
|
|
||||||
}
|
|
||||||
if profile.file.is_some() {
|
|
||||||
items[index].file = profile.file;
|
|
||||||
}
|
|
||||||
if profile.mode.is_some() {
|
|
||||||
items[index].mode = profile.mode;
|
|
||||||
}
|
|
||||||
if profile.url.is_some() {
|
|
||||||
items[index].url = profile.url;
|
|
||||||
}
|
|
||||||
if profile.selected.is_some() {
|
|
||||||
items[index].selected = profile.selected;
|
|
||||||
}
|
|
||||||
if profile.extra.is_some() {
|
|
||||||
items[index].extra = profile.extra;
|
|
||||||
}
|
|
||||||
|
|
||||||
profiles.items = Some(items);
|
|
||||||
save_profiles(&profiles)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// change the current profile
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn put_profiles(
|
|
||||||
current: usize,
|
|
||||||
lock: State<'_, ProfileLock>,
|
|
||||||
clash_info: State<'_, ClashInfoState>,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
if lock.0.lock().is_err() {
|
|
||||||
return Err(format!("can not get file lock"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let clash_info = match clash_info.0.lock() {
|
|
||||||
Ok(arc) => arc.clone(),
|
|
||||||
_ => return Err(format!("can not get clash info")),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut profiles = read_profiles();
|
|
||||||
let items_len = match &profiles.items {
|
|
||||||
Some(list) => list.len(),
|
|
||||||
None => 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
if current >= items_len {
|
|
||||||
return Err(format!("the index out of bound"));
|
|
||||||
}
|
|
||||||
|
|
||||||
profiles.current = Some(current);
|
|
||||||
match save_profiles(&profiles) {
|
|
||||||
Ok(_) => put_clash_profile(&clash_info).await,
|
|
||||||
Err(err) => Err(err),
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,154 +0,0 @@
|
|||||||
use crate::{
|
|
||||||
config::VergeConfig,
|
|
||||||
events::{
|
|
||||||
emit::ClashInfoPayload,
|
|
||||||
state::{ClashInfoState, VergeConfLock},
|
|
||||||
},
|
|
||||||
utils::{
|
|
||||||
clash::run_clash_bin,
|
|
||||||
config::{read_clash, save_clash, save_verge},
|
|
||||||
sysopt::{get_proxy_config, set_proxy_config, SysProxyConfig, DEFAULT_BYPASS},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
use serde_yaml::Mapping;
|
|
||||||
use tauri::{api::process::kill_children, AppHandle, State};
|
|
||||||
|
|
||||||
/// restart the sidecar
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn restart_sidecar(app_handle: AppHandle, clash_info: State<'_, ClashInfoState>) {
|
|
||||||
kill_children();
|
|
||||||
let payload = run_clash_bin(&app_handle);
|
|
||||||
|
|
||||||
if let Ok(mut arc) = clash_info.0.lock() {
|
|
||||||
*arc = payload;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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_info: State<'_, ClashInfoState>) -> Result<ClashInfoPayload, String> {
|
|
||||||
match clash_info.0.lock() {
|
|
||||||
Ok(arc) => Ok(arc.clone()),
|
|
||||||
Err(_) => Err(format!("can not get clash info")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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) -> Result<(), String> {
|
|
||||||
let mut config = read_clash();
|
|
||||||
for (key, value) in payload.iter() {
|
|
||||||
if config.contains_key(key) {
|
|
||||||
config[key] = value.clone();
|
|
||||||
} else {
|
|
||||||
config.insert(key.clone(), value.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
save_clash(&config)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// set the system proxy
|
|
||||||
/// Tips: only support windows now
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn set_sys_proxy(
|
|
||||||
enable: bool,
|
|
||||||
clash_info: State<'_, ClashInfoState>,
|
|
||||||
verge_lock: State<'_, VergeConfLock>,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
let clash_info = match clash_info.0.lock() {
|
|
||||||
Ok(arc) => arc.clone(),
|
|
||||||
_ => return Err(format!("can not get clash info")),
|
|
||||||
};
|
|
||||||
|
|
||||||
let verge_info = match verge_lock.0.lock() {
|
|
||||||
Ok(arc) => arc.clone(),
|
|
||||||
_ => return Err(format!("can not get verge info")),
|
|
||||||
};
|
|
||||||
|
|
||||||
let port = match clash_info.controller {
|
|
||||||
Some(ctrl) => ctrl.port,
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
if port.is_none() {
|
|
||||||
return Err(format!("can not get clash core's port"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let config = if enable {
|
|
||||||
let server = format!("127.0.0.1:{}", port.unwrap());
|
|
||||||
let bypass = verge_info
|
|
||||||
.system_proxy_bypass
|
|
||||||
.unwrap_or(String::from(DEFAULT_BYPASS));
|
|
||||||
SysProxyConfig {
|
|
||||||
enable,
|
|
||||||
server,
|
|
||||||
bypass,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
SysProxyConfig {
|
|
||||||
enable,
|
|
||||||
server: String::from(""),
|
|
||||||
bypass: String::from(""),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match set_proxy_config(&config) {
|
|
||||||
Ok(_) => Ok(()),
|
|
||||||
Err(_) => Err(format!("can not set proxy")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// get the system proxy
|
|
||||||
/// Tips: only support windows now
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn get_sys_proxy() -> Result<SysProxyConfig, String> {
|
|
||||||
match get_proxy_config() {
|
|
||||||
Ok(value) => Ok(value),
|
|
||||||
Err(err) => Err(err.to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// get the verge config
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn get_verge_config(verge_lock: State<'_, VergeConfLock>) -> Result<VergeConfig, String> {
|
|
||||||
match verge_lock.0.lock() {
|
|
||||||
Ok(arc) => Ok(arc.clone()),
|
|
||||||
Err(_) => Err(format!("can not get the lock")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// patch the verge config
|
|
||||||
/// this command only save the config and not responsible for other things
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn patch_verge_config(
|
|
||||||
payload: VergeConfig,
|
|
||||||
verge_lock: State<'_, VergeConfLock>,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
let mut verge = match verge_lock.0.lock() {
|
|
||||||
Ok(v) => v,
|
|
||||||
Err(_) => return Err(format!("can not get the lock")),
|
|
||||||
};
|
|
||||||
|
|
||||||
if payload.theme_mode.is_some() {
|
|
||||||
verge.theme_mode = payload.theme_mode;
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo
|
|
||||||
if payload.enable_self_startup.is_some() {
|
|
||||||
verge.enable_self_startup = payload.enable_self_startup;
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo
|
|
||||||
if payload.enable_system_proxy.is_some() {
|
|
||||||
verge.enable_system_proxy = payload.enable_system_proxy;
|
|
||||||
}
|
|
||||||
|
|
||||||
if payload.system_proxy_bypass.is_some() {
|
|
||||||
verge.system_proxy_bypass = payload.system_proxy_bypass;
|
|
||||||
}
|
|
||||||
|
|
||||||
save_verge(&verge)
|
|
||||||
}
|
|
@ -1,30 +1,367 @@
|
|||||||
|
use crate::utils::{dirs, help};
|
||||||
|
use anyhow::Result;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_yaml::{Mapping, Value};
|
||||||
|
use std::{
|
||||||
|
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||||
|
str::FromStr,
|
||||||
|
};
|
||||||
|
|
||||||
/// ### `config.yaml` schema
|
#[derive(Default, Debug, Clone)]
|
||||||
/// here should contain all configuration options.
|
pub struct IClashTemp(pub Mapping);
|
||||||
/// See: https://github.com/Dreamacro/clash/wiki/configuration for details
|
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
|
||||||
pub struct ClashConfig {
|
|
||||||
pub port: Option<u32>,
|
|
||||||
|
|
||||||
/// alias to `mixed-port`
|
impl IClashTemp {
|
||||||
pub mixed_port: Option<u32>,
|
pub fn new() -> Self {
|
||||||
|
let template = Self::template();
|
||||||
/// alias to `allow-lan`
|
match dirs::clash_path().and_then(|path| help::read_mapping(&path)) {
|
||||||
pub allow_lan: Option<bool>,
|
Ok(mut map) => {
|
||||||
|
template.0.keys().for_each(|key| {
|
||||||
/// alias to `external-controller`
|
if !map.contains_key(key) {
|
||||||
pub external_ctrl: Option<String>,
|
map.insert(key.clone(), template.0.get(key).unwrap().clone());
|
||||||
|
}
|
||||||
pub secret: Option<String>,
|
});
|
||||||
|
Self(Self::guard(map))
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log::error!(target: "app", "{err}");
|
||||||
|
template
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
pub fn template() -> Self {
|
||||||
pub struct ClashController {
|
let mut map = Mapping::new();
|
||||||
|
let mut tun = Mapping::new();
|
||||||
|
tun.insert("enable".into(), false.into());
|
||||||
|
tun.insert("stack".into(), "gvisor".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());
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
map.insert("redir-port".into(), 7895.into());
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
map.insert("tproxy-port".into(), 7896.into());
|
||||||
|
map.insert("mixed-port".into(), 7897.into());
|
||||||
|
map.insert("socks-port".into(), 7898.into());
|
||||||
|
map.insert("port".into(), 7899.into());
|
||||||
|
map.insert("log-level".into(), "info".into());
|
||||||
|
map.insert("allow-lan".into(), false.into());
|
||||||
|
map.insert("mode".into(), "rule".into());
|
||||||
|
map.insert("external-controller".into(), "127.0.0.1:9097".into());
|
||||||
|
let mut cors_map = Mapping::new();
|
||||||
|
cors_map.insert("allow-private-network".into(), true.into());
|
||||||
|
cors_map.insert("allow-origins".into(), vec!["*"].into());
|
||||||
|
map.insert("secret".into(), "".into());
|
||||||
|
map.insert("tun".into(), tun.into());
|
||||||
|
map.insert("external-controller-cors".into(), cors_map.into());
|
||||||
|
map.insert("unified-delay".into(), true.into());
|
||||||
|
Self(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn guard(mut config: Mapping) -> Mapping {
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
let redir_port = Self::guard_redir_port(&config);
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
let tproxy_port = Self::guard_tproxy_port(&config);
|
||||||
|
let mixed_port = Self::guard_mixed_port(&config);
|
||||||
|
let socks_port = Self::guard_socks_port(&config);
|
||||||
|
let port = Self::guard_port(&config);
|
||||||
|
let ctrl = Self::guard_server_ctrl(&config);
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
config.insert("redir-port".into(), redir_port.into());
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
config.insert("tproxy-port".into(), tproxy_port.into());
|
||||||
|
config.insert("mixed-port".into(), mixed_port.into());
|
||||||
|
config.insert("socks-port".into(), socks_port.into());
|
||||||
|
config.insert("port".into(), port.into());
|
||||||
|
config.insert("external-controller".into(), ctrl.into());
|
||||||
|
config
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn patch_config(&mut self, patch: Mapping) {
|
||||||
|
for (key, value) in patch.into_iter() {
|
||||||
|
self.0.insert(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_config(&self) -> Result<()> {
|
||||||
|
help::save_yaml(
|
||||||
|
&dirs::clash_path()?,
|
||||||
|
&self.0,
|
||||||
|
Some("# Generated by Clash Verge"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_mixed_port(&self) -> u16 {
|
||||||
|
Self::guard_mixed_port(&self.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
pub fn get_socks_port(&self) -> u16 {
|
||||||
|
Self::guard_socks_port(&self.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
pub fn get_port(&self) -> u16 {
|
||||||
|
Self::guard_port(&self.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_client_info(&self) -> ClashInfo {
|
||||||
|
let config = &self.0;
|
||||||
|
|
||||||
|
ClashInfo {
|
||||||
|
mixed_port: Self::guard_mixed_port(config),
|
||||||
|
socks_port: Self::guard_socks_port(config),
|
||||||
|
port: Self::guard_port(config),
|
||||||
|
server: Self::guard_client_ctrl(config),
|
||||||
|
secret: config.get("secret").and_then(|value| match value {
|
||||||
|
Value::String(val_str) => Some(val_str.clone()),
|
||||||
|
Value::Bool(val_bool) => Some(val_bool.to_string()),
|
||||||
|
Value::Number(val_num) => Some(val_num.to_string()),
|
||||||
|
_ => None,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
pub fn guard_redir_port(config: &Mapping) -> u16 {
|
||||||
|
let mut port = config
|
||||||
|
.get("redir-port")
|
||||||
|
.and_then(|value| match value {
|
||||||
|
Value::String(val_str) => val_str.parse().ok(),
|
||||||
|
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.unwrap_or(7895);
|
||||||
|
if port == 0 {
|
||||||
|
port = 7895;
|
||||||
|
}
|
||||||
|
port
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub fn guard_tproxy_port(config: &Mapping) -> u16 {
|
||||||
|
let mut port = config
|
||||||
|
.get("tproxy-port")
|
||||||
|
.and_then(|value| match value {
|
||||||
|
Value::String(val_str) => val_str.parse().ok(),
|
||||||
|
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.unwrap_or(7896);
|
||||||
|
if port == 0 {
|
||||||
|
port = 7896;
|
||||||
|
}
|
||||||
|
port
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn guard_mixed_port(config: &Mapping) -> u16 {
|
||||||
|
let raw_value = config.get("mixed-port");
|
||||||
|
|
||||||
|
let mut port = raw_value
|
||||||
|
.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
|
/// clash core port
|
||||||
pub port: Option<String>,
|
pub mixed_port: u16,
|
||||||
|
pub socks_port: u16,
|
||||||
|
pub port: u16,
|
||||||
/// same as `external-controller`
|
/// same as `external-controller`
|
||||||
pub server: Option<String>,
|
pub server: String,
|
||||||
|
/// clash secret
|
||||||
pub secret: Option<String>,
|
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>>,
|
||||||
|
}
|
||||||
|
165
src-tauri/src/config/config.rs
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
use super::{Draft, IClashTemp, IProfiles, IRuntime, IVerge};
|
||||||
|
use crate::{
|
||||||
|
config::PrfItem,
|
||||||
|
core::{handle, CoreManager},
|
||||||
|
enhance, logging,
|
||||||
|
utils::{dirs, help, logging::Type},
|
||||||
|
};
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use tokio::time::{sleep, Duration};
|
||||||
|
|
||||||
|
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 async fn init_config() -> Result<()> {
|
||||||
|
if Self::profiles()
|
||||||
|
.data()
|
||||||
|
.get_item(&"Merge".to_string())
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
let merge_item = PrfItem::from_merge(Some("Merge".to_string()))?;
|
||||||
|
Self::profiles().data().append_item(merge_item.clone())?;
|
||||||
|
}
|
||||||
|
if Self::profiles()
|
||||||
|
.data()
|
||||||
|
.get_item(&"Script".to_string())
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
let script_item = PrfItem::from_script(Some("Script".to_string()))?;
|
||||||
|
Self::profiles().data().append_item(script_item.clone())?;
|
||||||
|
}
|
||||||
|
// 生成运行时配置
|
||||||
|
if let Err(err) = Self::generate().await {
|
||||||
|
logging!(error, Type::Config, true, "生成运行时配置失败: {}", err);
|
||||||
|
} else {
|
||||||
|
logging!(info, Type::Config, true, "生成运行时配置成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成运行时配置文件并验证
|
||||||
|
let config_result = Self::generate_file(ConfigType::Run);
|
||||||
|
|
||||||
|
let validation_result = if config_result.is_ok() {
|
||||||
|
// 验证配置文件
|
||||||
|
logging!(info, Type::Config, true, "开始验证配置");
|
||||||
|
|
||||||
|
match CoreManager::global().validate_config().await {
|
||||||
|
Ok((is_valid, error_msg)) => {
|
||||||
|
if !is_valid {
|
||||||
|
logging!(
|
||||||
|
warn,
|
||||||
|
Type::Config,
|
||||||
|
true,
|
||||||
|
"[首次启动] 配置验证失败,使用默认最小配置启动: {}",
|
||||||
|
error_msg
|
||||||
|
);
|
||||||
|
CoreManager::global()
|
||||||
|
.use_default_config("config_validate::boot_error", &error_msg)
|
||||||
|
.await?;
|
||||||
|
Some(("config_validate::boot_error", error_msg))
|
||||||
|
} else {
|
||||||
|
logging!(info, Type::Config, true, "配置验证成功");
|
||||||
|
Some(("config_validate::success", String::new()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
logging!(warn, Type::Config, true, "验证进程执行失败: {}", err);
|
||||||
|
CoreManager::global()
|
||||||
|
.use_default_config("config_validate::process_terminated", "")
|
||||||
|
.await?;
|
||||||
|
Some(("config_validate::process_terminated", String::new()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logging!(warn, Type::Config, true, "生成配置文件失败,使用默认配置");
|
||||||
|
CoreManager::global()
|
||||||
|
.use_default_config("config_validate::error", "")
|
||||||
|
.await?;
|
||||||
|
Some(("config_validate::error", String::new()))
|
||||||
|
};
|
||||||
|
|
||||||
|
// 在单独的任务中发送通知
|
||||||
|
if let Some((msg_type, msg_content)) = validation_result {
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
sleep(Duration::from_secs(2)).await;
|
||||||
|
handle::Handle::notice_message(msg_type, &msg_content);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 将订阅丢到对应的文件中
|
||||||
|
pub fn generate_file(typ: ConfigType) -> Result<PathBuf> {
|
||||||
|
let path = match typ {
|
||||||
|
ConfigType::Run => dirs::app_home_dir()?.join(RUNTIME_CONFIG),
|
||||||
|
ConfigType::Check => dirs::app_home_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 async fn generate() -> Result<()> {
|
||||||
|
let (config, exists_keys, logs) = enhance::enhance().await;
|
||||||
|
|
||||||
|
*Config::runtime().draft() = IRuntime {
|
||||||
|
config: Some(config),
|
||||||
|
exists_keys,
|
||||||
|
chain_logs: logs,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ConfigType {
|
||||||
|
Run,
|
||||||
|
Check,
|
||||||
|
}
|
127
src-tauri/src/config/draft.rs
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
use super::{IClashTemp, IProfiles, IRuntime, IVerge};
|
||||||
|
use parking_lot::{MappedMutexGuard, Mutex, MutexGuard};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Draft<T: Clone + ToOwned> {
|
||||||
|
inner: Arc<Mutex<(T, Option<T>)>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! draft_define {
|
||||||
|
($id: ident) => {
|
||||||
|
impl Draft<$id> {
|
||||||
|
#[allow(unused)]
|
||||||
|
pub fn data(&self) -> MappedMutexGuard<$id> {
|
||||||
|
MutexGuard::map(self.inner.lock(), |guard| &mut guard.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn latest(&self) -> MappedMutexGuard<$id> {
|
||||||
|
MutexGuard::map(self.inner.lock(), |inner| {
|
||||||
|
if inner.1.is_none() {
|
||||||
|
&mut inner.0
|
||||||
|
} else {
|
||||||
|
inner.1.as_mut().unwrap()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draft(&self) -> MappedMutexGuard<$id> {
|
||||||
|
MutexGuard::map(self.inner.lock(), |inner| {
|
||||||
|
if inner.1.is_none() {
|
||||||
|
inner.1 = Some(inner.0.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
inner.1.as_mut().unwrap()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply(&self) -> Option<$id> {
|
||||||
|
let mut inner = self.inner.lock();
|
||||||
|
|
||||||
|
match inner.1.take() {
|
||||||
|
Some(draft) => {
|
||||||
|
let old_value = inner.0.to_owned();
|
||||||
|
inner.0 = draft.to_owned();
|
||||||
|
Some(old_value)
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn discard(&self) -> Option<$id> {
|
||||||
|
let mut inner = self.inner.lock();
|
||||||
|
inner.1.take()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<$id> for Draft<$id> {
|
||||||
|
fn from(data: $id) -> Self {
|
||||||
|
Draft {
|
||||||
|
inner: Arc::new(Mutex::new((data, None))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// draft_define!(IClash);
|
||||||
|
draft_define!(IClashTemp);
|
||||||
|
draft_define!(IProfiles);
|
||||||
|
draft_define!(IRuntime);
|
||||||
|
draft_define!(IVerge);
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_draft() {
|
||||||
|
let verge = IVerge {
|
||||||
|
enable_auto_launch: Some(true),
|
||||||
|
enable_tun_mode: Some(false),
|
||||||
|
..IVerge::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let draft = Draft::from(verge);
|
||||||
|
|
||||||
|
assert_eq!(draft.data().enable_auto_launch, Some(true));
|
||||||
|
assert_eq!(draft.data().enable_tun_mode, Some(false));
|
||||||
|
|
||||||
|
assert_eq!(draft.draft().enable_auto_launch, Some(true));
|
||||||
|
assert_eq!(draft.draft().enable_tun_mode, Some(false));
|
||||||
|
|
||||||
|
let mut d = draft.draft();
|
||||||
|
d.enable_auto_launch = Some(false);
|
||||||
|
d.enable_tun_mode = Some(true);
|
||||||
|
drop(d);
|
||||||
|
|
||||||
|
assert_eq!(draft.data().enable_auto_launch, Some(true));
|
||||||
|
assert_eq!(draft.data().enable_tun_mode, Some(false));
|
||||||
|
|
||||||
|
assert_eq!(draft.draft().enable_auto_launch, Some(false));
|
||||||
|
assert_eq!(draft.draft().enable_tun_mode, Some(true));
|
||||||
|
|
||||||
|
assert_eq!(draft.latest().enable_auto_launch, Some(false));
|
||||||
|
assert_eq!(draft.latest().enable_tun_mode, Some(true));
|
||||||
|
|
||||||
|
assert!(draft.apply().is_some());
|
||||||
|
assert!(draft.apply().is_none());
|
||||||
|
|
||||||
|
assert_eq!(draft.data().enable_auto_launch, Some(false));
|
||||||
|
assert_eq!(draft.data().enable_tun_mode, Some(true));
|
||||||
|
|
||||||
|
assert_eq!(draft.draft().enable_auto_launch, Some(false));
|
||||||
|
assert_eq!(draft.draft().enable_tun_mode, Some(true));
|
||||||
|
|
||||||
|
let mut d = draft.draft();
|
||||||
|
d.enable_auto_launch = Some(true);
|
||||||
|
drop(d);
|
||||||
|
|
||||||
|
assert_eq!(draft.data().enable_auto_launch, Some(false));
|
||||||
|
|
||||||
|
assert_eq!(draft.draft().enable_auto_launch, Some(true));
|
||||||
|
|
||||||
|
assert!(draft.discard().is_some());
|
||||||
|
|
||||||
|
assert_eq!(draft.data().enable_auto_launch, Some(false));
|
||||||
|
|
||||||
|
assert!(draft.discard().is_none());
|
||||||
|
|
||||||
|
assert_eq!(draft.draft().enable_auto_launch, Some(false));
|
||||||
|
}
|
95
src-tauri/src/config/encrypt.rs
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
use crate::utils::dirs::get_encryption_key;
|
||||||
|
use aes_gcm::{
|
||||||
|
aead::{Aead, KeyInit},
|
||||||
|
Aes256Gcm, Key,
|
||||||
|
};
|
||||||
|
use base64::{engine::general_purpose::STANDARD, Engine};
|
||||||
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||||
|
|
||||||
|
const NONCE_LENGTH: usize = 12;
|
||||||
|
|
||||||
|
/// Encrypt data
|
||||||
|
pub fn encrypt_data(data: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
|
let encryption_key = get_encryption_key()?;
|
||||||
|
let key = Key::<Aes256Gcm>::from_slice(&encryption_key);
|
||||||
|
let cipher = Aes256Gcm::new(key);
|
||||||
|
|
||||||
|
// Generate random nonce
|
||||||
|
let mut nonce = vec![0u8; NONCE_LENGTH];
|
||||||
|
getrandom::fill(&mut nonce)?;
|
||||||
|
|
||||||
|
// Encrypt data
|
||||||
|
let ciphertext = cipher
|
||||||
|
.encrypt(nonce.as_slice().into(), data.as_bytes())
|
||||||
|
.map_err(|e| format!("Encryption failed: {}", e))?;
|
||||||
|
|
||||||
|
// Concatenate nonce and ciphertext and encode them in base64
|
||||||
|
let mut combined = nonce;
|
||||||
|
combined.extend(ciphertext);
|
||||||
|
Ok(STANDARD.encode(combined))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decrypt data
|
||||||
|
pub fn decrypt_data(encrypted: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
|
let encryption_key = get_encryption_key()?;
|
||||||
|
let key = Key::<Aes256Gcm>::from_slice(&encryption_key);
|
||||||
|
let cipher = Aes256Gcm::new(key);
|
||||||
|
// Decode from base64
|
||||||
|
let data = STANDARD.decode(encrypted)?;
|
||||||
|
if data.len() < NONCE_LENGTH {
|
||||||
|
return Err("Invalid encrypted data".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Separate nonce and ciphertext
|
||||||
|
let (nonce, ciphertext) = data.split_at(NONCE_LENGTH);
|
||||||
|
|
||||||
|
// Decrypt data
|
||||||
|
let plaintext = cipher
|
||||||
|
.decrypt(nonce.into(), ciphertext)
|
||||||
|
.map_err(|e| format!("Decryption failed: {}", e))?;
|
||||||
|
|
||||||
|
String::from_utf8(plaintext).map_err(|e| e.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serialize encrypted function
|
||||||
|
pub fn serialize_encrypted<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
T: Serialize,
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
// 如果序列化失败,返回 None
|
||||||
|
let json = match serde_json::to_string(value) {
|
||||||
|
Ok(j) => j,
|
||||||
|
Err(_) => return serializer.serialize_none(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 如果加密失败,返回 None
|
||||||
|
match encrypt_data(&json) {
|
||||||
|
Ok(encrypted) => serializer.serialize_str(&encrypted),
|
||||||
|
Err(_) => serializer.serialize_none(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserialize decrypted function
|
||||||
|
pub fn deserialize_encrypted<'a, T, D>(deserializer: D) -> Result<T, D::Error>
|
||||||
|
where
|
||||||
|
T: for<'de> Deserialize<'de> + Default,
|
||||||
|
D: Deserializer<'a>,
|
||||||
|
{
|
||||||
|
// 如果反序列化字符串失败,返回默认值
|
||||||
|
let encrypted = match String::deserialize(deserializer) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(_) => return Ok(T::default()),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 如果解密失败,返回默认值
|
||||||
|
let decrypted_string = match decrypt_data(&encrypted) {
|
||||||
|
Ok(data) => data,
|
||||||
|
Err(_) => return Ok(T::default()),
|
||||||
|
};
|
||||||
|
// 如果 JSON 解析失败,返回默认值
|
||||||
|
match serde_json::from_str(&decrypted_string) {
|
||||||
|
Ok(value) => Ok(value),
|
||||||
|
Err(_) => Ok(T::default()),
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,18 @@
|
|||||||
mod clash;
|
mod clash;
|
||||||
|
#[allow(clippy::module_inception)]
|
||||||
|
mod config;
|
||||||
|
mod draft;
|
||||||
|
mod encrypt;
|
||||||
|
mod prfitem;
|
||||||
mod profiles;
|
mod profiles;
|
||||||
|
mod runtime;
|
||||||
mod verge;
|
mod verge;
|
||||||
|
|
||||||
pub use self::clash::*;
|
pub use self::{
|
||||||
pub use self::profiles::*;
|
clash::*, config::*, draft::*, encrypt::*, prfitem::*, profiles::*, runtime::*, verge::*,
|
||||||
pub use self::verge::*;
|
};
|
||||||
|
|
||||||
|
pub const DEFAULT_PAC: &str = r#"function FindProxyForURL(url, host) {
|
||||||
|
return "PROXY 127.0.0.1:%mixed-port%; SOCKS5 127.0.0.1:%mixed-port%; DIRECT;";
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
564
src-tauri/src/config/prfitem.rs
Normal file
@ -0,0 +1,564 @@
|
|||||||
|
use crate::utils::{dirs, help, resolve::VERSION, tmpl};
|
||||||
|
use anyhow::{bail, Context, Result};
|
||||||
|
use reqwest::StatusCode;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_yaml::Mapping;
|
||||||
|
use std::fs;
|
||||||
|
use sysproxy::Sysproxy;
|
||||||
|
|
||||||
|
use super::Config;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
||||||
|
pub struct PrfItem {
|
||||||
|
pub uid: Option<String>,
|
||||||
|
|
||||||
|
/// profile item type
|
||||||
|
/// enum value: remote | local | script | merge
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub itype: Option<String>,
|
||||||
|
|
||||||
|
/// profile name
|
||||||
|
pub name: Option<String>,
|
||||||
|
|
||||||
|
/// profile file
|
||||||
|
pub file: Option<String>,
|
||||||
|
|
||||||
|
/// profile description
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub desc: Option<String>,
|
||||||
|
|
||||||
|
/// source url
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub url: Option<String>,
|
||||||
|
|
||||||
|
/// selected information
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub selected: Option<Vec<PrfSelected>>,
|
||||||
|
|
||||||
|
/// subscription user info
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub extra: Option<PrfExtra>,
|
||||||
|
|
||||||
|
/// updated time
|
||||||
|
pub updated: Option<usize>,
|
||||||
|
|
||||||
|
/// some options of the item
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub option: Option<PrfOption>,
|
||||||
|
|
||||||
|
/// profile web page url
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub home: Option<String>,
|
||||||
|
|
||||||
|
/// the file data
|
||||||
|
#[serde(skip)]
|
||||||
|
pub file_data: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct PrfSelected {
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub now: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Copy, Deserialize, Serialize)]
|
||||||
|
pub struct PrfExtra {
|
||||||
|
pub upload: u64,
|
||||||
|
pub download: u64,
|
||||||
|
pub total: u64,
|
||||||
|
pub expire: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
pub struct PrfOption {
|
||||||
|
/// for `remote` profile's http request
|
||||||
|
/// see issue #13
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub user_agent: Option<String>,
|
||||||
|
|
||||||
|
/// for `remote` profile
|
||||||
|
/// use system proxy
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub with_proxy: Option<bool>,
|
||||||
|
|
||||||
|
/// for `remote` profile
|
||||||
|
/// use self proxy
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub self_proxy: Option<bool>,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub update_interval: Option<u64>,
|
||||||
|
|
||||||
|
/// for `remote` profile
|
||||||
|
/// disable certificate validation
|
||||||
|
/// default is `false`
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub danger_accept_invalid_certs: Option<bool>,
|
||||||
|
|
||||||
|
pub merge: Option<String>,
|
||||||
|
|
||||||
|
pub script: Option<String>,
|
||||||
|
|
||||||
|
pub rules: Option<String>,
|
||||||
|
|
||||||
|
pub proxies: Option<String>,
|
||||||
|
|
||||||
|
pub groups: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
a.merge = b.merge.or(a.merge);
|
||||||
|
a.script = b.script.or(a.script);
|
||||||
|
a.rules = b.rules.or(a.rules);
|
||||||
|
a.proxies = b.proxies.or(a.proxies);
|
||||||
|
a.groups = b.groups.or(a.groups);
|
||||||
|
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, item.option)
|
||||||
|
}
|
||||||
|
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>,
|
||||||
|
option: Option<PrfOption>,
|
||||||
|
) -> Result<PrfItem> {
|
||||||
|
let uid = help::get_uid("L");
|
||||||
|
let file = format!("{uid}.yaml");
|
||||||
|
let opt_ref = option.as_ref();
|
||||||
|
let update_interval = opt_ref.and_then(|o| o.update_interval);
|
||||||
|
let mut merge = opt_ref.and_then(|o| o.merge.clone());
|
||||||
|
let mut script = opt_ref.and_then(|o| o.script.clone());
|
||||||
|
let mut rules = opt_ref.and_then(|o| o.rules.clone());
|
||||||
|
let mut proxies = opt_ref.and_then(|o| o.proxies.clone());
|
||||||
|
let mut groups = opt_ref.and_then(|o| o.groups.clone());
|
||||||
|
|
||||||
|
if merge.is_none() {
|
||||||
|
let merge_item = PrfItem::from_merge(None)?;
|
||||||
|
Config::profiles().data().append_item(merge_item.clone())?;
|
||||||
|
merge = merge_item.uid;
|
||||||
|
}
|
||||||
|
if script.is_none() {
|
||||||
|
let script_item = PrfItem::from_script(None)?;
|
||||||
|
Config::profiles().data().append_item(script_item.clone())?;
|
||||||
|
script = script_item.uid;
|
||||||
|
}
|
||||||
|
if rules.is_none() {
|
||||||
|
let rules_item = PrfItem::from_rules()?;
|
||||||
|
Config::profiles().data().append_item(rules_item.clone())?;
|
||||||
|
rules = rules_item.uid;
|
||||||
|
}
|
||||||
|
if proxies.is_none() {
|
||||||
|
let proxies_item = PrfItem::from_proxies()?;
|
||||||
|
Config::profiles()
|
||||||
|
.data()
|
||||||
|
.append_item(proxies_item.clone())?;
|
||||||
|
proxies = proxies_item.uid;
|
||||||
|
}
|
||||||
|
if groups.is_none() {
|
||||||
|
let groups_item = PrfItem::from_groups()?;
|
||||||
|
Config::profiles().data().append_item(groups_item.clone())?;
|
||||||
|
groups = groups_item.uid;
|
||||||
|
}
|
||||||
|
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: Some(PrfOption {
|
||||||
|
update_interval,
|
||||||
|
merge,
|
||||||
|
script,
|
||||||
|
rules,
|
||||||
|
proxies,
|
||||||
|
groups,
|
||||||
|
..PrfOption::default()
|
||||||
|
}),
|
||||||
|
home: None,
|
||||||
|
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||||
|
file_data: Some(file_data.unwrap_or(tmpl::ITEM_LOCAL.into())),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ## Remote type
|
||||||
|
/// create a new item from url
|
||||||
|
pub async fn from_url(
|
||||||
|
url: &str,
|
||||||
|
name: Option<String>,
|
||||||
|
desc: Option<String>,
|
||||||
|
option: Option<PrfOption>,
|
||||||
|
) -> Result<PrfItem> {
|
||||||
|
let opt_ref = option.as_ref();
|
||||||
|
let with_proxy = opt_ref.is_some_and(|o| o.with_proxy.unwrap_or(false));
|
||||||
|
let self_proxy = opt_ref.is_some_and(|o| o.self_proxy.unwrap_or(false));
|
||||||
|
let accept_invalid_certs =
|
||||||
|
opt_ref.is_some_and(|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 merge = opt_ref.and_then(|o| o.merge.clone());
|
||||||
|
let mut script = opt_ref.and_then(|o| o.script.clone());
|
||||||
|
let mut rules = opt_ref.and_then(|o| o.rules.clone());
|
||||||
|
let mut proxies = opt_ref.and_then(|o| o.proxies.clone());
|
||||||
|
let mut groups = opt_ref.and_then(|o| o.groups.clone());
|
||||||
|
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 update_interval = match update_interval {
|
||||||
|
Some(val) => Some(val),
|
||||||
|
None => match header.get("profile-update-interval") {
|
||||||
|
Some(value) => match value.to_str().unwrap_or("").parse::<u64>() {
|
||||||
|
Ok(val) => Some(val * 60), // hour -> min
|
||||||
|
Err(_) => None,
|
||||||
|
},
|
||||||
|
None => None,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let home = match header.get("profile-web-page-url") {
|
||||||
|
Some(value) => {
|
||||||
|
let str_value = value.to_str().unwrap_or("");
|
||||||
|
Some(str_value.to_string())
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let uid = help::get_uid("R");
|
||||||
|
let file = format!("{uid}.yaml");
|
||||||
|
let name = name.unwrap_or(filename.unwrap_or("Remote File".into()));
|
||||||
|
let data = resp.text_with_charset("utf-8").await?;
|
||||||
|
|
||||||
|
// process the charset "UTF-8 with BOM"
|
||||||
|
let data = data.trim_start_matches('\u{feff}');
|
||||||
|
|
||||||
|
// check the data whether the valid yaml format
|
||||||
|
let yaml = serde_yaml::from_str::<Mapping>(data)
|
||||||
|
.context("the remote profile data is invalid yaml")?;
|
||||||
|
|
||||||
|
if !yaml.contains_key("proxies") && !yaml.contains_key("proxy-providers") {
|
||||||
|
bail!("profile does not contain `proxies` or `proxy-providers`");
|
||||||
|
}
|
||||||
|
|
||||||
|
if merge.is_none() {
|
||||||
|
let merge_item = PrfItem::from_merge(None)?;
|
||||||
|
Config::profiles().data().append_item(merge_item.clone())?;
|
||||||
|
merge = merge_item.uid;
|
||||||
|
}
|
||||||
|
if script.is_none() {
|
||||||
|
let script_item = PrfItem::from_script(None)?;
|
||||||
|
Config::profiles().data().append_item(script_item.clone())?;
|
||||||
|
script = script_item.uid;
|
||||||
|
}
|
||||||
|
if rules.is_none() {
|
||||||
|
let rules_item = PrfItem::from_rules()?;
|
||||||
|
Config::profiles().data().append_item(rules_item.clone())?;
|
||||||
|
rules = rules_item.uid;
|
||||||
|
}
|
||||||
|
if proxies.is_none() {
|
||||||
|
let proxies_item = PrfItem::from_proxies()?;
|
||||||
|
Config::profiles()
|
||||||
|
.data()
|
||||||
|
.append_item(proxies_item.clone())?;
|
||||||
|
proxies = proxies_item.uid;
|
||||||
|
}
|
||||||
|
if groups.is_none() {
|
||||||
|
let groups_item = PrfItem::from_groups()?;
|
||||||
|
Config::profiles().data().append_item(groups_item.clone())?;
|
||||||
|
groups = groups_item.uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(PrfItem {
|
||||||
|
uid: Some(uid),
|
||||||
|
itype: Some("remote".into()),
|
||||||
|
name: Some(name),
|
||||||
|
desc,
|
||||||
|
file: Some(file),
|
||||||
|
url: Some(url.into()),
|
||||||
|
selected: None,
|
||||||
|
extra,
|
||||||
|
option: Some(PrfOption {
|
||||||
|
update_interval,
|
||||||
|
merge,
|
||||||
|
script,
|
||||||
|
rules,
|
||||||
|
proxies,
|
||||||
|
groups,
|
||||||
|
..PrfOption::default()
|
||||||
|
}),
|
||||||
|
home,
|
||||||
|
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||||
|
file_data: Some(data.into()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ## Merge type (enhance)
|
||||||
|
/// create the enhanced item by using `merge` rule
|
||||||
|
pub fn from_merge(uid: Option<String>) -> Result<PrfItem> {
|
||||||
|
let mut id = help::get_uid("m");
|
||||||
|
let mut template = tmpl::ITEM_MERGE_EMPTY.into();
|
||||||
|
if let Some(uid) = uid {
|
||||||
|
id = uid;
|
||||||
|
template = tmpl::ITEM_MERGE.into();
|
||||||
|
}
|
||||||
|
let file = format!("{id}.yaml");
|
||||||
|
|
||||||
|
Ok(PrfItem {
|
||||||
|
uid: Some(id),
|
||||||
|
itype: Some("merge".into()),
|
||||||
|
name: None,
|
||||||
|
desc: None,
|
||||||
|
file: Some(file),
|
||||||
|
url: None,
|
||||||
|
selected: None,
|
||||||
|
extra: None,
|
||||||
|
option: None,
|
||||||
|
home: None,
|
||||||
|
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||||
|
file_data: Some(template),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ## Script type (enhance)
|
||||||
|
/// create the enhanced item by using javascript quick.js
|
||||||
|
pub fn from_script(uid: Option<String>) -> Result<PrfItem> {
|
||||||
|
let mut id = help::get_uid("s");
|
||||||
|
if let Some(uid) = uid {
|
||||||
|
id = uid;
|
||||||
|
}
|
||||||
|
let file = format!("{id}.js"); // js ext
|
||||||
|
|
||||||
|
Ok(PrfItem {
|
||||||
|
uid: Some(id),
|
||||||
|
itype: Some("script".into()),
|
||||||
|
name: None,
|
||||||
|
desc: None,
|
||||||
|
file: Some(file),
|
||||||
|
url: None,
|
||||||
|
home: None,
|
||||||
|
selected: None,
|
||||||
|
extra: None,
|
||||||
|
option: None,
|
||||||
|
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||||
|
file_data: Some(tmpl::ITEM_SCRIPT.into()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ## Rules type (enhance)
|
||||||
|
pub fn from_rules() -> Result<PrfItem> {
|
||||||
|
let uid = help::get_uid("r");
|
||||||
|
let file = format!("{uid}.yaml"); // yaml ext
|
||||||
|
|
||||||
|
Ok(PrfItem {
|
||||||
|
uid: Some(uid),
|
||||||
|
itype: Some("rules".into()),
|
||||||
|
name: None,
|
||||||
|
desc: None,
|
||||||
|
file: Some(file),
|
||||||
|
url: None,
|
||||||
|
home: None,
|
||||||
|
selected: None,
|
||||||
|
extra: None,
|
||||||
|
option: None,
|
||||||
|
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||||
|
file_data: Some(tmpl::ITEM_RULES.into()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ## Proxies type (enhance)
|
||||||
|
pub fn from_proxies() -> Result<PrfItem> {
|
||||||
|
let uid = help::get_uid("p");
|
||||||
|
let file = format!("{uid}.yaml"); // yaml ext
|
||||||
|
|
||||||
|
Ok(PrfItem {
|
||||||
|
uid: Some(uid),
|
||||||
|
itype: Some("proxies".into()),
|
||||||
|
name: None,
|
||||||
|
desc: None,
|
||||||
|
file: Some(file),
|
||||||
|
url: None,
|
||||||
|
home: None,
|
||||||
|
selected: None,
|
||||||
|
extra: None,
|
||||||
|
option: None,
|
||||||
|
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||||
|
file_data: Some(tmpl::ITEM_PROXIES.into()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ## Groups type (enhance)
|
||||||
|
pub fn from_groups() -> Result<PrfItem> {
|
||||||
|
let uid = help::get_uid("g");
|
||||||
|
let file = format!("{uid}.yaml"); // yaml ext
|
||||||
|
|
||||||
|
Ok(PrfItem {
|
||||||
|
uid: Some(uid),
|
||||||
|
itype: Some("groups".into()),
|
||||||
|
name: None,
|
||||||
|
desc: None,
|
||||||
|
file: Some(file),
|
||||||
|
url: None,
|
||||||
|
home: None,
|
||||||
|
selected: None,
|
||||||
|
extra: None,
|
||||||
|
option: None,
|
||||||
|
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||||
|
file_data: Some(tmpl::ITEM_GROUPS.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")
|
||||||
|
}
|
||||||
|
}
|