Compare commits
2729 Commits
updater
...
updater-al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c423fe90e2 | ||
|
|
623461b649 | ||
|
|
1f9ba4de5c | ||
|
|
10f792b8bc | ||
|
|
357b64e9a0 | ||
|
|
f3ad6cee41 | ||
|
|
972310ea4e | ||
|
|
f5315fd737 | ||
|
|
c40c1960ed | ||
|
|
c8ae37811c | ||
|
|
15f05748a5 | ||
|
|
0a0a25913b | ||
|
|
945999889c | ||
|
|
ce7e1d6456 | ||
|
|
72c4b9de8a | ||
|
|
a8257e8cb2 | ||
|
|
15d3c765ac | ||
|
|
e7d3d7e0ae | ||
|
|
c0f3d35e13 | ||
|
|
fc30fab9cd | ||
|
|
a67e8388a9 | ||
|
|
ac7307b1f7 | ||
|
|
4068e5ec9c | ||
|
|
ea71181692 | ||
|
|
9b46c1a991 | ||
|
|
26acce94a4 | ||
|
|
2a0e1e206e | ||
|
|
f83d1d1582 | ||
|
|
77a090d5eb | ||
|
|
a9561e0ded | ||
|
|
dd505e4d58 | ||
|
|
dd0e9d4e1b | ||
|
|
8bc451ff08 | ||
|
|
032e5bf32e | ||
|
|
41ba8f6c7a | ||
|
|
799783d3ef | ||
|
|
77fb40506f | ||
|
|
5858f05c13 | ||
|
|
0432cad112 | ||
|
|
47ec8a348c | ||
|
|
eef348b8dc | ||
|
|
e78a02d0ba | ||
|
|
4e54b61380 | ||
|
|
cc39b2734e | ||
|
|
16eaccb10e | ||
|
|
dbc6f54a77 | ||
|
|
9bd5950f60 | ||
|
|
2c7c416f60 | ||
|
|
3eb0ab4197 | ||
|
|
b7cb511032 | ||
|
|
18850bb51a | ||
|
|
4d718e0cea | ||
|
|
3a2f567055 | ||
|
|
4f474e1098 | ||
|
|
22eac1a832 | ||
|
|
07738dd82d | ||
|
|
f07d508018 | ||
|
|
b591a90100 | ||
|
|
db3cfdf66f | ||
|
|
5cf3e1a817 | ||
|
|
25cfd162f6 | ||
|
|
7001ef2030 | ||
|
|
cbcab72a7c | ||
|
|
d05b1c3130 | ||
|
|
fbdcda9a7f | ||
|
|
0ebc482698 | ||
|
|
09969d95de | ||
|
|
689042df60 | ||
|
|
564fe15df2 | ||
|
|
30015dfd67 | ||
|
|
698c47da69 | ||
|
|
46b7a520cd | ||
|
|
cadd6c3497 | ||
|
|
c4682ab6e9 | ||
|
|
59594855b8 | ||
|
|
44b7af0c6e | ||
|
|
80f550d67e | ||
|
|
44f085604a | ||
|
|
1144f8017a | ||
|
|
d8fa17a3e7 | ||
|
|
bf868e0ae2 | ||
|
|
e1dac63f69 | ||
|
|
2657234761 | ||
|
|
4a345fdd41 | ||
|
|
fee6a586d7 | ||
|
|
7c738483b7 | ||
|
|
c3d8ed28a2 | ||
|
|
f47b4b961b | ||
|
|
7763abf6c2 | ||
|
|
9d9d078346 | ||
|
|
76cec7aa54 | ||
|
|
2223a99ed1 | ||
|
|
4926d33849 | ||
|
|
9978182cae | ||
|
|
1d77bc57df | ||
|
|
1bcb6646c4 | ||
|
|
06130b95d2 | ||
|
|
0536a45959 | ||
|
|
b150eb7e64 | ||
|
|
d5c0b09a2f | ||
|
|
a36d2633c9 | ||
|
|
72783e3ff5 | ||
|
|
929abb3c04 | ||
|
|
6810b1f221 | ||
|
|
a96678caea | ||
|
|
2d59177256 | ||
|
|
62b743fa20 | ||
|
|
b85aaa816e | ||
|
|
74fc3dea33 | ||
|
|
55167e75af | ||
|
|
63d39c24dc | ||
|
|
1d4d8aeb2b | ||
|
|
c9589fc75d | ||
|
|
d0c3a306bc | ||
|
|
1e3566ed7d | ||
|
|
756d303f6a | ||
|
|
a5b948a41c | ||
|
|
6440f856a2 | ||
|
|
25acb8121f | ||
|
|
c05bd31f6b | ||
|
|
9416de6442 | ||
|
|
c507c483fb | ||
|
|
32ee1b36d2 | ||
|
|
2b89f07fe5 | ||
|
|
0621a17d17 | ||
|
|
4840e07da8 | ||
|
|
af1689ee07 | ||
|
|
d3dbc11b1b | ||
|
|
10479b0936 | ||
|
|
fa1f9875d9 | ||
|
|
7466139320 | ||
|
|
7a5bcf67c5 | ||
|
|
10e3010324 | ||
|
|
464f8095ab | ||
|
|
8b800f679b | ||
|
|
5b2f946828 | ||
|
|
b1c31f7a6f | ||
|
|
d49618786e | ||
|
|
866ec2720d | ||
|
|
b4595d7886 | ||
|
|
9af0803e9b | ||
|
|
ebecabc0cc | ||
|
|
c9a9e934f4 | ||
|
|
544f63a2ff | ||
|
|
1dbfc96ebf | ||
|
|
250dcf569c | ||
|
|
0ef552d384 | ||
|
|
998ded5e0b | ||
|
|
d60c3b4d64 | ||
|
|
b6db209670 | ||
|
|
29fd97e402 | ||
|
|
32ebc8d174 | ||
|
|
420b0a254a | ||
|
|
400efa00ec | ||
|
|
4024b72954 | ||
|
|
7cd1816866 | ||
|
|
5983ac5449 | ||
|
|
b70e202201 | ||
|
|
cc5ebec0cb | ||
|
|
1d2fd06507 | ||
|
|
8d99af4c15 | ||
|
|
861428d3bd | ||
|
|
00e3f13bc9 | ||
|
|
2a897ecf8a | ||
|
|
8448217bd4 | ||
|
|
305a64c3e3 | ||
|
|
e1d6c74e4f | ||
|
|
7556758284 | ||
|
|
3926288b7f | ||
|
|
4d56a5cd8c | ||
|
|
d554679ee2 | ||
|
|
6fe7aabe14 | ||
|
|
f4e57a2831 | ||
|
|
32008b9364 | ||
|
|
9d89eeb974 | ||
|
|
95c23730ef | ||
|
|
7acbb5da4f | ||
|
|
ddd85d4d87 | ||
|
|
53a46d0dc6 | ||
|
|
92ae277e3a | ||
|
|
dd190659ef | ||
|
|
6c5488be70 | ||
|
|
316e1d4268 | ||
|
|
c3c13ba6e6 | ||
|
|
b0a82bc7ad | ||
|
|
7f0ebbd83d | ||
|
|
4f86f3c0ec | ||
|
|
c100b9c54d | ||
|
|
ebc9fc5eba | ||
|
|
29ae70bbf6 | ||
|
|
becc51bcd2 | ||
|
|
1993e5dd51 | ||
|
|
d1a2bd78d7 | ||
|
|
a5521404b6 | ||
|
|
8aa7b34197 | ||
|
|
1ddbe7c2cc | ||
|
|
6578cd8c51 | ||
|
|
c2c46d1cae | ||
|
|
95231e7cd3 | ||
|
|
5b6c9be99f | ||
|
|
d587ed09a5 | ||
|
|
10576780ed | ||
|
|
b37b121afb | ||
|
|
2b69ed4915 | ||
|
|
9a666d807c | ||
|
|
050b363066 | ||
|
|
8cae9d4e0a | ||
|
|
b5b65ac8c5 | ||
|
|
7370f00857 | ||
|
|
dc798fe2dd | ||
|
|
eda8fc125f | ||
|
|
8296675574 | ||
|
|
e2ad2d23f8 | ||
|
|
d8df283e93 | ||
|
|
ce42ca77a9 | ||
|
|
cfe8328f9e | ||
|
|
ff5a2c6ca4 | ||
|
|
23b0493d0b | ||
|
|
779291151e | ||
|
|
ba0f4cdde0 | ||
|
|
3983762202 | ||
|
|
c72413cbe6 | ||
|
|
73b9a71c84 | ||
|
|
e7bf997f8c | ||
|
|
dbfcc80afe | ||
|
|
32b6821b8a | ||
|
|
3ce5d3bf2d | ||
|
|
f6023dc618 | ||
|
|
9d9dd73790 | ||
|
|
7383459b9a | ||
|
|
55cde38562 | ||
|
|
d6a79316a6 | ||
|
|
bd3231bfa8 | ||
|
|
8d62c0d521 | ||
|
|
4d37e6870c | ||
|
|
0bb042e085 | ||
|
|
65ed9cb5b2 | ||
|
|
ffc0693afc | ||
|
|
2622cc06eb | ||
|
|
15b117dc15 | ||
|
|
16846aefd3 | ||
|
|
babc2c8f9b | ||
|
|
a0b70f9c34 | ||
|
|
dc46eb9d21 | ||
|
|
824ad9fa29 | ||
|
|
0646fa96a6 | ||
|
|
d05952e945 | ||
|
|
abe5cf1b84 | ||
|
|
07b424cb09 | ||
|
|
c7494de0a7 | ||
|
|
1a9a2ff9e0 | ||
|
|
4f15bdf74d | ||
|
|
541b78ba6f | ||
|
|
05b910dc17 | ||
|
|
41629df189 | ||
|
|
c718ef3058 | ||
|
|
2b1d02f0cc | ||
|
|
272a0c916d | ||
|
|
16187858a3 | ||
|
|
f6ea01238a | ||
|
|
ae4067aee6 | ||
|
|
d401a83c75 | ||
|
|
41a662f784 | ||
|
|
1df9fff0a7 | ||
|
|
40de99e65d | ||
|
|
76227770a4 | ||
|
|
b70cad537c | ||
|
|
1282cc56bf | ||
|
|
85d08fadd9 | ||
|
|
fad73a281a | ||
|
|
18d24d5952 | ||
|
|
6e6462742c | ||
|
|
16381d1895 | ||
|
|
1970155c51 | ||
|
|
51ef1329be | ||
|
|
b6a6f5f434 | ||
|
|
644fdc071f | ||
|
|
25d66a4eee | ||
|
|
9070ef1dbe | ||
|
|
f700dc124e | ||
|
|
5fcea4c684 | ||
|
|
843f40d7d5 | ||
|
|
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 |
25
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
25
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -12,14 +12,16 @@ body:
|
|||||||
1. 请 **确保** 您已经查阅了 [Clash Verge Rev 官方文档](https://clash-verge-rev.github.io/guide/term.html) 以及 [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
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下进行讨论
|
2. 请 **确保** [已有的问题](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue) 中没有人提交过相似issue,否则请在已有的issue下进行讨论
|
||||||
3. 请 **务必** 给issue填写一个简洁明了的标题,以便他人快速检索
|
3. 请 **务必** 给issue填写一个简洁明了的标题,以便他人快速检索
|
||||||
4. 请 **务必** 先下载 [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) 版本测试,确保问题依然存在
|
4. 请 **务必** 查看 [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) 版本更新日志
|
||||||
5. 请 **务必** 按照模板规范详细描述问题,否则issue将会被关闭
|
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:
|
## 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)
|
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
|
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
|
3. Please be sure to fill in a concise and clear title for the issue so that others can quickly search
|
||||||
4. Please be sure to download the [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) version for testing to ensure that the problem still exists
|
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 describe the problem in detail according to the template specification, otherwise the issue will be closed
|
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
|
- type: textarea
|
||||||
id: description
|
id: description
|
||||||
@@ -55,20 +57,9 @@ body:
|
|||||||
description: 请提供你的操作系统版本,Linux请额外提供桌面环境及窗口系统 / Please provide your OS version, for Linux, please also provide the desktop environment and window system
|
description: 请提供你的操作系统版本,Linux请额外提供桌面环境及窗口系统 / Please provide your OS version, for Linux, please also provide the desktop environment and window system
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: checkboxes
|
|
||||||
id: os-labels
|
|
||||||
attributes:
|
|
||||||
label: 系统标签 / OS Labels
|
|
||||||
description: 请选择受影响的操作系统(至少选择一个) / Please select the affected operating system(s) (select at least one)
|
|
||||||
options:
|
|
||||||
- label: windows
|
|
||||||
- label: macos
|
|
||||||
- label: linux
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: 日志 / Log
|
label: 日志(勿上传日志文件,请粘贴日志内容) / Log (Do not upload the log file, paste the log content directly)
|
||||||
description: 请提供完整或相关部分的Debug日志(请在“软件左侧菜单”->“设置”->“日志等级”调整到debug,Verge错误请把“杂项设置”->“app日志等级”调整到debug/trace,并重启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 trace, and restart Verge to take effect. The log file is under "Software left menu" -> "Settings" -> "Log directory")
|
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:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
58
.github/ISSUE_TEMPLATE/i18n_request.yml
vendored
Normal file
58
.github/ISSUE_TEMPLATE/i18n_request.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
name: I18N / 多语言相关
|
||||||
|
title: "[I18N] "
|
||||||
|
description: 用于多语言翻译、国际化相关问题或建议 / For issues or suggestions related to translations and internationalization
|
||||||
|
labels: ["I18n"]
|
||||||
|
type: "Task"
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## I18N 相关问题/建议
|
||||||
|
请用此模板提交翻译错误、缺失、建议或新增语言请求。
|
||||||
|
Please use this template for translation errors, missing translations, suggestions, or new language requests.
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: 问题描述 / Description
|
||||||
|
description: 详细描述你的 I18N 问题或建议 / Please describe your I18N issue or suggestion in detail
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: language
|
||||||
|
attributes:
|
||||||
|
label: 相关语言 / Language
|
||||||
|
description: 例如 zh, en, jp, ru, ... / e.g. zh, en, jp, ru, ...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: suggestion
|
||||||
|
attributes:
|
||||||
|
label: 建议或修正内容 / Suggestion or Correction
|
||||||
|
description: 如果是翻译修正或建议,请填写建议的内容 / If this is a translation correction or suggestion, please provide the suggested content
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: i18n-type
|
||||||
|
attributes:
|
||||||
|
label: 问题类型 / Issue Type
|
||||||
|
description: 请选择适用类型(可多选) / Please select the applicable type(s)
|
||||||
|
options:
|
||||||
|
- label: 翻译错误 / Translation error
|
||||||
|
- label: 翻译缺失 / Missing translation
|
||||||
|
- label: 建议优化 / Suggestion
|
||||||
|
- label: 新增语言 / New language
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: verge-version
|
||||||
|
attributes:
|
||||||
|
label: 软件版本 / Verge Version
|
||||||
|
description: 请提供你使用的 Verge 具体版本 / Please provide the specific version of Verge you are using
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
398
.github/workflows/alpha.yml
vendored
398
.github/workflows/alpha.yml
vendored
@@ -1,54 +1,262 @@
|
|||||||
name: Alpha Build
|
name: Alpha Build
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
# 因为 alpha 不再负责频繁构建,且需要相对于 autobuild 更稳定使用环境
|
||||||
|
# 所以不再使用 workflow_dispatch 触发
|
||||||
|
# 应当通过 git tag 来触发构建
|
||||||
|
# TODO 手动控制版本号
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
schedule:
|
# inputs:
|
||||||
# UTC+8 00:00 (UTC 16:00 previous day) and UTC+8 12:00 (UTC 04:00)
|
# tag_name:
|
||||||
- cron: "0 16,4 * * *"
|
# description: "Alpha tag name (e.g. v1.2.3-alpha.1)"
|
||||||
|
# required: true
|
||||||
|
# type: string
|
||||||
|
|
||||||
|
# push:
|
||||||
|
# # 应当限制在 dev 分支上触发发布。
|
||||||
|
# branches:
|
||||||
|
# - dev
|
||||||
|
# # 应当限制 v*.*.*-alpha* 的 tag 来触发发布。
|
||||||
|
# tags:
|
||||||
|
# - "v*.*.*-alpha*"
|
||||||
permissions: write-all
|
permissions: write-all
|
||||||
env:
|
env:
|
||||||
|
TAG_NAME: alpha
|
||||||
|
TAG_CHANNEL: Alpha
|
||||||
CARGO_INCREMENTAL: 0
|
CARGO_INCREMENTAL: 0
|
||||||
RUST_BACKTRACE: short
|
RUST_BACKTRACE: short
|
||||||
concurrency:
|
concurrency:
|
||||||
# only allow per workflow per commit (and not pr) to run at a time
|
|
||||||
group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}"
|
group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}"
|
||||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check_commit:
|
check_alpha_tag:
|
||||||
|
name: Check Alpha Tag package.json Version Consistency
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
|
||||||
should_run: ${{ steps.check.outputs.should_run }}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
fetch-depth: 2
|
|
||||||
|
|
||||||
- name: Check if commit changed
|
- name: Check tag and package.json version
|
||||||
id: check
|
id: check_tag
|
||||||
run: |
|
run: |
|
||||||
# For manual workflow_dispatch, always run
|
TAG_REF="${GITHUB_REF##*/}"
|
||||||
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
echo "Current tag: $TAG_REF"
|
||||||
echo "should_run=true" >> $GITHUB_OUTPUT
|
if [[ ! "$TAG_REF" =~ -alpha ]]; then
|
||||||
exit 0
|
echo "Current tag is not an alpha tag."
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
PKG_VERSION=$(jq -r .version package.json)
|
||||||
|
echo "package.json version: $PKG_VERSION"
|
||||||
|
if [[ "$PKG_VERSION" != *alpha* ]]; then
|
||||||
|
echo "package.json version is not an alpha version."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ "$TAG_REF" != "v$PKG_VERSION" ]]; then
|
||||||
|
echo "Tag ($TAG_REF) does not match package.json version (v$PKG_VERSION)."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Alpha tag and package.json version are consistent."
|
||||||
|
|
||||||
# Check if current commit is different from the previous one
|
delete_old_assets:
|
||||||
CURRENT_COMMIT=$(git rev-parse HEAD)
|
name: Delete Old Alpha Release Assets and Tags
|
||||||
PREVIOUS_COMMIT=$(git rev-parse HEAD~1)
|
needs: check_alpha_tag
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Delete Old Alpha Tags Except Latest
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
script: |
|
||||||
|
const tagPattern = /-alpha.*/; // 匹配带有 -alpha 的 tag
|
||||||
|
const owner = context.repo.owner;
|
||||||
|
const repo = context.repo.repo;
|
||||||
|
|
||||||
if [ "$CURRENT_COMMIT" != "$PREVIOUS_COMMIT" ]; then
|
try {
|
||||||
echo "New commit detected: $CURRENT_COMMIT"
|
// 获取所有 tag
|
||||||
echo "should_run=true" >> $GITHUB_OUTPUT
|
const { data: tags } = await github.rest.repos.listTags({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
per_page: 100 // 调整 per_page 以获取更多 tag
|
||||||
|
});
|
||||||
|
|
||||||
|
// 过滤出包含 -alpha 的 tag
|
||||||
|
const alphaTags = (await Promise.all(
|
||||||
|
tags
|
||||||
|
.filter(tag => tagPattern.test(tag.name))
|
||||||
|
.map(async tag => {
|
||||||
|
// 获取每个 tag 的 commit 信息以获得日期
|
||||||
|
const { data: commit } = await github.rest.repos.getCommit({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
ref: tag.commit.sha
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
...tag,
|
||||||
|
commitDate: commit.committer && commit.committer.date ? commit.committer.date : commit.commit.author.date
|
||||||
|
};
|
||||||
|
})
|
||||||
|
)).sort((a, b) => {
|
||||||
|
// 按 commit 日期降序排序(最新的在前面)
|
||||||
|
return new Date(b.commitDate) - new Date(a.commitDate);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Found ${alphaTags.length} alpha tags`);
|
||||||
|
|
||||||
|
if (alphaTags.length === 0) {
|
||||||
|
console.log('No alpha tags found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保留最新的 tag
|
||||||
|
const latestTag = alphaTags[0];
|
||||||
|
console.log(`Keeping latest alpha tag: ${latestTag.name}`);
|
||||||
|
|
||||||
|
// 处理其他旧的 alpha tag
|
||||||
|
for (const tag of alphaTags.slice(1)) {
|
||||||
|
console.log(`Processing tag: ${tag.name}`);
|
||||||
|
|
||||||
|
// 获取与 tag 关联的 release
|
||||||
|
try {
|
||||||
|
const { data: release } = await github.rest.repos.getReleaseByTag({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
tag: tag.name
|
||||||
|
});
|
||||||
|
|
||||||
|
// 删除 release 下的所有资产
|
||||||
|
if (release.assets && release.assets.length > 0) {
|
||||||
|
console.log(`Deleting ${release.assets.length} assets for release ${tag.name}`);
|
||||||
|
for (const asset of release.assets) {
|
||||||
|
console.log(`Deleting asset: ${asset.name} (${asset.id})`);
|
||||||
|
await github.rest.repos.deleteReleaseAsset({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
asset_id: asset.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除 release
|
||||||
|
console.log(`Deleting release for tag: ${tag.name}`);
|
||||||
|
await github.rest.repos.deleteRelease({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
release_id: release.id
|
||||||
|
});
|
||||||
|
|
||||||
|
// 删除 tag
|
||||||
|
console.log(`Deleting tag: ${tag.name}`);
|
||||||
|
await github.rest.git.deleteRef({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
ref: `tags/${tag.name}`
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (error.status === 404) {
|
||||||
|
console.log(`No release found for tag ${tag.name}, deleting tag directly`);
|
||||||
|
await github.rest.git.deleteRef({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
ref: `tags/${tag.name}`
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error(`Error processing tag ${tag.name}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Old alpha tags and releases deleted successfully');
|
||||||
|
} catch (error) {
|
||||||
|
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 UPDATE logs
|
||||||
|
id: fetch_update_logs
|
||||||
|
run: |
|
||||||
|
if [ -f "UPDATELOG.md" ]; then
|
||||||
|
UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md)
|
||||||
|
if [ -n "$UPDATE_LOGS" ]; then
|
||||||
|
echo "Found update logs"
|
||||||
|
echo "UPDATE_LOGS<<EOF" >> $GITHUB_ENV
|
||||||
|
echo "$UPDATE_LOGS" >> $GITHUB_ENV
|
||||||
|
echo "EOF" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "No update sections found in UPDATELOG.md"
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
echo "No new commits since last run"
|
echo "UPDATELOG.md file not found"
|
||||||
echo "should_run=false" >> $GITHUB_OUTPUT
|
fi
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Set Env
|
||||||
|
run: |
|
||||||
|
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- run: |
|
||||||
|
if [ -z "$UPDATE_LOGS" ]; then
|
||||||
|
echo "No update logs found, using default message"
|
||||||
|
UPDATE_LOGS="More new features are now supported. Check for detailed changelog soon."
|
||||||
|
else
|
||||||
|
echo "Using found update logs"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
alpha:
|
cat > release.txt << EOF
|
||||||
needs: check_commit
|
$UPDATE_LOGS
|
||||||
if: ${{ needs.check_commit.outputs.should_run == 'true' }}
|
|
||||||
|
## 我应该下载哪个版本?
|
||||||
|
|
||||||
|
### 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: ${{ env.TAG_NAME }}
|
||||||
|
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
|
||||||
|
body_path: release.txt
|
||||||
|
prerelease: true
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
generate_release_notes: true
|
||||||
|
|
||||||
|
alpha-x86-windows-macos-linux:
|
||||||
|
name: Alpha x86 Windows, MacOS and Linux
|
||||||
|
needs: update_tag
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -63,7 +271,6 @@ jobs:
|
|||||||
target: x86_64-apple-darwin
|
target: x86_64-apple-darwin
|
||||||
- os: ubuntu-22.04
|
- os: ubuntu-22.04
|
||||||
target: x86_64-unknown-linux-gnu
|
target: x86_64-unknown-linux-gnu
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
@@ -79,14 +286,13 @@ jobs:
|
|||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
workspaces: src-tauri
|
workspaces: src-tauri
|
||||||
cache-all-crates: true
|
save-if: false
|
||||||
cache-on-failure: true
|
|
||||||
|
|
||||||
- name: Install dependencies (ubuntu only)
|
- name: Install dependencies (ubuntu only)
|
||||||
if: matrix.os == 'ubuntu-22.04'
|
if: matrix.os == 'ubuntu-22.04'
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||||
|
|
||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -101,10 +307,10 @@ jobs:
|
|||||||
- name: Pnpm install and check
|
- name: Pnpm install and check
|
||||||
run: |
|
run: |
|
||||||
pnpm i
|
pnpm i
|
||||||
pnpm check ${{ matrix.target }}
|
pnpm run prebuild ${{ matrix.target }}
|
||||||
|
|
||||||
- name: Alpha Version update
|
# - name: Release ${{ env.TAG_CHANNEL }} Version
|
||||||
run: pnpm run fix-alpha-version
|
# run: pnpm release-version ${{ env.TAG_NAME }}
|
||||||
|
|
||||||
- name: Tauri build
|
- name: Tauri build
|
||||||
uses: tauri-apps/tauri-action@v0
|
uses: tauri-apps/tauri-action@v0
|
||||||
@@ -120,17 +326,17 @@ jobs:
|
|||||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
with:
|
with:
|
||||||
tagName: alpha
|
tagName: ${{ env.TAG_NAME }}
|
||||||
releaseName: "Clash Verge Rev Alpha"
|
releaseName: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
|
||||||
releaseBody: "More new features are now supported."
|
releaseBody: "More new features are now supported."
|
||||||
releaseDraft: false
|
releaseDraft: false
|
||||||
prerelease: true
|
prerelease: true
|
||||||
tauriScript: pnpm
|
tauriScript: pnpm
|
||||||
args: --target ${{ matrix.target }}
|
args: --target ${{ matrix.target }}
|
||||||
|
|
||||||
alpha-for-linux-arm:
|
alpha-arm-linux:
|
||||||
needs: check_commit
|
name: Alpha ARM Linux
|
||||||
if: ${{ needs.check_commit.outputs.should_run == 'true' }}
|
needs: update_tag
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -156,7 +362,7 @@ jobs:
|
|||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
workspaces: src-tauri
|
workspaces: src-tauri
|
||||||
cache-all-crates: true
|
save-if: false
|
||||||
|
|
||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -171,10 +377,13 @@ jobs:
|
|||||||
- name: Pnpm install and check
|
- name: Pnpm install and check
|
||||||
run: |
|
run: |
|
||||||
pnpm i
|
pnpm i
|
||||||
pnpm check ${{ matrix.target }}
|
pnpm run prebuild ${{ matrix.target }}
|
||||||
|
|
||||||
- name: "Setup for linux"
|
# - name: Release ${{ env.TAG_CHANNEL }} Version
|
||||||
run: |-
|
# run: pnpm release-version ${{ env.TAG_NAME }}
|
||||||
|
|
||||||
|
- name: Setup for linux
|
||||||
|
run: |
|
||||||
sudo ls -lR /etc/apt/
|
sudo ls -lR /etc/apt/
|
||||||
|
|
||||||
cat > /tmp/sources.list << EOF
|
cat > /tmp/sources.list << EOF
|
||||||
@@ -193,23 +402,29 @@ jobs:
|
|||||||
sudo mv /tmp/sources.list /etc/apt/sources.list
|
sudo mv /tmp/sources.list /etc/apt/sources.list
|
||||||
|
|
||||||
sudo dpkg --add-architecture ${{ matrix.arch }}
|
sudo dpkg --add-architecture ${{ matrix.arch }}
|
||||||
sudo apt update
|
sudo apt-get update -y
|
||||||
|
sudo apt-get -f install -y
|
||||||
|
|
||||||
sudo apt install -y \
|
sudo apt-get install -y \
|
||||||
|
linux-libc-dev:${{ matrix.arch }} \
|
||||||
|
libc6-dev:${{ matrix.arch }}
|
||||||
|
|
||||||
|
sudo apt-get install -y \
|
||||||
|
libxslt1.1:${{ matrix.arch }} \
|
||||||
libwebkit2gtk-4.1-dev:${{ matrix.arch }} \
|
libwebkit2gtk-4.1-dev:${{ matrix.arch }} \
|
||||||
libayatana-appindicator3-dev:${{ matrix.arch }} \
|
libayatana-appindicator3-dev:${{ matrix.arch }} \
|
||||||
libssl-dev:${{ matrix.arch }} \
|
libssl-dev:${{ matrix.arch }} \
|
||||||
patchelf:${{ matrix.arch }} \
|
patchelf:${{ matrix.arch }} \
|
||||||
librsvg2-dev:${{ matrix.arch }}
|
librsvg2-dev:${{ matrix.arch }}
|
||||||
|
|
||||||
- name: "Install aarch64 tools"
|
- name: Install aarch64 tools
|
||||||
if: matrix.target == 'aarch64-unknown-linux-gnu'
|
if: matrix.target == 'aarch64-unknown-linux-gnu'
|
||||||
run: |
|
run: |
|
||||||
sudo apt install -y \
|
sudo apt install -y \
|
||||||
gcc-aarch64-linux-gnu \
|
gcc-aarch64-linux-gnu \
|
||||||
g++-aarch64-linux-gnu
|
g++-aarch64-linux-gnu
|
||||||
|
|
||||||
- name: "Install armv7 tools"
|
- name: Install armv7 tools
|
||||||
if: matrix.target == 'armv7-unknown-linux-gnueabihf'
|
if: matrix.target == 'armv7-unknown-linux-gnueabihf'
|
||||||
run: |
|
run: |
|
||||||
sudo apt install -y \
|
sudo apt install -y \
|
||||||
@@ -242,18 +457,17 @@ jobs:
|
|||||||
- name: Upload Release
|
- name: Upload Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
tag_name: alpha
|
tag_name: ${{ env.TAG_NAME }}
|
||||||
name: "Clash Verge Rev Alpha"
|
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
|
||||||
body: "More new features are now supported."
|
|
||||||
prerelease: true
|
prerelease: true
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
files: |
|
files: |
|
||||||
src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
|
src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
|
||||||
src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm
|
src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm
|
||||||
|
|
||||||
alpha-for-fixed-webview2:
|
alpha-x86-arm-windows_webview2:
|
||||||
needs: check_commit
|
name: Alpha x86 and ARM Windows with WebView2
|
||||||
if: ${{ needs.check_commit.outputs.should_run == 'true' }}
|
needs: update_tag
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -276,8 +490,7 @@ jobs:
|
|||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
workspaces: src-tauri
|
workspaces: src-tauri
|
||||||
cache-all-crates: true
|
save-if: false
|
||||||
cache-on-failure: true
|
|
||||||
|
|
||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -292,7 +505,10 @@ jobs:
|
|||||||
- name: Pnpm install and check
|
- name: Pnpm install and check
|
||||||
run: |
|
run: |
|
||||||
pnpm i
|
pnpm i
|
||||||
pnpm check ${{ matrix.target }}
|
pnpm run prebuild ${{ matrix.target }}
|
||||||
|
|
||||||
|
# - name: Release ${{ env.TAG_CHANNEL }} Version
|
||||||
|
# run: pnpm release-version ${{ env.TAG_NAME }}
|
||||||
|
|
||||||
- name: Download WebView2 Runtime
|
- name: Download WebView2 Runtime
|
||||||
run: |
|
run: |
|
||||||
@@ -336,77 +552,13 @@ jobs:
|
|||||||
- name: Upload Release
|
- name: Upload Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
tag_name: alpha
|
tag_name: ${{ env.TAG_NAME }}
|
||||||
name: "Clash Verge Rev Alpha"
|
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
|
||||||
body: "More new features are now supported."
|
|
||||||
prerelease: true
|
prerelease: true
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
|
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
|
||||||
|
|
||||||
- name: Portable Bundle
|
- name: Portable Bundle
|
||||||
run: pnpm portable-fixed-webview2 ${{ matrix.target }} --alpha
|
run: pnpm portable-fixed-webview2 ${{ matrix.target }} --${{ env.TAG_NAME }}
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
update_tag:
|
|
||||||
name: Update tag
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: [check_commit, alpha, alpha-for-linux-arm, alpha-for-fixed-webview2]
|
|
||||||
if: ${{ needs.check_commit.outputs.should_run == 'true' }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set Env
|
|
||||||
run: |
|
|
||||||
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
# - name: Update Tag
|
|
||||||
# uses: richardsimko/update-tag@v1
|
|
||||||
# with:
|
|
||||||
# tag_name: alpha
|
|
||||||
# env:
|
|
||||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- run: |
|
|
||||||
cat > release.txt << 'EOF'
|
|
||||||
## 我应该下载哪个版本?
|
|
||||||
|
|
||||||
### 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
|
|
||||||
|
|||||||
466
.github/workflows/autobuild.yml
vendored
Normal file
466
.github/workflows/autobuild.yml
vendored
Normal file
@@ -0,0 +1,466 @@
|
|||||||
|
name: Auto Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
# UTC+8 0,6,12,18
|
||||||
|
- cron: "0 16,22,4,10 * * *"
|
||||||
|
permissions: write-all
|
||||||
|
env:
|
||||||
|
TAG_NAME: autobuild
|
||||||
|
TAG_CHANNEL: AutoBuild
|
||||||
|
CARGO_INCREMENTAL: 0
|
||||||
|
RUST_BACKTRACE: short
|
||||||
|
concurrency:
|
||||||
|
group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}"
|
||||||
|
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check_commit:
|
||||||
|
name: Check Commit Needs Build
|
||||||
|
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: |
|
||||||
|
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||||
|
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
CURRENT_VERSION=$(cat package.json | jq -r '.version')
|
||||||
|
echo "Current version: $CURRENT_VERSION"
|
||||||
|
|
||||||
|
git checkout HEAD~1 package.json
|
||||||
|
PREVIOUS_VERSION=$(cat package.json | jq -r '.version')
|
||||||
|
echo "Previous version: $PREVIOUS_VERSION"
|
||||||
|
|
||||||
|
git checkout HEAD package.json
|
||||||
|
|
||||||
|
if [ "$CURRENT_VERSION" != "$PREVIOUS_VERSION" ]; then
|
||||||
|
echo "Version changed from $PREVIOUS_VERSION to $CURRENT_VERSION"
|
||||||
|
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
update_tag:
|
||||||
|
name: Update tag
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: check_commit
|
||||||
|
if: ${{ needs.check_commit.outputs.should_run == 'true' }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Fetch UPDATE logs
|
||||||
|
id: fetch_update_logs
|
||||||
|
run: |
|
||||||
|
if [ -f "UPDATELOG.md" ]; then
|
||||||
|
UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md)
|
||||||
|
if [ -n "$UPDATE_LOGS" ]; then
|
||||||
|
echo "Found update logs"
|
||||||
|
echo "UPDATE_LOGS<<EOF" >> $GITHUB_ENV
|
||||||
|
echo "$UPDATE_LOGS" >> $GITHUB_ENV
|
||||||
|
echo "EOF" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "No update 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: |
|
||||||
|
if [ -z "$UPDATE_LOGS" ]; then
|
||||||
|
echo "No update logs found, using default message"
|
||||||
|
UPDATE_LOGS="More new features are now supported. Check for detailed changelog soon."
|
||||||
|
else
|
||||||
|
echo "Using found update logs"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat > release.txt << EOF
|
||||||
|
$UPDATE_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: ${{ env.TAG_NAME }}
|
||||||
|
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
|
||||||
|
body_path: release.txt
|
||||||
|
prerelease: true
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
generate_release_notes: true
|
||||||
|
|
||||||
|
autobuild-x86-windows-macos-linux:
|
||||||
|
name: Autobuild x86 Windows, MacOS and Linux
|
||||||
|
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
|
||||||
|
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
||||||
|
|
||||||
|
- 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
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
name: Install pnpm
|
||||||
|
with:
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Install Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "22"
|
||||||
|
cache: "pnpm"
|
||||||
|
|
||||||
|
- name: Pnpm install and check
|
||||||
|
run: |
|
||||||
|
pnpm i
|
||||||
|
pnpm run prebuild ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Release ${{ env.TAG_CHANNEL }} Version
|
||||||
|
run: pnpm release-version ${{ env.TAG_NAME }}
|
||||||
|
|
||||||
|
- 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: ${{ env.TAG_NAME }}
|
||||||
|
releaseName: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
|
||||||
|
releaseBody: "More new features are now supported."
|
||||||
|
releaseDraft: false
|
||||||
|
prerelease: true
|
||||||
|
tauriScript: pnpm
|
||||||
|
args: --target ${{ matrix.target }}
|
||||||
|
|
||||||
|
autobuild-arm-linux:
|
||||||
|
name: Autobuild ARM Linux
|
||||||
|
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
|
||||||
|
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Install Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "22"
|
||||||
|
cache: "pnpm"
|
||||||
|
|
||||||
|
- name: Pnpm install and check
|
||||||
|
run: |
|
||||||
|
pnpm i
|
||||||
|
pnpm run prebuild ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Release ${{ env.TAG_CHANNEL }} Version
|
||||||
|
run: pnpm release-version ${{ env.TAG_NAME }}
|
||||||
|
|
||||||
|
- 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-get update -y
|
||||||
|
sudo apt-get -f install -y
|
||||||
|
|
||||||
|
sudo apt-get install -y \
|
||||||
|
linux-libc-dev:${{ matrix.arch }} \
|
||||||
|
libc6-dev:${{ matrix.arch }}
|
||||||
|
|
||||||
|
sudo apt-get 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: ${{ env.TAG_NAME }}
|
||||||
|
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
|
||||||
|
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
|
||||||
|
|
||||||
|
autobuild-x86-arm-windows_webview2:
|
||||||
|
name: Autobuild x86 and ARM Windows with 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
|
||||||
|
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Install Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "22"
|
||||||
|
cache: "pnpm"
|
||||||
|
|
||||||
|
- name: Pnpm install and check
|
||||||
|
run: |
|
||||||
|
pnpm i
|
||||||
|
pnpm run prebuild ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Release ${{ env.TAG_CHANNEL }} Version
|
||||||
|
run: pnpm release-version ${{ env.TAG_NAME }}
|
||||||
|
|
||||||
|
- 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: ${{ env.TAG_NAME }}
|
||||||
|
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
|
||||||
|
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 }} --${{ env.TAG_NAME }}
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
63
.github/workflows/clippy.yml
vendored
Normal file
63
.github/workflows/clippy.yml
vendored
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
name: Clippy Lint
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
clippy:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: windows-latest
|
||||||
|
target: x86_64-pc-windows-msvc
|
||||||
|
- os: macos-latest
|
||||||
|
target: aarch64-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
|
||||||
|
save-if: false
|
||||||
|
|
||||||
|
- 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"
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Pnpm install and check
|
||||||
|
run: |
|
||||||
|
pnpm i
|
||||||
|
pnpm run prebuild ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Build Web Assets
|
||||||
|
run: pnpm run web:build
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||||
|
|
||||||
|
- name: Run Clippy
|
||||||
|
run: cargo clippy --manifest-path src-tauri/Cargo.toml --all-targets --all-features -- -D warnings
|
||||||
64
.github/workflows/cross_check.yaml
vendored
Normal file
64
.github/workflows/cross_check.yaml
vendored
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
name: Cross Platform Cargo Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
# pull_request:
|
||||||
|
# push:
|
||||||
|
# branches: [main, dev]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cargo-check:
|
||||||
|
# Treat all Rust compiler warnings as errors
|
||||||
|
env:
|
||||||
|
RUSTFLAGS: "-D warnings"
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: macos-latest
|
||||||
|
target: aarch64-apple-darwin
|
||||||
|
- os: windows-latest
|
||||||
|
target: x86_64-pc-windows-msvc
|
||||||
|
- os: ubuntu-latest
|
||||||
|
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
|
||||||
|
with:
|
||||||
|
targets: ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Add Rust Target
|
||||||
|
run: rustup target add ${{ matrix.target }}
|
||||||
|
|
||||||
|
- 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 run prebuild ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Rust Cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: src-tauri
|
||||||
|
save-if: false
|
||||||
|
|
||||||
|
- name: Cargo Check (deny warnings)
|
||||||
|
working-directory: src-tauri
|
||||||
|
run: |
|
||||||
|
cargo check --target ${{ matrix.target }} --workspace --all-features
|
||||||
5
.github/workflows/dev.yml
vendored
5
.github/workflows/dev.yml
vendored
@@ -42,8 +42,7 @@ jobs:
|
|||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
workspaces: src-tauri
|
workspaces: src-tauri
|
||||||
cache-all-crates: true
|
save-if: false
|
||||||
cache-on-failure: true
|
|
||||||
|
|
||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -58,7 +57,7 @@ jobs:
|
|||||||
- name: Pnpm install and check
|
- name: Pnpm install and check
|
||||||
run: |
|
run: |
|
||||||
pnpm i
|
pnpm i
|
||||||
pnpm check ${{ matrix.target }}
|
pnpm run prebuild ${{ matrix.target }}
|
||||||
|
|
||||||
- name: Tauri build
|
- name: Tauri build
|
||||||
uses: tauri-apps/tauri-action@v0
|
uses: tauri-apps/tauri-action@v0
|
||||||
|
|||||||
50
.github/workflows/fmt.yml
vendored
Normal file
50
.github/workflows/fmt.yml
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
name: Check Formatting
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
rustfmt:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: install Rust stable and rustfmt
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
components: rustfmt
|
||||||
|
|
||||||
|
- name: run cargo fmt
|
||||||
|
run: cargo fmt --manifest-path ./src-tauri/Cargo.toml --all -- --check
|
||||||
|
|
||||||
|
prettier:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- run: npm i -g --force corepack
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "lts/*"
|
||||||
|
- run: pnpm i --frozen-lockfile
|
||||||
|
- run: pnpm format:check
|
||||||
|
|
||||||
|
# taplo:
|
||||||
|
# name: taplo (.toml files)
|
||||||
|
# runs-on: ubuntu-latest
|
||||||
|
# steps:
|
||||||
|
# - uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# - name: install Rust stable
|
||||||
|
# uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
# - name: install taplo-cli
|
||||||
|
# uses: taiki-e/install-action@v2
|
||||||
|
# with:
|
||||||
|
# tool: taplo-cli
|
||||||
|
|
||||||
|
# - run: taplo fmt --check --diff
|
||||||
53
.github/workflows/release.yml
vendored
53
.github/workflows/release.yml
vendored
@@ -1,7 +1,16 @@
|
|||||||
name: Release Build
|
name: Release Build
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
# ! 为了避免重复发布版本,应当通过独特 git tag 触发。
|
||||||
|
# ! 不再使用 workflow_dispatch 触发。
|
||||||
|
# workflow_dispatch:
|
||||||
|
push:
|
||||||
|
# 应当限制在 main 分支上触发发布。
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
# 应当限制 v*.*.* 的 tag 触发发布。
|
||||||
|
tags:
|
||||||
|
- "v*.*.*"
|
||||||
permissions: write-all
|
permissions: write-all
|
||||||
env:
|
env:
|
||||||
CARGO_INCREMENTAL: 0
|
CARGO_INCREMENTAL: 0
|
||||||
@@ -12,7 +21,28 @@ concurrency:
|
|||||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
check_tag_version:
|
||||||
|
name: Check Release Tag and package.json Version Consistency
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Check tag and package.json version
|
||||||
|
run: |
|
||||||
|
TAG_REF="${GITHUB_REF##*/}"
|
||||||
|
echo "Current tag: $TAG_REF"
|
||||||
|
PKG_VERSION=$(jq -r .version package.json)
|
||||||
|
echo "package.json version: $PKG_VERSION"
|
||||||
|
if [[ "$TAG_REF" != "v$PKG_VERSION" ]]; then
|
||||||
|
echo "Tag ($TAG_REF) does not match package.json version (v$PKG_VERSION)."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Tag and package.json version are consistent."
|
||||||
|
|
||||||
release:
|
release:
|
||||||
|
name: Release Build
|
||||||
|
needs: check_tag_version
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -43,14 +73,13 @@ jobs:
|
|||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
workspaces: src-tauri
|
workspaces: src-tauri
|
||||||
cache-all-crates: true
|
save-if: false
|
||||||
cache-on-failure: true
|
|
||||||
|
|
||||||
- name: Install dependencies (ubuntu only)
|
- name: Install dependencies (ubuntu only)
|
||||||
if: matrix.os == 'ubuntu-22.04'
|
if: matrix.os == 'ubuntu-22.04'
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||||
|
|
||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -65,7 +94,7 @@ jobs:
|
|||||||
- name: Pnpm install and check
|
- name: Pnpm install and check
|
||||||
run: |
|
run: |
|
||||||
pnpm i
|
pnpm i
|
||||||
pnpm check ${{ matrix.target }}
|
pnpm run prebuild ${{ matrix.target }}
|
||||||
|
|
||||||
- name: Tauri build
|
- name: Tauri build
|
||||||
uses: tauri-apps/tauri-action@v0
|
uses: tauri-apps/tauri-action@v0
|
||||||
@@ -88,6 +117,7 @@ jobs:
|
|||||||
args: --target ${{ matrix.target }}
|
args: --target ${{ matrix.target }}
|
||||||
|
|
||||||
release-for-linux-arm:
|
release-for-linux-arm:
|
||||||
|
name: Release Build for Linux ARM
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -113,7 +143,7 @@ jobs:
|
|||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
workspaces: src-tauri
|
workspaces: src-tauri
|
||||||
cache-all-crates: true
|
save-if: false
|
||||||
|
|
||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -128,7 +158,7 @@ jobs:
|
|||||||
- name: Pnpm install and check
|
- name: Pnpm install and check
|
||||||
run: |
|
run: |
|
||||||
pnpm i
|
pnpm i
|
||||||
pnpm check ${{ matrix.target }}
|
pnpm run prebuild ${{ matrix.target }}
|
||||||
|
|
||||||
- name: "Setup for linux"
|
- name: "Setup for linux"
|
||||||
run: |-
|
run: |-
|
||||||
@@ -153,6 +183,7 @@ jobs:
|
|||||||
sudo apt update
|
sudo apt update
|
||||||
|
|
||||||
sudo apt install -y \
|
sudo apt install -y \
|
||||||
|
libxslt1.1:${{ matrix.arch }} \
|
||||||
libwebkit2gtk-4.1-dev:${{ matrix.arch }} \
|
libwebkit2gtk-4.1-dev:${{ matrix.arch }} \
|
||||||
libayatana-appindicator3-dev:${{ matrix.arch }} \
|
libayatana-appindicator3-dev:${{ matrix.arch }} \
|
||||||
libssl-dev:${{ matrix.arch }} \
|
libssl-dev:${{ matrix.arch }} \
|
||||||
@@ -208,6 +239,7 @@ jobs:
|
|||||||
src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm
|
src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm
|
||||||
|
|
||||||
release-for-fixed-webview2:
|
release-for-fixed-webview2:
|
||||||
|
name: Release Build for Fixed WebView2
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -230,8 +262,7 @@ jobs:
|
|||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
workspaces: src-tauri
|
workspaces: src-tauri
|
||||||
cache-all-crates: true
|
save-if: false
|
||||||
cache-on-failure: true
|
|
||||||
|
|
||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -246,7 +277,7 @@ jobs:
|
|||||||
- name: Pnpm install and check
|
- name: Pnpm install and check
|
||||||
run: |
|
run: |
|
||||||
pnpm i
|
pnpm i
|
||||||
pnpm check ${{ matrix.target }}
|
pnpm run prebuild ${{ matrix.target }}
|
||||||
|
|
||||||
- name: Download WebView2 Runtime
|
- name: Download WebView2 Runtime
|
||||||
run: |
|
run: |
|
||||||
@@ -302,6 +333,7 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
release-update:
|
release-update:
|
||||||
|
name: Release Update
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [release, release-for-linux-arm]
|
needs: [release, release-for-linux-arm]
|
||||||
steps:
|
steps:
|
||||||
@@ -352,6 +384,7 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
submit-to-winget:
|
submit-to-winget:
|
||||||
|
name: Submit to Winget
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [release-update]
|
needs: [release-update]
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,3 +9,4 @@ scripts/_env.sh
|
|||||||
.vscode
|
.vscode
|
||||||
.tool-versions
|
.tool-versions
|
||||||
.idea
|
.idea
|
||||||
|
.old
|
||||||
|
|||||||
@@ -1 +1,26 @@
|
|||||||
pnpm pretty-quick --staged
|
#!/bin/bash
|
||||||
|
|
||||||
|
#pnpm pretty-quick --staged
|
||||||
|
|
||||||
|
if git diff --cached --name-only | grep -q '^src/'; then
|
||||||
|
pnpm format:check
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Code format check failed in src/. Please fix formatting issues."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if git diff --cached --name-only | grep -q '^src-tauri/'; then
|
||||||
|
cd src-tauri
|
||||||
|
cargo fmt
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "rustfmt failed to format the code. Please fix the issues and try again."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
cd ..
|
||||||
|
fi
|
||||||
|
|
||||||
|
#git add .
|
||||||
|
|
||||||
|
# 允许提交
|
||||||
|
exit 0
|
||||||
|
|||||||
27
.husky/pre-push
Normal file
27
.husky/pre-push
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if git diff --cached --name-only | grep -q '^src-tauri/'; then
|
||||||
|
cargo clippy --manifest-path ./src-tauri/Cargo.toml
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Clippy found issues in src-tauri. Please fix them before pushing."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
remote_name="$1"
|
||||||
|
remote_url=$(git remote get-url "$remote_name")
|
||||||
|
|
||||||
|
if [[ "$remote_url" =~ github\.com[:/]+clash-verge-rev/clash-verge-rev(\.git)?$ ]]; then
|
||||||
|
echo "[pre-push] Detected push to clash-verge-rev/clash-verge-rev ($remote_url)"
|
||||||
|
echo "[pre-push] Running pnpm format:check..."
|
||||||
|
|
||||||
|
pnpm format:check
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "❌ Code format check failed. Please fix formatting before pushing."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "[pre-push] Not pushing to target repo. Skipping format check."
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
8
.prettierignore
Normal file
8
.prettierignore
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# README.md
|
||||||
|
# UPDATELOG.md
|
||||||
|
# CONTRIBUTING.md
|
||||||
|
|
||||||
|
pnpm-lock.yaml
|
||||||
|
|
||||||
|
src-tauri/target/
|
||||||
|
src-tauri/gen/
|
||||||
6
.prettierrc
Normal file
6
.prettierrc
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"semi": false,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"experimentalOperatorPosition": "start"
|
||||||
|
}
|
||||||
@@ -34,19 +34,27 @@ npm install pnpm -g
|
|||||||
|
|
||||||
### Install Dependencies
|
### Install Dependencies
|
||||||
|
|
||||||
|
Install node packages
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
pnpm install
|
pnpm install
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Install apt packages ONLY for Ubuntu
|
||||||
|
|
||||||
|
```shell
|
||||||
|
apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||||
|
```
|
||||||
|
|
||||||
### Download the Mihomo Core Binary
|
### Download the Mihomo Core Binary
|
||||||
|
|
||||||
You have two options for downloading the clash binary:
|
You have two options for downloading the clash binary:
|
||||||
|
|
||||||
- Automatically download it via the provided script:
|
- Automatically download it via the provided script:
|
||||||
```shell
|
```shell
|
||||||
pnpm run check
|
pnpm run prebuild
|
||||||
# Use '--force' to force update to the latest version
|
# Use '--force' to force update to the latest version
|
||||||
# pnpm run check --force
|
# pnpm run prebuild --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).
|
- 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).
|
||||||
|
|
||||||
@@ -96,6 +104,29 @@ pnpm portable
|
|||||||
|
|
||||||
## Contributing Your Changes
|
## Contributing Your Changes
|
||||||
|
|
||||||
|
#### Before commit your changes
|
||||||
|
|
||||||
|
If you changed the rust code, it's recommanded to execute code style formatting and quailty checks.
|
||||||
|
|
||||||
|
1. Code quailty checks
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# For rust backend
|
||||||
|
$ clash-verge-rev: pnpm clippy
|
||||||
|
# For frontend (not yet).
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Code style formatting
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# For rust backend
|
||||||
|
$ clash-verge-rev: cd src-tauri
|
||||||
|
$ clash-verge-rev/src-tauri: cargo fmt
|
||||||
|
# For frontend
|
||||||
|
$ clash-verge-rev: pnpm format:check
|
||||||
|
$ clash-verge-rev: pnpm format
|
||||||
|
```
|
||||||
|
|
||||||
Once you have made your changes:
|
Once you have made your changes:
|
||||||
|
|
||||||
1. Fork the repository.
|
1. Fork the repository.
|
||||||
|
|||||||
27
README.md
27
README.md
@@ -18,10 +18,18 @@ A Clash Meta GUI based on <a href="https://github.com/tauri-apps/tauri">Tauri</a
|
|||||||
## Install
|
## Install
|
||||||
|
|
||||||
请到发布页面下载对应的安装包:[Release page](https://github.com/clash-verge-rev/clash-verge-rev/releases)<br>
|
请到发布页面下载对应的安装包:[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>
|
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).
|
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/)
|
#### 我应当怎样选择发行版
|
||||||
|
|
||||||
|
| 版本 | 特征 | 链接 |
|
||||||
|
| :-------- | :--------------------------------------- | :------------------------------------------------------------------------------------- |
|
||||||
|
| Stable | 正式版,高可靠性,适合日常使用。 | [Release](https://github.com/clash-verge-rev/clash-verge-rev/releases) |
|
||||||
|
| Alpha | 早期测试版,功能未完善,可能存在缺陷。 | [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) |
|
||||||
|
| AutoBuild | 滚动更新版,持续集成更新,适合开发测试。 | [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) |
|
||||||
|
|
||||||
|
#### 安装说明和常见问题,请到 [文档页](https://clash-verge-rev.github.io/) 查看
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -29,7 +37,7 @@ Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
|
|||||||
|
|
||||||
## Promotion
|
## Promotion
|
||||||
|
|
||||||
[狗狗加速 —— 技术流机场 Doggygo VPN](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
#### [狗狗加速 —— 技术流机场 Doggygo VPN](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||||
|
|
||||||
- 高性能海外机场,免费试用,优惠套餐,解锁流媒体,全球首家支持 Hysteria 协议。
|
- 高性能海外机场,免费试用,优惠套餐,解锁流媒体,全球首家支持 Hysteria 协议。
|
||||||
- 使用 Clash Verge 专属邀请链接注册送 3 天,每天 1G 流量免费试用:[点此注册](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
- 使用 Clash Verge 专属邀请链接注册送 3 天,每天 1G 流量免费试用:[点此注册](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||||
@@ -41,6 +49,19 @@ Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
|
|||||||
- 解锁流媒体及 ChatGPT
|
- 解锁流媒体及 ChatGPT
|
||||||
- 官网:[https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
- 官网:[https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||||
|
|
||||||
|
#### 本项目的构建与发布环境由 [YXVM](https://yxvm.com/aff.php?aff=827) 独立服务器全力支持,
|
||||||
|
|
||||||
|
感谢提供 独享资源、高性能、高速网络 的强大后端环境。如果你觉得下载够快、使用够爽,那是因为我们用了好服务器!
|
||||||
|
|
||||||
|
🧩 YXVM 独立服务器优势:
|
||||||
|
|
||||||
|
- 🌎 优质网络,回程优化,下载快到飞起
|
||||||
|
- 🔧 物理机独享资源,非VPS可比,性能拉满
|
||||||
|
- 🧠 适合跑代理、搭建 WEB 站 CDN 站 、搞 CI/CD 或任何高负载应用
|
||||||
|
- 💡 支持即开即用,多机房选择,CN2 / IEPL 可选
|
||||||
|
- 📦 本项目使用配置已在售,欢迎同款入手!
|
||||||
|
- 🎯 想要同款构建体验?[立即下单 YXVM 独立服务器!](https://yxvm.com/aff.php?aff=827)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- 基于性能强劲的 Rust 和 Tauri 2 框架
|
- 基于性能强劲的 Rust 和 Tauri 2 框架
|
||||||
|
|||||||
338
UPDATELOG.md
338
UPDATELOG.md
@@ -1,3 +1,337 @@
|
|||||||
|
## v2.3.1
|
||||||
|
|
||||||
|
### 🐞 修复问题
|
||||||
|
|
||||||
|
- 增加配置文件校验,修复从古老版本升级上来的"No such file or directory (os error 2)"错误
|
||||||
|
- 修复扩展脚本转义错误
|
||||||
|
- 修复 macOS Intel X86 架构构建错误导致无法运行
|
||||||
|
- 修复 Linux 下界面边框白边问题
|
||||||
|
- 修复 托盘 无响应问题
|
||||||
|
- 修复 托盘 无法从轻量模式退出并恢复窗口
|
||||||
|
- 修复 快速切换订阅可能导致的卡死问题
|
||||||
|
|
||||||
|
### ✨ 新增功能
|
||||||
|
|
||||||
|
- 新增 window-state 窗口状态管理和恢复
|
||||||
|
|
||||||
|
### 🚀 优化改进
|
||||||
|
|
||||||
|
- 优化 托盘 统一响应
|
||||||
|
- 优化 静默启动+自启动轻量模式 运行方式
|
||||||
|
- 升级依赖
|
||||||
|
|
||||||
|
## v2.3.0
|
||||||
|
|
||||||
|
**发行代号:御**
|
||||||
|
代号释义: 「御」,象征掌控与守护,寓意本次版本对系统稳定性、安全性与用户体验的全面驾驭与提升。
|
||||||
|
|
||||||
|
尽管 `external-controller` 密钥现已自动补全默认值且不允许为空,**仍建议手动修改密钥以提高安全性**。
|
||||||
|
|
||||||
|
### ⚠️ 已知问题
|
||||||
|
|
||||||
|
- 仅在 Ubuntu 22.04/24.04、Fedora 41 的 **GNOME 桌面环境** 做过简单测试,不保证其他 Linux 发行版兼容,后续将逐步适配和优化。
|
||||||
|
- macOS:
|
||||||
|
|
||||||
|
- MacOS 下自动升级成功后请关闭程序等待 30 秒重启,因为 MacOS 的端口释放特性,卸载服务后需重启应用等 30 秒才能恢复内核通信。立即启动可能无法正常启动内核。
|
||||||
|
- 墙贴主要为浅色,深色 Tray 图标存在闪烁问题;
|
||||||
|
- 彩色 Tray 图标颜色偏淡;
|
||||||
|
|
||||||
|
- 已确认窗口状态管理器存在上游缺陷,已暂时移除窗口大小与位置记忆功能。
|
||||||
|
|
||||||
|
### 🐞 修复问题
|
||||||
|
|
||||||
|
- 修复首页“代理模式”快速切换导致的卡死问题
|
||||||
|
- 修复 MacOS 快捷键关闭窗口无法启用自动轻量模式
|
||||||
|
- 修复静默启动异常窗口的创建与关闭流程
|
||||||
|
- 修复 Windows 下错误注册的全局快捷键 `Ctrl+Q`
|
||||||
|
- 修复解锁测试报错信息与 VLESS URL 解码时的网络类型错误
|
||||||
|
- 修复切换自定义代理地址后系统代理状态异常
|
||||||
|
- 修复 macOS TUN 默认无效网卡名称
|
||||||
|
- 修复更改订阅后托盘 UI 不同步的问题
|
||||||
|
- 修复服务模式安装后无法立即开启 TUN 模式
|
||||||
|
- 修复无法删除 `.window-state.json`
|
||||||
|
- 修复无法修改配置更新 HTTP 请求超时问题
|
||||||
|
- 修复 `getDelayFix` 钩子异常
|
||||||
|
- 修复外部扩展脚本覆写代理组时首页无法显示代理组
|
||||||
|
- 修复 Verge 导出诊断版本与设置页面不同步
|
||||||
|
- 修复切换语言时设置页面可能加载失败
|
||||||
|
- 修复编辑器中连字符处理问题
|
||||||
|
- 修复提权漏洞,改用带认证的 IPC 通信机制
|
||||||
|
- 修复静默启动无法使用自动轻量模式
|
||||||
|
- 修复 JS 脚本转义特殊字符报错
|
||||||
|
- 修复 macOS 静默启动时异常启动 Dock 栏图标
|
||||||
|
|
||||||
|
### ✨ 新增功能
|
||||||
|
|
||||||
|
- **Mihomo(Meta) 内核升级至 v1.19.10**
|
||||||
|
- 支持设置代理地址为非 `127.0.0.1`,提升 WSL 兼容性
|
||||||
|
- 系统代理守卫:可检测意外变更并自动恢复
|
||||||
|
- 托盘新增当前轻量模式状态显示
|
||||||
|
- 关闭系统代理时同时断开已建立的连接
|
||||||
|
- 新增 WebDAV 功能:
|
||||||
|
|
||||||
|
- 加入 UA 请求头
|
||||||
|
- 支持目录重定向
|
||||||
|
- 备份目录检查与上传重试机制
|
||||||
|
|
||||||
|
- 自动订阅更新机制:
|
||||||
|
|
||||||
|
- 加入请求超时机制防止卡死
|
||||||
|
- 支持在代理状态下自动重试订阅更新
|
||||||
|
- 支持订阅卡片点击切换下次自动更新时间,并显示更新结果提示
|
||||||
|
|
||||||
|
- DNS 设置新增 Hosts 配置功能
|
||||||
|
- 首页代理节点支持排序
|
||||||
|
- 支持服务模式手动卸载,回退至 Sidecar 模式
|
||||||
|
- 核心状态管理支持切换、升级、重启
|
||||||
|
- 配置加载阶段自动补全 `external-controller secret`
|
||||||
|
- 新增日志自动清理周期选项(含1天)
|
||||||
|
- 新增 Zashboard 一键跳转入口
|
||||||
|
- 使用系统默认窗口管理器
|
||||||
|
|
||||||
|
### 🚀 优化改进
|
||||||
|
|
||||||
|
- **系统相关:**
|
||||||
|
|
||||||
|
- 系统代理 Bypass 设置优化
|
||||||
|
- 优化代理设置更新逻辑与守卫机制
|
||||||
|
- Windows 启动方式调整为 Startup 文件夹,解决管理员模式下自启问题
|
||||||
|
|
||||||
|
- **性能与稳定性:**
|
||||||
|
|
||||||
|
- 全面异步化处理配置加载、UI 启动、事件通知等关键流程,解决卡顿问题
|
||||||
|
- 优化 MihomoManager 实现与窗口创建流程
|
||||||
|
- 改进内核日志等级为 `warn`,减少噪音输出
|
||||||
|
- 重构主进程与通知系统,提升响应性与分离度
|
||||||
|
- 优化网络请求与错误处理机制
|
||||||
|
- 添加网络管理器防止资源竞争引发 UI 卡死
|
||||||
|
- 优化配置文件加载内存使用
|
||||||
|
- 优化缓存 Mihomo proxy 和 providers 信息内存使用
|
||||||
|
|
||||||
|
- **前端与界面体验:**
|
||||||
|
|
||||||
|
- 切换规则页自动刷新数据
|
||||||
|
- 非激活订阅编辑时不再触发配置重载
|
||||||
|
- 优化托盘速率显示,macOS 下默认关闭
|
||||||
|
- Windows 快捷键名称更名为 `Clash Verge`
|
||||||
|
- 更新失败可回退至使用代理重试
|
||||||
|
- 支持异步端口查找与保存,端口支持随机生成
|
||||||
|
- 修改端口检测范围至 `1111-65536`
|
||||||
|
- 优化保存机制,使用平滑函数防止卡顿
|
||||||
|
|
||||||
|
- **配置增强与安全性:**
|
||||||
|
|
||||||
|
- 配置缺失 `secret` 字段时自动补全为 `set-your-secret`
|
||||||
|
- 强制为 Mihomo 配置补全 `external-controller-cors` 字段(默认不允许跨域,限制本地访问)计划后续支持自定义 cors
|
||||||
|
- 优化窗口权限设置与状态初始化逻辑
|
||||||
|
- 网络延迟测试替换为 HTTPS 协议:`https://cp.cloudflare.com/generate_204`
|
||||||
|
- 优化 IP 信息获取流程,添加去重机制与轮询检测算法
|
||||||
|
|
||||||
|
- 同步修复翻译错误与不一致项,优化整体语言体验
|
||||||
|
- 加强语言切换后的页面稳定性,避免加载异常
|
||||||
|
|
||||||
|
### 🗑️ 移除内容
|
||||||
|
|
||||||
|
- 窗口状态管理器(上游存在缺陷)
|
||||||
|
- WebDAV 跨平台备份恢复限制
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v2.2.3
|
||||||
|
|
||||||
|
#### 已知问题
|
||||||
|
|
||||||
|
- 仅在Ubuntu 22.04/24.04,Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优
|
||||||
|
- MacOS 自定义图标与速率显示推荐图标尺寸为 256x256。其他尺寸(可能)会导致不正常图标和速率间隙
|
||||||
|
- MacOS 下 墙贴主要为浅色,Tray 图标深色时图标闪烁;彩色 Tray 速率颜色淡
|
||||||
|
- Linux 下 Clash Verge Rev 内存占用显著高于 Windows / MacOS
|
||||||
|
|
||||||
|
### 2.2.3 相对于 2.2.2
|
||||||
|
|
||||||
|
#### 修复了:
|
||||||
|
|
||||||
|
- 首页“当前代理”因为重复刷新导致的CPU占用过高的问题
|
||||||
|
- “开机自启”和“DNS覆写”开关跳动问题
|
||||||
|
- 自定义托盘图标未能应用更改
|
||||||
|
- MacOS 自定义托盘图标显示速率时图标和文本间隙过大
|
||||||
|
- MacOS 托盘速率显示不全
|
||||||
|
- Linux 在系统服务模式下无法拉起 Mihomo 内核
|
||||||
|
- 使用异步操作,避免获取系统信息和切换代理模式可能带来的崩溃
|
||||||
|
- 相同节点名称可能导致的页面渲染出错
|
||||||
|
- URL Schemes被截断的问题
|
||||||
|
- 首页流量统计卡更好的时间戳范围
|
||||||
|
- 静默启动无法触发自动轻量化计时器
|
||||||
|
|
||||||
|
#### 新增了:
|
||||||
|
|
||||||
|
- Mihomo(Meta)内核升级至 1.19.4
|
||||||
|
- Clash Verge Rev 从现在开始不再强依赖系统服务和管理权限
|
||||||
|
- 支持根据用户偏好选择Sidecar(用户空间)模式或安装服务
|
||||||
|
- 增加载入初始配置文件的错误提示,防止切换到错误的订阅配置
|
||||||
|
- 检测是否以管理员模式运行软件,如果是提示无法使用开机自启
|
||||||
|
- 代理组显示节点数量
|
||||||
|
- 统一运行模式检测,支持管理员模式下开启TUN模式
|
||||||
|
- 托盘切换代理模式会根据设置自动断开之前连接
|
||||||
|
- 如订阅获取失败回退使用Clash内核代理再次尝试
|
||||||
|
|
||||||
|
#### 移除了:
|
||||||
|
|
||||||
|
- 实时保存窗口位置和大小。这个功能可能会导致窗口异常大小和位置,还需观察。
|
||||||
|
|
||||||
|
#### 优化了:
|
||||||
|
|
||||||
|
- 重构了后端内核管理逻辑,更轻量化和有效的管理内核,提高了性能和稳定性
|
||||||
|
- 前端统一刷新应用数据,优化数据获取和刷新逻辑
|
||||||
|
- 优化首页流量图表代码,调整图表文字边距
|
||||||
|
- MacOS 托盘速率更好的显示样式和更新逻辑
|
||||||
|
- 首页仅在有流量图表时显示流量图表区域
|
||||||
|
- 更新DNS默认覆写配置
|
||||||
|
- 移除测试目录,简化资源初始化逻辑
|
||||||
|
|
||||||
|
## v2.2.2
|
||||||
|
|
||||||
|
**发行代号:拓**
|
||||||
|
|
||||||
|
感谢 Tunglies 对 Verge 后端重构,性能优化做出的重大贡献!
|
||||||
|
|
||||||
|
代号释义: 本次发布在功能上的大幅扩展。新首页设计为用户带来全新交互体验,DNS 覆写功能增强网络控制能力,解锁测试页面助力内容访问自由度提升,轻量模式提供灵活使用选择。此外,macOS 应用菜单集成、sidecar 模式、诊断信息导出等新特性进一步丰富了软件的适用场景。这些新增功能显著拓宽了 Clash Verge 的功能边界,为用户提供了更强大的工具和可能性。
|
||||||
|
|
||||||
|
#### 已知问题
|
||||||
|
|
||||||
|
- 仅在Ubuntu 22.04/24.04,Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优
|
||||||
|
|
||||||
|
### 2.2.2 相对于 2.2.1(已下架不再提供)
|
||||||
|
|
||||||
|
#### 修复了:
|
||||||
|
|
||||||
|
- 弹黑框的问题(原因是服务崩溃触发重装机制)
|
||||||
|
- MacOS进入轻量模式以后隐藏Dock图标
|
||||||
|
- 增加轻量模式缺失的tray翻译
|
||||||
|
- Linux下的窗口边框被削掉的问题
|
||||||
|
|
||||||
|
#### 新增了:
|
||||||
|
|
||||||
|
- 加强服务检测和重装逻辑
|
||||||
|
- 增强内核与服务保活机制
|
||||||
|
- 增加服务模式下的僵尸进程清理机制
|
||||||
|
- 新增当服务模式多次尝试失败后自动回退至用户空间模式
|
||||||
|
|
||||||
|
### 2.2.1 相对于 2.2.0(已下架不再提供)
|
||||||
|
|
||||||
|
#### 修复了:
|
||||||
|
|
||||||
|
1. **首页**
|
||||||
|
- 修复 Direct 模式首页无法渲染
|
||||||
|
- 修复 首页启用轻量模式导致 ClashVergeRev 从托盘退出
|
||||||
|
- 修复 系统代理标识判断不准的问题
|
||||||
|
- 修复 系统代理地址错误的问题
|
||||||
|
- 代理模式“多余的切换动画”
|
||||||
|
2. **系统**
|
||||||
|
- 修复 MacOS 无法使用快捷键粘贴/选择/复制订阅地址。
|
||||||
|
- 修复 代理端口设置同步问题。
|
||||||
|
- 修复 Linux 无法与 Mihomo 核心 和 ClashVergeRev 服务通信
|
||||||
|
3. **界面**
|
||||||
|
- 修复 连接详情卡没有跟随主题色
|
||||||
|
4. **轻量模式**
|
||||||
|
- 修复 MacOS 轻量模式下 Dock 栏图标无法隐藏。
|
||||||
|
|
||||||
|
#### 新增了:
|
||||||
|
|
||||||
|
1. **首页**
|
||||||
|
- 首页文本过长自动截断
|
||||||
|
2. **轻量模式**
|
||||||
|
- 新增托盘进入轻量模式支持
|
||||||
|
- 新增进入轻量模式快捷键支持
|
||||||
|
3. **系统**
|
||||||
|
- 在 ClashVergeRev 对 Mihomo 进行操作时,总是尝试确保两者运行
|
||||||
|
- 服务器模式下启动mihomo内核的时候查找并停止其他已经存在的内核进程,防止内核假死等问题带来的通信失败
|
||||||
|
4. **托盘**
|
||||||
|
- 新增 MacOS 启用托盘速率显示时,可选隐藏托盘图标显示
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2.2.0(已下架不再提供)
|
||||||
|
|
||||||
|
#### 新增功能
|
||||||
|
|
||||||
|
1. **首页**
|
||||||
|
|
||||||
|
- 新增首页功能,默认启动页面改为首页。
|
||||||
|
- 首页流量图卡片显示上传/下载名称。
|
||||||
|
- 首页支持轻量模式切换。
|
||||||
|
- 流量统计数据持久保存。
|
||||||
|
- 限制首页配置文件卡片URL长度。
|
||||||
|
|
||||||
|
2. **DNS 设置与覆写**
|
||||||
|
|
||||||
|
- 新增 DNS 覆写功能。
|
||||||
|
- 默认启用 DNS 覆写。
|
||||||
|
|
||||||
|
3. **解锁测试**
|
||||||
|
|
||||||
|
- 新增解锁测试页面。
|
||||||
|
|
||||||
|
4. **轻量模式**
|
||||||
|
|
||||||
|
- 新增轻量模式及设置。
|
||||||
|
- 添加自动轻量模式定时器。
|
||||||
|
|
||||||
|
5. **系统支持**
|
||||||
|
|
||||||
|
- Mihomo(meta)内核升级 1.19.3
|
||||||
|
- macOS 支持 CMD+W 关闭窗口。
|
||||||
|
- 新增 macOS 应用菜单。
|
||||||
|
- 添加 macOS 安装服务时候的管理员权限提示。
|
||||||
|
- 新增 sidecar(用户空间启动内核) 模式。
|
||||||
|
|
||||||
|
6. **其他**
|
||||||
|
- 增强延迟测试日志和错误处理。
|
||||||
|
- 添加诊断信息导出。
|
||||||
|
- 新增代理命令。
|
||||||
|
|
||||||
|
#### 修复
|
||||||
|
|
||||||
|
1. **系统**
|
||||||
|
|
||||||
|
- 修复 Windows 热键崩溃。
|
||||||
|
- 修复 macOS 无框标题。
|
||||||
|
- 修复 macOS 静默启动崩溃。
|
||||||
|
- 修复 macOS tray图标错位到左上角的问题。
|
||||||
|
- 修复 Windows/Linux 运行时崩溃。
|
||||||
|
- 修复 Win10 阴影和边框问题。
|
||||||
|
- 修复 升级或重装后开机自启状态检测和同步问题。
|
||||||
|
|
||||||
|
2. **构建**
|
||||||
|
- 修复构建失败问题。
|
||||||
|
|
||||||
|
#### 优化
|
||||||
|
|
||||||
|
1. **性能**
|
||||||
|
|
||||||
|
- 重构后端,巨幅性能优化。
|
||||||
|
- 优化首页组件性能。
|
||||||
|
- 优化流量图表资源使用。
|
||||||
|
- 提升代理组列表滚动性能。
|
||||||
|
- 加快应用退出速度。
|
||||||
|
- 加快进入轻量模式速度。
|
||||||
|
- 优化小数值速度更新。
|
||||||
|
- 增加请求超时至 60 秒。
|
||||||
|
- 修复代理节点选择同步。
|
||||||
|
- 优化修改verge配置性能。
|
||||||
|
|
||||||
|
2. **重构**
|
||||||
|
|
||||||
|
- 重构后端,巨幅性能优化。
|
||||||
|
- 优化定时器管理。
|
||||||
|
- 重构 MihomoManager 处理流量。
|
||||||
|
- 优化 WebSocket 连接。
|
||||||
|
|
||||||
|
3. **其他**
|
||||||
|
- 更新依赖。
|
||||||
|
- 默认 TUN 堆栈改为 gvisor。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v2.1.2
|
## v2.1.2
|
||||||
|
|
||||||
**发行代号:臻**
|
**发行代号:臻**
|
||||||
@@ -6,14 +340,14 @@
|
|||||||
|
|
||||||
感谢 Tychristine 对社区群组管理做出的重大贡献!
|
感谢 Tychristine 对社区群组管理做出的重大贡献!
|
||||||
|
|
||||||
##### 2.1.2相对2.1.1(已下架不在提供)更新了:
|
##### 2.1.2相对2.1.1(已下架不再提供)更新了:
|
||||||
|
|
||||||
- 无法更新和签名验证失败的问题(该死的CDN缓存)
|
- 无法更新和签名验证失败的问题(该死的CDN缓存)
|
||||||
- 设置菜单区分Verge基本设置和高级设置
|
- 设置菜单区分Verge基本设置和高级设置
|
||||||
- 增加v2 Updater的更多功能和权限
|
- 增加v2 Updater的更多功能和权限
|
||||||
- 退出Verge后Tun代理状态仍保留的问题
|
- 退出Verge后Tun代理状态仍保留的问题
|
||||||
|
|
||||||
##### 2.1.1相对2.1.0(已下架不在提供)更新了:
|
##### 2.1.1相对2.1.0(已下架不再提供)更新了:
|
||||||
|
|
||||||
- 检测所需的Clash Verge Service版本(杀毒软件误报可能与此有关,因为检测和安装新版本Service需管理员权限)
|
- 检测所需的Clash Verge Service版本(杀毒软件误报可能与此有关,因为检测和安装新版本Service需管理员权限)
|
||||||
- MacOS下支持彩色托盘图标和更好速率显示(感谢Tunglies)
|
- MacOS下支持彩色托盘图标和更好速率显示(感谢Tunglies)
|
||||||
|
|||||||
4
crowdin.yml
Normal file
4
crowdin.yml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
files:
|
||||||
|
- source: /src/locales/en.json
|
||||||
|
translation: /src/locales
|
||||||
|
multilingual: 1
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 314 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 274 KiB |
116
package.json
116
package.json
@@ -1,24 +1,28 @@
|
|||||||
{
|
{
|
||||||
"name": "clash-verge",
|
"name": "clash-verge",
|
||||||
"version": "2.1.3-alpha",
|
"version": "2.3.1",
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev -- --profile fast-dev",
|
"dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev",
|
||||||
"dev:diff": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev -- --profile fast-dev",
|
"dev:diff": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev",
|
||||||
"build": "cross-env NODE_OPTIONS='--max-old-space-size=4096' tauri build",
|
"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",
|
"build:fast": "cross-env NODE_OPTIONS='--max-old-space-size=4096' tauri build -- --profile fast-release",
|
||||||
"tauri": "tauri",
|
"tauri": "tauri",
|
||||||
"web:dev": "vite",
|
"web:dev": "vite",
|
||||||
"web:build": "tsc --noEmit && vite build",
|
"web:build": "tsc --noEmit && vite build",
|
||||||
"web:serve": "vite preview",
|
"web:serve": "vite preview",
|
||||||
"check": "node scripts/check.mjs",
|
"prebuild": "node scripts/prebuild.mjs",
|
||||||
"updater": "node scripts/updater.mjs",
|
"updater": "node scripts/updater.mjs",
|
||||||
"updater-fixed-webview2": "node scripts/updater-fixed-webview2.mjs",
|
"updater-fixed-webview2": "node scripts/updater-fixed-webview2.mjs",
|
||||||
"portable": "node scripts/portable.mjs",
|
"portable": "node scripts/portable.mjs",
|
||||||
"portable-fixed-webview2": "node scripts/portable-fixed-webview2.mjs",
|
"portable-fixed-webview2": "node scripts/portable-fixed-webview2.mjs",
|
||||||
"fix-alpha-version": "node scripts/alpha_version.mjs",
|
"fix-alpha-version": "node scripts/fix-alpha_version.mjs",
|
||||||
"prepare": "husky",
|
"release-version": "node scripts/release-version.mjs",
|
||||||
"clean": "cd ./src-tauri && cargo clean && cd -"
|
"publish-version": "node scripts/publish-version.mjs",
|
||||||
|
"fmt": "cargo fmt --manifest-path ./src-tauri/Cargo.toml",
|
||||||
|
"clippy": "cargo clippy --manifest-path ./src-tauri/Cargo.toml",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"format:check": "prettier --check ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
@@ -27,73 +31,77 @@
|
|||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.0",
|
"@emotion/styled": "^11.14.0",
|
||||||
"@juggle/resize-observer": "^3.4.0",
|
"@juggle/resize-observer": "^3.4.0",
|
||||||
"@mui/icons-material": "^6.4.2",
|
"@mui/icons-material": "^7.1.1",
|
||||||
"@mui/lab": "6.0.0-beta.25",
|
"@mui/lab": "7.0.0-beta.13",
|
||||||
"@mui/material": "^6.4.2",
|
"@mui/material": "^7.1.1",
|
||||||
"@mui/x-data-grid": "^7.25.0",
|
"@mui/x-data-grid": "^8.5.2",
|
||||||
"@tauri-apps/api": "2.2.0",
|
"@tauri-apps/api": "2.5.0",
|
||||||
"@tauri-apps/plugin-clipboard-manager": "^2.2.1",
|
"@tauri-apps/plugin-clipboard-manager": "^2.2.3",
|
||||||
"@tauri-apps/plugin-dialog": "^2.2.0",
|
"@tauri-apps/plugin-dialog": "^2.2.2",
|
||||||
"@tauri-apps/plugin-fs": "^2.2.0",
|
"@tauri-apps/plugin-fs": "^2.3.0",
|
||||||
"@tauri-apps/plugin-global-shortcut": "^2.2.0",
|
"@tauri-apps/plugin-global-shortcut": "^2.2.1",
|
||||||
"@tauri-apps/plugin-notification": "^2.2.1",
|
"@tauri-apps/plugin-notification": "^2.2.3",
|
||||||
"@tauri-apps/plugin-process": "^2.2.0",
|
"@tauri-apps/plugin-process": "^2.2.2",
|
||||||
"@tauri-apps/plugin-shell": "2.2.0",
|
"@tauri-apps/plugin-shell": "2.2.2",
|
||||||
"@tauri-apps/plugin-updater": "2.3.0",
|
"@tauri-apps/plugin-updater": "2.8.1",
|
||||||
|
"@tauri-apps/plugin-window-state": "^2.2.3",
|
||||||
|
"@types/d3-shape": "^3.1.7",
|
||||||
"@types/json-schema": "^7.0.15",
|
"@types/json-schema": "^7.0.15",
|
||||||
"ahooks": "^3.8.4",
|
"ahooks": "^3.8.5",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.10.0",
|
||||||
|
"chart.js": "^4.5.0",
|
||||||
"cli-color": "^2.0.4",
|
"cli-color": "^2.0.4",
|
||||||
|
"d3-shape": "^3.2.0",
|
||||||
"dayjs": "1.11.13",
|
"dayjs": "1.11.13",
|
||||||
"foxact": "^0.2.43",
|
"foxact": "^0.2.49",
|
||||||
"glob": "^11.0.1",
|
"glob": "^11.0.3",
|
||||||
"i18next": "^24.2.2",
|
"i18next": "^25.2.1",
|
||||||
"js-base64": "^3.7.7",
|
"js-base64": "^3.7.7",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"monaco-editor": "^0.52.2",
|
"monaco-editor": "^0.52.2",
|
||||||
"monaco-yaml": "^5.2.3",
|
"monaco-yaml": "^5.4.0",
|
||||||
"nanoid": "^5.0.9",
|
"nanoid": "^5.1.5",
|
||||||
"peggy": "^4.2.0",
|
"peggy": "^5.0.3",
|
||||||
"react": "^18.3.1",
|
"react": "19.1.0",
|
||||||
"react-dom": "^18.3.1",
|
"react-chartjs-2": "^5.3.0",
|
||||||
"react-error-boundary": "^4.0.12",
|
"react-dom": "19.1.0",
|
||||||
"react-hook-form": "^7.54.2",
|
"react-error-boundary": "6.0.0",
|
||||||
"react-i18next": "^13.5.0",
|
"react-hook-form": "^7.58.1",
|
||||||
"react-markdown": "^9.0.1",
|
"react-i18next": "15.5.3",
|
||||||
"react-monaco-editor": "^0.56.0",
|
"react-markdown": "10.1.0",
|
||||||
"react-router-dom": "^6.22.3",
|
"react-monaco-editor": "0.58.0",
|
||||||
"react-transition-group": "^4.4.5",
|
"react-router-dom": "7.6.2",
|
||||||
"react-virtuoso": "^4.6.3",
|
"react-virtuoso": "^4.13.0",
|
||||||
"sockette": "^2.0.6",
|
"sockette": "^2.0.6",
|
||||||
"swr": "^2.3.0",
|
"swr": "^2.3.3",
|
||||||
"tar": "^7.4.3",
|
"tar": "^7.4.3",
|
||||||
"types-pac": "^1.0.3",
|
"types-pac": "^1.0.3",
|
||||||
"zustand": "^5.0.3"
|
"zustand": "^5.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@actions/github": "^6.0.0",
|
"@actions/github": "^6.0.1",
|
||||||
"@tauri-apps/cli": "2.2.7",
|
"@tauri-apps/cli": "2.5.0",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/react": "^18.3.18",
|
"@types/react": "19.1.8",
|
||||||
"@types/react-dom": "^18.3.5",
|
"@types/react-dom": "19.1.6",
|
||||||
"@types/react-transition-group": "^4.4.12",
|
"@vitejs/plugin-legacy": "^6.1.1",
|
||||||
"@vitejs/plugin-legacy": "^6.0.0",
|
"@vitejs/plugin-react": "4.5.2",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
|
"commander": "^14.0.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"https-proxy-agent": "^7.0.6",
|
"https-proxy-agent": "^7.0.6",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"meta-json-schema": "^1.19.1",
|
"meta-json-schema": "^1.19.10",
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.5.3",
|
||||||
"pretty-quick": "^4.0.0",
|
"pretty-quick": "^4.2.2",
|
||||||
"sass": "^1.83.4",
|
"sass": "^1.89.2",
|
||||||
"terser": "^5.37.0",
|
"terser": "^5.43.0",
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^5.8.3",
|
||||||
"vite": "^6.0.11",
|
"vite": "^6.3.5",
|
||||||
"vite-plugin-monaco-editor": "^1.1.0",
|
"vite-plugin-monaco-editor": "^1.1.0",
|
||||||
"vite-plugin-svgr": "^4.3.0"
|
"vite-plugin-svgr": "^4.3.0"
|
||||||
},
|
},
|
||||||
|
|||||||
8059
pnpm-lock.yaml
generated
8059
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
42
renovate.json
Normal file
42
renovate.json
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"extends": ["config:recommended"],
|
||||||
|
"baseBranches": ["dev"],
|
||||||
|
"enabledManagers": ["cargo", "npm"],
|
||||||
|
"labels": ["dependencies"],
|
||||||
|
"ignorePaths": [
|
||||||
|
"**/node_modules/**",
|
||||||
|
"**/bower_components/**",
|
||||||
|
"**/vendor/**",
|
||||||
|
"**/__tests__/**",
|
||||||
|
"**/test/**",
|
||||||
|
"**/tests/**",
|
||||||
|
"**/__fixtures__/**",
|
||||||
|
"**/crate/**",
|
||||||
|
"shared/**"
|
||||||
|
],
|
||||||
|
"rangeStrategy": "bump",
|
||||||
|
"packageRules": [
|
||||||
|
{
|
||||||
|
"semanticCommitType": "chore",
|
||||||
|
"matchPackageNames": ["*"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Disable node/pnpm version updates",
|
||||||
|
"matchPackageNames": ["node", "pnpm"],
|
||||||
|
"matchDepTypes": ["engines", "packageManager"],
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Group all cargo dependencies into a single PR",
|
||||||
|
"matchManagers": ["cargo"],
|
||||||
|
"groupName": "cargo dependencies"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Group all npm dependencies into a single PR",
|
||||||
|
"matchManagers": ["npm"],
|
||||||
|
"groupName": "npm dependencies"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"postUpdateOptions": ["pnpmDedupe"],
|
||||||
|
"ignoreDeps": ["serde_yaml"]
|
||||||
|
}
|
||||||
102
scripts/check-unused-i18n.js
Normal file
102
scripts/check-unused-i18n.js
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
const LOCALES_DIR = path.resolve(__dirname, "../src/locales");
|
||||||
|
const SRC_DIRS = [
|
||||||
|
path.resolve(__dirname, "../src"),
|
||||||
|
path.resolve(__dirname, "../src-tauri"),
|
||||||
|
];
|
||||||
|
const exts = [".js", ".ts", ".tsx", ".jsx", ".vue", ".rs"];
|
||||||
|
|
||||||
|
// 递归获取所有文件
|
||||||
|
function getAllFiles(dir, exts) {
|
||||||
|
let files = [];
|
||||||
|
fs.readdirSync(dir).forEach((file) => {
|
||||||
|
const full = path.join(dir, file);
|
||||||
|
if (fs.statSync(full).isDirectory()) {
|
||||||
|
files = files.concat(getAllFiles(full, exts));
|
||||||
|
} else if (exts.includes(path.extname(full))) {
|
||||||
|
files.push(full);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取所有源码内容为一个大字符串
|
||||||
|
function getAllSourceContent() {
|
||||||
|
const files = SRC_DIRS.flatMap((dir) => getAllFiles(dir, exts));
|
||||||
|
return files.map((f) => fs.readFileSync(f, "utf8")).join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 白名单 key,不检查这些 key 是否被使用
|
||||||
|
const WHITELIST_KEYS = [
|
||||||
|
"theme.light",
|
||||||
|
"theme.dark",
|
||||||
|
"theme.system",
|
||||||
|
"Already Using Latest Core Version",
|
||||||
|
];
|
||||||
|
|
||||||
|
// 主流程
|
||||||
|
function processI18nFile(i18nPath, lang, allSource) {
|
||||||
|
const i18n = JSON.parse(fs.readFileSync(i18nPath, "utf8"));
|
||||||
|
const keys = Object.keys(i18n);
|
||||||
|
|
||||||
|
const used = {};
|
||||||
|
const unused = [];
|
||||||
|
|
||||||
|
let checked = 0;
|
||||||
|
const total = keys.length;
|
||||||
|
keys.forEach((key) => {
|
||||||
|
if (WHITELIST_KEYS.includes(key)) {
|
||||||
|
used[key] = i18n[key];
|
||||||
|
} else {
|
||||||
|
// 只查找一次
|
||||||
|
const regex = new RegExp(`["'\`]${key}["'\`]`);
|
||||||
|
if (regex.test(allSource)) {
|
||||||
|
used[key] = i18n[key];
|
||||||
|
} else {
|
||||||
|
unused.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
checked++;
|
||||||
|
if (checked % 20 === 0 || checked === total) {
|
||||||
|
const percent = ((checked / total) * 100).toFixed(1);
|
||||||
|
process.stdout.write(
|
||||||
|
`\r[${lang}] Progress: ${checked}/${total} (${percent}%)`,
|
||||||
|
);
|
||||||
|
if (checked === total) process.stdout.write("\n");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 输出未使用的 key
|
||||||
|
console.log(`\n[${lang}] Unused keys:`, unused);
|
||||||
|
|
||||||
|
// 备份原文件
|
||||||
|
const oldPath = i18nPath + ".old";
|
||||||
|
fs.renameSync(i18nPath, oldPath);
|
||||||
|
|
||||||
|
// 写入精简后的 i18n 文件(保留原文件名)
|
||||||
|
fs.writeFileSync(i18nPath, JSON.stringify(used, null, 2), "utf8");
|
||||||
|
console.log(
|
||||||
|
`[${lang}] Cleaned i18n file written to src/locales/${path.basename(i18nPath)}`,
|
||||||
|
);
|
||||||
|
console.log(`[${lang}] Original file backed up as ${path.basename(oldPath)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
// 支持 zhtw.json、zh-tw.json、zh_CN.json 等
|
||||||
|
const files = fs
|
||||||
|
.readdirSync(LOCALES_DIR)
|
||||||
|
.filter((f) => /^[a-z0-9\-_]+\.json$/i.test(f) && !f.endsWith(".old"));
|
||||||
|
const allSource = getAllSourceContent();
|
||||||
|
files.forEach((file) => {
|
||||||
|
const lang = path.basename(file, ".json");
|
||||||
|
processI18nFile(path.join(LOCALES_DIR, file), lang, allSource);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
@@ -38,6 +38,17 @@ async function updatePackageVersion(newVersion) {
|
|||||||
const packageJson = JSON.parse(data);
|
const packageJson = JSON.parse(data);
|
||||||
// 获取键值替换
|
// 获取键值替换
|
||||||
let result = packageJson.version.replace("alpha", newVersion);
|
let result = packageJson.version.replace("alpha", newVersion);
|
||||||
|
// 检查当前版本号是否已经包含了 alpha- 后缀
|
||||||
|
if (!packageJson.version.includes(`alpha-`)) {
|
||||||
|
// 如果只有 alpha 而没有 alpha-,则替换为 alpha-newVersion
|
||||||
|
result = packageJson.version.replace("alpha", `alpha-${newVersion}`);
|
||||||
|
} else {
|
||||||
|
// 如果已经是 alpha-xxx 格式,则更新 xxx 部分
|
||||||
|
result = packageJson.version.replace(
|
||||||
|
/alpha-[^-]*/,
|
||||||
|
`alpha-${newVersion}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
console.log("[INFO]: Current version is: ", result);
|
console.log("[INFO]: Current version is: ", result);
|
||||||
packageJson.version = result;
|
packageJson.version = result;
|
||||||
// 写入版本号
|
// 写入版本号
|
||||||
66
scripts/publish-version.mjs
Normal file
66
scripts/publish-version.mjs
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
// scripts/publish-version.mjs
|
||||||
|
import { spawn } from "child_process";
|
||||||
|
import { existsSync } from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
const rootDir = process.cwd();
|
||||||
|
const scriptPath = path.join(rootDir, "scripts", "release-version.mjs");
|
||||||
|
|
||||||
|
if (!existsSync(scriptPath)) {
|
||||||
|
console.error("release-version.mjs not found!");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const versionArg = process.argv[2];
|
||||||
|
if (!versionArg) {
|
||||||
|
console.error("Usage: pnpm publish-version <version>");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 调用 release-version.mjs
|
||||||
|
const runRelease = () =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
const child = spawn("node", [scriptPath, versionArg], { stdio: "inherit" });
|
||||||
|
child.on("exit", (code) => {
|
||||||
|
if (code === 0) resolve();
|
||||||
|
else reject(new Error("release-version failed"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. 判断是否需要打 tag
|
||||||
|
function isSemver(version) {
|
||||||
|
return /^v?\d+\.\d+\.\d+(-[0-9A-Za-z-.]+)?$/.test(version);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
await runRelease();
|
||||||
|
|
||||||
|
let tag = null;
|
||||||
|
if (versionArg === "alpha") {
|
||||||
|
// 读取 package.json 里的主版本
|
||||||
|
const pkg = await import(path.join(rootDir, "package.json"), {
|
||||||
|
assert: { type: "json" },
|
||||||
|
});
|
||||||
|
tag = `v${pkg.default.version}-alpha`;
|
||||||
|
} else if (isSemver(versionArg)) {
|
||||||
|
// 1.2.3 或 v1.2.3
|
||||||
|
tag = versionArg.startsWith("v") ? versionArg : `v${versionArg}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tag) {
|
||||||
|
// 打 tag 并推送
|
||||||
|
const { execSync } = await import("child_process");
|
||||||
|
try {
|
||||||
|
execSync(`git tag ${tag}`, { stdio: "inherit" });
|
||||||
|
execSync(`git push origin ${tag}`, { stdio: "inherit" });
|
||||||
|
console.log(`[INFO]: Git tag ${tag} created and pushed.`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`[ERROR]: Failed to create or push git tag: ${tag}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("[INFO]: No git tag created for this version.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
||||||
253
scripts/release-version.mjs
Normal file
253
scripts/release-version.mjs
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
/**
|
||||||
|
* CLI tool to update version numbers in package.json, src-tauri/Cargo.toml, and src-tauri/tauri.conf.json.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* pnpm release-version <version>
|
||||||
|
*
|
||||||
|
* <version> can be:
|
||||||
|
* - A full semver version (e.g., 1.2.3, v1.2.3, 1.2.3-beta, v1.2.3+build)
|
||||||
|
* - A tag: "alpha", "beta", "rc", or "autobuild"
|
||||||
|
* - "alpha", "beta", "rc": Appends the tag to the current base version (e.g., 1.2.3-beta)
|
||||||
|
* - "autobuild": Appends a timestamped autobuild tag (e.g., 1.2.3+autobuild.2406101530)
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
* pnpm release-version 1.2.3
|
||||||
|
* pnpm release-version v1.2.3-beta
|
||||||
|
* pnpm release-version beta
|
||||||
|
* pnpm release-version autobuild
|
||||||
|
*
|
||||||
|
* The script will:
|
||||||
|
* - Validate and normalize the version argument
|
||||||
|
* - Update the version field in package.json
|
||||||
|
* - Update the version field in src-tauri/Cargo.toml
|
||||||
|
* - Update the version field in src-tauri/tauri.conf.json
|
||||||
|
*
|
||||||
|
* Errors are logged and the process exits with code 1 on failure.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from "fs/promises";
|
||||||
|
import path from "path";
|
||||||
|
import { program } from "commander";
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前 git 短 commit hash
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function getGitShortCommit() {
|
||||||
|
try {
|
||||||
|
return execSync("git rev-parse --short HEAD").toString().trim();
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("[WARN]: Failed to get git short commit, fallback to 'nogit'");
|
||||||
|
return "nogit";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成短时间戳(格式:YYMMDD)或带 commit(格式:YYMMDD.cc39b27)
|
||||||
|
* @param {boolean} withCommit 是否带 commit
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function generateShortTimestamp(withCommit = false) {
|
||||||
|
const now = new Date();
|
||||||
|
const month = String(now.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(now.getDate()).padStart(2, "0");
|
||||||
|
if (withCommit) {
|
||||||
|
const gitShort = getGitShortCommit();
|
||||||
|
return `${month}${day}.${gitShort}`;
|
||||||
|
}
|
||||||
|
return `${month}${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证版本号格式
|
||||||
|
* @param {string} version
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function isValidVersion(version) {
|
||||||
|
return /^v?\d+\.\d+\.\d+(-(alpha|beta|rc)(\.\d+)?)?(\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*)?$/i.test(
|
||||||
|
version,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标准化版本号
|
||||||
|
* @param {string} version
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function normalizeVersion(version) {
|
||||||
|
return version.startsWith("v") ? version : `v${version}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提取基础版本号(去掉所有 -tag 和 +build 部分)
|
||||||
|
* @param {string} version
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function getBaseVersion(version) {
|
||||||
|
let base = version.replace(/-(alpha|beta|rc)(\.\d+)?/i, "");
|
||||||
|
base = base.replace(/\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*/g, "");
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新 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}`,
|
||||||
|
);
|
||||||
|
} 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 baseVersion = getBaseVersion(versionWithoutV);
|
||||||
|
|
||||||
|
const updatedLines = lines.map((line) => {
|
||||||
|
if (line.trim().startsWith("version =")) {
|
||||||
|
return line.replace(
|
||||||
|
/version\s*=\s*"[^"]+"/,
|
||||||
|
`version = "${baseVersion}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return line;
|
||||||
|
});
|
||||||
|
|
||||||
|
await fs.writeFile(cargoTomlPath, updatedLines.join("\n"), "utf8");
|
||||||
|
console.log(`[INFO]: Cargo.toml version updated to: ${baseVersion}`);
|
||||||
|
} 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;
|
||||||
|
const baseVersion = getBaseVersion(versionWithoutV);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"[INFO]: Current tauri.conf.json version is: ",
|
||||||
|
tauriConfig.version,
|
||||||
|
);
|
||||||
|
tauriConfig.version = baseVersion;
|
||||||
|
await fs.writeFile(
|
||||||
|
tauriConfigPath,
|
||||||
|
JSON.stringify(tauriConfig, null, 2),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
console.log(`[INFO]: tauri.conf.json version updated to: ${baseVersion}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating tauri.conf.json version:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前版本号
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主函数
|
||||||
|
*/
|
||||||
|
async function main(versionArg) {
|
||||||
|
if (!versionArg) {
|
||||||
|
console.error("Error: Version argument is required");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let newVersion;
|
||||||
|
const validTags = ["alpha", "beta", "rc", "autobuild"];
|
||||||
|
|
||||||
|
if (validTags.includes(versionArg.toLowerCase())) {
|
||||||
|
const currentVersion = await getCurrentVersion();
|
||||||
|
const baseVersion = getBaseVersion(currentVersion);
|
||||||
|
|
||||||
|
if (versionArg.toLowerCase() === "autobuild") {
|
||||||
|
// 格式: 2.3.0+autobuild.250613.cc39b27
|
||||||
|
newVersion = `${baseVersion}+autobuild.${generateShortTimestamp(true)}`;
|
||||||
|
} else {
|
||||||
|
newVersion = `${baseVersion}-${versionArg.toLowerCase()}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!isValidVersion(versionArg)) {
|
||||||
|
console.error("Error: Invalid version format");
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
program
|
||||||
|
.name("pnpm release-version")
|
||||||
|
.description("Update project version numbers")
|
||||||
|
.argument("<version>", "version tag or full version")
|
||||||
|
.action(main)
|
||||||
|
.parse(process.argv);
|
||||||
@@ -43,3 +43,42 @@ export async function resolveUpdateLog(tag) {
|
|||||||
|
|
||||||
return map[tag].join("\n").trim();
|
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();
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import fetch from "node-fetch";
|
import fetch from "node-fetch";
|
||||||
import { getOctokit, context } from "@actions/github";
|
import { getOctokit, context } from "@actions/github";
|
||||||
import { resolveUpdateLog } from "./updatelog.mjs";
|
import { resolveUpdateLog, resolveUpdateLogDefault } from "./updatelog.mjs";
|
||||||
|
|
||||||
// Add stable update JSON filenames
|
// Add stable update JSON filenames
|
||||||
const UPDATE_TAG_NAME = "updater";
|
const UPDATE_TAG_NAME = "updater";
|
||||||
@@ -8,8 +8,8 @@ const UPDATE_JSON_FILE = "update.json";
|
|||||||
const UPDATE_JSON_PROXY = "update-proxy.json";
|
const UPDATE_JSON_PROXY = "update-proxy.json";
|
||||||
// Add alpha update JSON filenames
|
// Add alpha update JSON filenames
|
||||||
const ALPHA_TAG_NAME = "updater-alpha";
|
const ALPHA_TAG_NAME = "updater-alpha";
|
||||||
const ALPHA_UPDATE_JSON_FILE = "update-alpha.json";
|
const ALPHA_UPDATE_JSON_FILE = "update.json";
|
||||||
const ALPHA_UPDATE_JSON_PROXY = "update-alpha-proxy.json";
|
const ALPHA_UPDATE_JSON_PROXY = "update-proxy.json";
|
||||||
|
|
||||||
/// generate update.json
|
/// generate update.json
|
||||||
/// upload to update tag's release asset
|
/// upload to update tag's release asset
|
||||||
@@ -78,192 +78,235 @@ async function resolveUpdater() {
|
|||||||
async function processRelease(github, options, tag, isAlpha) {
|
async function processRelease(github, options, tag, isAlpha) {
|
||||||
if (!tag) return;
|
if (!tag) return;
|
||||||
|
|
||||||
const { data: release } = await github.rest.repos.getReleaseByTag({
|
|
||||||
...options,
|
|
||||||
tag: tag.name,
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateData = {
|
|
||||||
name: tag.name,
|
|
||||||
notes: await resolveUpdateLog(tag.name).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 {
|
try {
|
||||||
const { data: updateRelease } = await github.rest.repos.getReleaseByTag({
|
const { data: release } = await github.rest.repos.getReleaseByTag({
|
||||||
...options,
|
...options,
|
||||||
tag: releaseTag,
|
tag: tag.name,
|
||||||
});
|
});
|
||||||
|
|
||||||
// File names based on release type
|
const updateData = {
|
||||||
const jsonFile = isAlpha ? ALPHA_UPDATE_JSON_FILE : UPDATE_JSON_FILE;
|
name: tag.name,
|
||||||
const proxyFile = isAlpha ? ALPHA_UPDATE_JSON_PROXY : UPDATE_JSON_PROXY;
|
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: "" },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
// Delete existing assets with these names
|
const promises = release.assets.map(async (asset) => {
|
||||||
for (let asset of updateRelease.assets) {
|
const { name, browser_download_url } = asset;
|
||||||
if (asset.name === jsonFile) {
|
|
||||||
await github.rest.repos.deleteReleaseAsset({
|
// Process all the platform URL and signature data
|
||||||
...options,
|
// win64 url
|
||||||
asset_id: asset.id,
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (asset.name === proxyFile) {
|
// win32 url
|
||||||
await github.rest.repos
|
if (name.endsWith("x86-setup.exe")) {
|
||||||
.deleteReleaseAsset({ ...options, asset_id: asset.id })
|
updateData.platforms["windows-x86"].url = browser_download_url;
|
||||||
.catch(console.error); // do not break the pipeline
|
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;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Upload new assets
|
// win arm url
|
||||||
await github.rest.repos.uploadReleaseAsset({
|
if (name.endsWith("arm64-setup.exe")) {
|
||||||
...options,
|
updateData.platforms["windows-aarch64"].url = browser_download_url;
|
||||||
release_id: updateRelease.id,
|
}
|
||||||
name: jsonFile,
|
// win arm signature
|
||||||
data: JSON.stringify(updateData, null, 2),
|
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 github.rest.repos.uploadReleaseAsset({
|
await Promise.allSettled(promises);
|
||||||
...options,
|
console.log(updateData);
|
||||||
release_id: updateRelease.id,
|
|
||||||
name: proxyFile,
|
// maybe should test the signature as well
|
||||||
data: JSON.stringify(updateDataNew, null, 2),
|
// 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(
|
console.log(
|
||||||
`Successfully uploaded ${isAlpha ? "alpha" : "stable"} update files to ${releaseTag}`,
|
`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) {
|
} catch (error) {
|
||||||
console.error(
|
if (error.status === 404) {
|
||||||
`Failed to process ${isAlpha ? "alpha" : "stable"} release:`,
|
console.log(`Release not found for tag: ${tag.name}, skipping...`);
|
||||||
error.message,
|
} else {
|
||||||
);
|
console.error(
|
||||||
|
`Failed to get release for tag: ${tag.name}`,
|
||||||
|
error.message,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1
src-tauri/.clippy.toml
Normal file
1
src-tauri/.clippy.toml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
avoid-breaking-exported-api = true
|
||||||
2154
src-tauri/Cargo.lock
generated
2154
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "clash-verge"
|
name = "clash-verge"
|
||||||
version = "2.1.2"
|
version = "2.3.1"
|
||||||
description = "clash verge"
|
description = "clash verge"
|
||||||
authors = ["zzzgydi", "wonfen", "MystiPanda"]
|
authors = ["zzzgydi", "wonfen", "MystiPanda"]
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
@@ -13,80 +13,95 @@ build = "build.rs"
|
|||||||
identifier = "io.github.clash-verge-rev.clash-verge-rev"
|
identifier = "io.github.clash-verge-rev.clash-verge-rev"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "2.0.6", features = [] }
|
tauri-build = { version = "2.2.0", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
warp = "0.3"
|
warp = "0.3.7"
|
||||||
anyhow = "1.0.97"
|
anyhow = "1.0.98"
|
||||||
dirs = "6.0"
|
dirs = "6.0"
|
||||||
open = "5.1"
|
open = "5.3.2"
|
||||||
log = "0.4"
|
log = "0.4.27"
|
||||||
dunce = "1.0"
|
dunce = "1.0.5"
|
||||||
log4rs = "1"
|
log4rs = "1.3.0"
|
||||||
nanoid = "0.4"
|
nanoid = "0.4"
|
||||||
chrono = "0.4.40"
|
chrono = "0.4.41"
|
||||||
sysinfo = "0.33.1"
|
sysinfo = "0.35.2"
|
||||||
boa_engine = "0.20.0"
|
boa_engine = "0.20.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0.140"
|
||||||
serde_yaml = "0.9"
|
serde_yaml = "0.9.34-deprecated"
|
||||||
once_cell = "1.20.3"
|
once_cell = "1.21.3"
|
||||||
|
lazy_static = "1.5.0"
|
||||||
port_scanner = "0.1.5"
|
port_scanner = "0.1.5"
|
||||||
delay_timer = "0.11.6"
|
delay_timer = "0.11.6"
|
||||||
parking_lot = "0.12"
|
parking_lot = "0.12.4"
|
||||||
percent-encoding = "2.3.1"
|
percent-encoding = "2.3.1"
|
||||||
window-shadows = { version = "0.2.2" }
|
tokio = { version = "1.45.1", features = [
|
||||||
tokio = { version = "1.43", features = ["full"] }
|
"rt-multi-thread",
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
"macros",
|
||||||
reqwest = { version = "0.12", features = ["json", "rustls-tls"] }
|
"time",
|
||||||
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs", rev = "3d748b5" }
|
"sync",
|
||||||
image = "0.25.5"
|
|
||||||
imageproc = "0.25.0"
|
|
||||||
rusttype = "0.9"
|
|
||||||
tauri = { version = "2.3.1", features = [
|
|
||||||
"protocol-asset",
|
|
||||||
"devtools",
|
|
||||||
"tray-icon",
|
|
||||||
"image-ico",
|
|
||||||
"image-png",
|
|
||||||
] }
|
] }
|
||||||
network-interface = { version = "2.0.0", features = ["serde"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
tauri-plugin-shell = "2.2.0"
|
reqwest = { version = "0.12.20", features = ["json", "rustls-tls", "cookies"] }
|
||||||
tauri-plugin-dialog = "2.2.0"
|
regex = "1.11.1"
|
||||||
tauri-plugin-fs = "2.2.0"
|
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" }
|
||||||
tauri-plugin-notification = "2.2.1"
|
image = "0.25.6"
|
||||||
tauri-plugin-process = "2.2.0"
|
imageproc = "0.25.0"
|
||||||
tauri-plugin-clipboard-manager = "2.2.1"
|
tauri = { version = "2.5.1", features = [
|
||||||
tauri-plugin-deep-link = "2.2.0"
|
"protocol-asset",
|
||||||
tauri-plugin-devtools = "2.0.0-rc"
|
"devtools",
|
||||||
url = "2.5.4"
|
"tray-icon",
|
||||||
zip = "2.2.3"
|
"image-ico",
|
||||||
reqwest_dav = "0.1.14"
|
"image-png",
|
||||||
|
] }
|
||||||
|
network-interface = { version = "2.0.1", features = ["serde"] }
|
||||||
|
tauri-plugin-shell = "2.2.2"
|
||||||
|
tauri-plugin-dialog = "2.2.2"
|
||||||
|
tauri-plugin-fs = "2.3.0"
|
||||||
|
tauri-plugin-process = "2.2.2"
|
||||||
|
tauri-plugin-clipboard-manager = "2.2.3"
|
||||||
|
tauri-plugin-deep-link = "2.3.0"
|
||||||
|
tauri-plugin-devtools = "2.0.0"
|
||||||
|
tauri-plugin-window-state = "2.2.3"
|
||||||
|
zip = "4.1.0"
|
||||||
|
reqwest_dav = "0.2.1"
|
||||||
aes-gcm = { version = "0.10.3", features = ["std"] }
|
aes-gcm = { version = "0.10.3", features = ["std"] }
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
getrandom = "0.3.1"
|
getrandom = "0.3.3"
|
||||||
tokio-tungstenite = "0.26.2"
|
tokio-tungstenite = "0.27.0"
|
||||||
futures = "0.3"
|
futures = "0.3.31"
|
||||||
sys-locale = "0.3.1"
|
sys-locale = "0.3.2"
|
||||||
async-trait = "0.1.87"
|
async-trait = "0.1.88"
|
||||||
mihomo_api = { path = "./src/crate_mihomo_api" }
|
mihomo_api = { path = "src_crates/crate_mihomo_api" }
|
||||||
ab_glyph = "0.2.29"
|
ab_glyph = "0.2.29"
|
||||||
tungstenite = "0.26.2"
|
tungstenite = "0.27.0"
|
||||||
|
libc = "0.2.173"
|
||||||
|
gethostname = "1.0.2"
|
||||||
|
hmac = "0.12.1"
|
||||||
|
sha2 = "0.10.9"
|
||||||
|
hex = "0.4.3"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
runas = "=1.2.0"
|
runas = "=1.2.0"
|
||||||
deelevate = "0.2.0"
|
deelevate = "0.2.0"
|
||||||
winreg = "0.55.0"
|
winreg = "0.55.0"
|
||||||
url = "2.5.4"
|
winapi = { version = "0.3.9", features = [
|
||||||
|
"winbase",
|
||||||
|
"fileapi",
|
||||||
|
"winnt",
|
||||||
|
"handleapi",
|
||||||
|
"errhandlingapi",
|
||||||
|
"minwindef",
|
||||||
|
"winerror",
|
||||||
|
] }
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
users = "0.11.0"
|
users = "0.11.0"
|
||||||
|
|
||||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||||
tauri-plugin-autostart = "2.2.0"
|
tauri-plugin-autostart = "2.4.0"
|
||||||
tauri-plugin-global-shortcut = "2.2.0"
|
tauri-plugin-global-shortcut = "2.2.1"
|
||||||
tauri-plugin-updater = "2.5.1"
|
tauri-plugin-updater = "2.8.1"
|
||||||
tauri-plugin-window-state = "2.2.1"
|
|
||||||
#openssl
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["custom-protocol"]
|
default = ["custom-protocol"]
|
||||||
@@ -102,34 +117,31 @@ strip = true
|
|||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
incremental = true
|
incremental = true
|
||||||
|
codegen-units = 256 # 增加编译单元,提升编译速度
|
||||||
|
opt-level = 0 # 禁用优化,进一步提升编译速度
|
||||||
|
debug = true # 保留调试信息
|
||||||
|
strip = false # 不剥离符号,保留调试信息
|
||||||
|
|
||||||
[profile.fast-release]
|
[profile.fast-release]
|
||||||
inherits = "release" # 继承 release 的配置
|
inherits = "release" # 继承 release 的配置
|
||||||
panic = "abort" # 与 release 相同
|
panic = "abort" # 与 release 相同
|
||||||
codegen-units = 256 # 增加编译单元,提升编译速度
|
codegen-units = 256 # 增加编译单元,提升编译速度
|
||||||
lto = false # 禁用 LTO,提升编译速度
|
lto = false # 禁用 LTO,提升编译速度
|
||||||
opt-level = 0 # 禁用优化,大幅提升编译速度
|
opt-level = 0 # 禁用优化,大幅提升编译速度
|
||||||
debug = true # 保留调试信息
|
debug = true # 保留调试信息
|
||||||
strip = false # 不剥离符号,保留调试信息
|
strip = false # 不剥离符号,保留调试信息
|
||||||
|
|
||||||
[profile.fast-dev]
|
|
||||||
inherits = "dev" # 继承 dev 的配置
|
|
||||||
codegen-units = 256 # 增加编译单元,提升编译速度
|
|
||||||
opt-level = 0 # 禁用优化,进一步提升编译速度
|
|
||||||
incremental = true # 启用增量编译
|
|
||||||
debug = true # 保留调试信息
|
|
||||||
strip = false # 不剥离符号,保留调试信息
|
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "app_lib"
|
name = "app_lib"
|
||||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
env_logger = "0.11.0"
|
tempfile = "3.20.0"
|
||||||
mockito = "1.7.0"
|
|
||||||
tempfile = "3.17.1"
|
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = ["src_crates/crate_mihomo_api"]
|
||||||
"src/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" }
|
||||||
|
|||||||
9
src-tauri/capabilities/desktop-windows.json
Normal file
9
src-tauri/capabilities/desktop-windows.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"identifier": "desktop-windows-capability",
|
||||||
|
"description": "permissions for desktop windows applications",
|
||||||
|
"windows": ["main"],
|
||||||
|
"permissions": [
|
||||||
|
"core:webview:allow-create-webview",
|
||||||
|
"core:webview:allow-create-webview-window"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -14,8 +14,9 @@
|
|||||||
"updater:allow-download-and-install",
|
"updater:allow-download-and-install",
|
||||||
"process:allow-restart",
|
"process:allow-restart",
|
||||||
"deep-link:default",
|
"deep-link:default",
|
||||||
"window-state:default",
|
"autostart:allow-enable",
|
||||||
"window-state:default",
|
"autostart:allow-disable",
|
||||||
"autostart:default"
|
"autostart:allow-is-enabled",
|
||||||
|
"core:window:allow-set-theme"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,6 @@
|
|||||||
"shell:allow-spawn",
|
"shell:allow-spawn",
|
||||||
"shell:allow-stdin-write",
|
"shell:allow-stdin-write",
|
||||||
"dialog:allow-open",
|
"dialog:allow-open",
|
||||||
"notification:default",
|
|
||||||
"global-shortcut:allow-is-registered",
|
"global-shortcut:allow-is-registered",
|
||||||
"global-shortcut:allow-register",
|
"global-shortcut:allow-register",
|
||||||
"global-shortcut:allow-register-all",
|
"global-shortcut:allow-register-all",
|
||||||
@@ -79,7 +78,6 @@
|
|||||||
"clipboard-manager:allow-read-text",
|
"clipboard-manager:allow-read-text",
|
||||||
"clipboard-manager:allow-write-text",
|
"clipboard-manager:allow-write-text",
|
||||||
"shell:default",
|
"shell:default",
|
||||||
"dialog:default",
|
"dialog:default"
|
||||||
"notification:default"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -916,6 +916,10 @@ FunctionEnd
|
|||||||
!macroend
|
!macroend
|
||||||
|
|
||||||
Section Uninstall
|
Section Uninstall
|
||||||
|
;删除 window-state.json 文件
|
||||||
|
SetShellVarContext current
|
||||||
|
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\window-state.json"
|
||||||
|
|
||||||
!insertmacro CheckIfAppIsRunning
|
!insertmacro CheckIfAppIsRunning
|
||||||
!insertmacro CheckAllVergeProcesses
|
!insertmacro CheckAllVergeProcesses
|
||||||
!insertmacro RemoveVergeService
|
!insertmacro RemoveVergeService
|
||||||
@@ -975,16 +979,23 @@ Section Uninstall
|
|||||||
RMDir "$INSTDIR"
|
RMDir "$INSTDIR"
|
||||||
|
|
||||||
!insertmacro DeleteAppUserModelId
|
!insertmacro DeleteAppUserModelId
|
||||||
!insertmacro UnpinShortcut "$SMPROGRAMS\$AppStartMenuFolder\${MAINBINARYNAME}.lnk"
|
!insertmacro UnpinShortcut "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk"
|
||||||
!insertmacro UnpinShortcut "$DESKTOP\${MAINBINARYNAME}.lnk"
|
!insertmacro UnpinShortcut "$DESKTOP\${PRODUCTNAME}.lnk"
|
||||||
|
; 兼容旧名称快捷方式
|
||||||
|
!insertmacro UnpinShortcut "$SMPROGRAMS\$AppStartMenuFolder\clash-verge.lnk"
|
||||||
|
!insertmacro UnpinShortcut "$DESKTOP\clash-verge.lnk"
|
||||||
|
|
||||||
; Remove start menu shortcut
|
; Remove start menu shortcut
|
||||||
!insertmacro MUI_STARTMENU_GETFOLDER Application $AppStartMenuFolder
|
!insertmacro MUI_STARTMENU_GETFOLDER Application $AppStartMenuFolder
|
||||||
Delete "$SMPROGRAMS\$AppStartMenuFolder\${MAINBINARYNAME}.lnk"
|
Delete "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk"
|
||||||
|
; 兼容旧名称快捷方式
|
||||||
|
Delete "$SMPROGRAMS\$AppStartMenuFolder\clash-verge.lnk"
|
||||||
RMDir "$SMPROGRAMS\$AppStartMenuFolder"
|
RMDir "$SMPROGRAMS\$AppStartMenuFolder"
|
||||||
|
|
||||||
; Remove desktop shortcuts
|
; Remove desktop shortcuts
|
||||||
Delete "$DESKTOP\${MAINBINARYNAME}.lnk"
|
Delete "$DESKTOP\${PRODUCTNAME}.lnk"
|
||||||
|
; 兼容旧名称快捷方式
|
||||||
|
Delete "$DESKTOP\clash-verge.lnk"
|
||||||
|
|
||||||
; Remove registry information for add/remove programs
|
; Remove registry information for add/remove programs
|
||||||
!if "${INSTALLMODE}" == "both"
|
!if "${INSTALLMODE}" == "both"
|
||||||
@@ -1004,6 +1015,10 @@ Section Uninstall
|
|||||||
RmDir /r "$LOCALAPPDATA\${BUNDLEID}"
|
RmDir /r "$LOCALAPPDATA\${BUNDLEID}"
|
||||||
${EndIf}
|
${EndIf}
|
||||||
|
|
||||||
|
;删除 window-state.json 文件
|
||||||
|
SetShellVarContext current
|
||||||
|
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\window-state.json"
|
||||||
|
|
||||||
${GetOptions} $CMDLINE "/P" $R0
|
${GetOptions} $CMDLINE "/P" $R0
|
||||||
IfErrors +2 0
|
IfErrors +2 0
|
||||||
SetAutoClose true
|
SetAutoClose true
|
||||||
@@ -1046,12 +1061,12 @@ FunctionEnd
|
|||||||
!macroend
|
!macroend
|
||||||
|
|
||||||
Function CreateDesktopShortcut
|
Function CreateDesktopShortcut
|
||||||
CreateShortcut "$DESKTOP\${MAINBINARYNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe"
|
CreateShortcut "$DESKTOP\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe"
|
||||||
!insertmacro SetLnkAppUserModelId "$DESKTOP\${MAINBINARYNAME}.lnk"
|
!insertmacro SetLnkAppUserModelId "$DESKTOP\${PRODUCTNAME}.lnk"
|
||||||
FunctionEnd
|
FunctionEnd
|
||||||
|
|
||||||
Function CreateStartMenuShortcut
|
Function CreateStartMenuShortcut
|
||||||
CreateDirectory "$SMPROGRAMS\$AppStartMenuFolder"
|
CreateDirectory "$SMPROGRAMS\$AppStartMenuFolder"
|
||||||
CreateShortcut "$SMPROGRAMS\$AppStartMenuFolder\${MAINBINARYNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe"
|
CreateShortcut "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe"
|
||||||
!insertmacro SetLnkAppUserModelId "$SMPROGRAMS\$AppStartMenuFolder\${MAINBINARYNAME}.lnk"
|
!insertmacro SetLnkAppUserModelId "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk"
|
||||||
FunctionEnd
|
FunctionEnd
|
||||||
|
|||||||
@@ -11,4 +11,3 @@ merge_derives = true
|
|||||||
use_try_shorthand = false
|
use_try_shorthand = false
|
||||||
use_field_init_shorthand = false
|
use_field_init_shorthand = false
|
||||||
force_explicit_abi = true
|
force_explicit_abi = true
|
||||||
imports_granularity = "Crate"
|
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
use super::CmdResult;
|
use super::CmdResult;
|
||||||
use crate::{feat, utils::dirs, wrap_err};
|
use crate::{
|
||||||
|
feat, logging,
|
||||||
|
utils::{dirs, logging::Type},
|
||||||
|
wrap_err,
|
||||||
|
};
|
||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
|
|
||||||
/// 打开应用程序所在目录
|
/// 打开应用程序所在目录
|
||||||
@@ -45,7 +49,7 @@ pub fn open_devtools(app_handle: tauri::AppHandle) {
|
|||||||
/// 退出应用
|
/// 退出应用
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn exit_app() {
|
pub fn exit_app() {
|
||||||
feat::quit(Some(0));
|
feat::quit();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 重启应用
|
/// 重启应用
|
||||||
@@ -70,46 +74,46 @@ pub fn get_app_dir() -> CmdResult<String> {
|
|||||||
Ok(app_home_dir)
|
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]
|
#[tauri::command]
|
||||||
pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String> {
|
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_cache_dir = wrap_err!(dirs::app_home_dir())?.join("icons").join("cache");
|
||||||
let icon_path = icon_cache_dir.join(&name);
|
let icon_path = icon_cache_dir.join(&name);
|
||||||
|
|
||||||
// 如果文件已存在,直接返回路径
|
|
||||||
if icon_path.exists() {
|
if icon_path.exists() {
|
||||||
return Ok(icon_path.to_string_lossy().to_string());
|
return Ok(icon_path.to_string_lossy().to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确保缓存目录存在
|
|
||||||
if !icon_cache_dir.exists() {
|
if !icon_cache_dir.exists() {
|
||||||
let _ = std::fs::create_dir_all(&icon_cache_dir);
|
let _ = std::fs::create_dir_all(&icon_cache_dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用临时文件名来下载
|
|
||||||
let temp_path = icon_cache_dir.join(format!("{}.downloading", &name));
|
let temp_path = icon_cache_dir.join(format!("{}.downloading", &name));
|
||||||
|
|
||||||
// 下载文件到临时位置
|
|
||||||
let response = wrap_err!(reqwest::get(&url).await)?;
|
let response = wrap_err!(reqwest::get(&url).await)?;
|
||||||
|
|
||||||
// 检查内容类型是否为图片
|
let content_type = response
|
||||||
let content_type = response.headers()
|
.headers()
|
||||||
.get(reqwest::header::CONTENT_TYPE)
|
.get(reqwest::header::CONTENT_TYPE)
|
||||||
.and_then(|v| v.to_str().ok())
|
.and_then(|v| v.to_str().ok())
|
||||||
.unwrap_or("");
|
.unwrap_or("");
|
||||||
|
|
||||||
let is_image = content_type.starts_with("image/");
|
let is_image = content_type.starts_with("image/");
|
||||||
|
|
||||||
// 获取响应内容
|
|
||||||
let content = wrap_err!(response.bytes().await)?;
|
let content = wrap_err!(response.bytes().await)?;
|
||||||
|
|
||||||
// 检查内容是否为HTML (针对CDN错误页面)
|
let is_html = content.len() > 15
|
||||||
let is_html = content.len() > 15 &&
|
&& (content.starts_with(b"<!DOCTYPE html")
|
||||||
(content.starts_with(b"<!DOCTYPE html") ||
|
|| content.starts_with(b"<html")
|
||||||
content.starts_with(b"<html") ||
|
|| content.starts_with(b"<?xml"));
|
||||||
content.starts_with(b"<?xml"));
|
|
||||||
|
|
||||||
// 只有当内容确实是图片时才保存
|
|
||||||
if is_image && !is_html {
|
if is_image && !is_html {
|
||||||
{
|
{
|
||||||
let mut file = match std::fs::File::create(&temp_path) {
|
let mut file = match std::fs::File::create(&temp_path) {
|
||||||
@@ -122,14 +126,13 @@ pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
wrap_err!(std::io::copy(&mut content.as_ref(), &mut file))?;
|
wrap_err!(std::io::copy(&mut content.as_ref(), &mut file))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 再次检查目标文件是否已存在,避免重命名覆盖其他线程已完成的文件
|
|
||||||
if !icon_path.exists() {
|
if !icon_path.exists() {
|
||||||
match std::fs::rename(&temp_path, &icon_path) {
|
match std::fs::rename(&temp_path, &icon_path) {
|
||||||
Ok(_) => {},
|
Ok(_) => {}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
let _ = std::fs::remove_file(&temp_path);
|
let _ = std::fs::remove_file(&temp_path);
|
||||||
if icon_path.exists() {
|
if icon_path.exists() {
|
||||||
@@ -140,11 +143,11 @@ pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String>
|
|||||||
} else {
|
} else {
|
||||||
let _ = std::fs::remove_file(&temp_path);
|
let _ = std::fs::remove_file(&temp_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(icon_path.to_string_lossy().to_string())
|
Ok(icon_path.to_string_lossy().to_string())
|
||||||
} else {
|
} else {
|
||||||
let _ = std::fs::remove_file(&temp_path);
|
let _ = std::fs::remove_file(&temp_path);
|
||||||
Err(format!("下载的内容不是有效图片: {}", url).into())
|
Err(format!("下载的内容不是有效图片: {}", url))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,8 +161,7 @@ pub struct IconInfo {
|
|||||||
/// 复制图标文件
|
/// 复制图标文件
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult<String> {
|
pub fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult<String> {
|
||||||
use std::fs;
|
use std::{fs, path::Path};
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
let file_path = Path::new(&path);
|
let file_path = Path::new(&path);
|
||||||
|
|
||||||
@@ -187,7 +189,14 @@ pub fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult<String> {
|
|||||||
)
|
)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
}
|
}
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Cmd,
|
||||||
|
true,
|
||||||
|
"Copying icon file path: {:?} -> file dist: {:?}",
|
||||||
|
path,
|
||||||
|
dest_path
|
||||||
|
);
|
||||||
match fs::copy(file_path, &dest_path) {
|
match fs::copy(file_path, &dest_path) {
|
||||||
Ok(_) => Ok(dest_path.to_string_lossy().to_string()),
|
Ok(_) => Ok(dest_path.to_string_lossy().to_string()),
|
||||||
Err(err) => Err(err.to_string()),
|
Err(err) => Err(err.to_string()),
|
||||||
@@ -196,3 +205,42 @@ pub fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult<String> {
|
|||||||
Err("file not found".to_string())
|
Err("file not found".to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 通知UI已准备就绪
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn notify_ui_ready() -> CmdResult<()> {
|
||||||
|
log::info!(target: "app", "前端UI已准备就绪");
|
||||||
|
crate::utils::resolve::mark_ui_ready();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// UI加载阶段
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn update_ui_stage(stage: String) -> CmdResult<()> {
|
||||||
|
log::info!(target: "app", "UI加载阶段更新: {}", stage);
|
||||||
|
|
||||||
|
use crate::utils::resolve::UiReadyStage;
|
||||||
|
|
||||||
|
let stage_enum = match stage.as_str() {
|
||||||
|
"NotStarted" => UiReadyStage::NotStarted,
|
||||||
|
"Loading" => UiReadyStage::Loading,
|
||||||
|
"DomReady" => UiReadyStage::DomReady,
|
||||||
|
"ResourcesLoaded" => UiReadyStage::ResourcesLoaded,
|
||||||
|
"Ready" => UiReadyStage::Ready,
|
||||||
|
_ => {
|
||||||
|
log::warn!(target: "app", "未知的UI加载阶段: {}", stage);
|
||||||
|
return Err(format!("未知的UI加载阶段: {}", stage));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
crate::utils::resolve::update_ui_ready_stage(stage_enum);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 重置UI就绪状态
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn reset_ui_ready_state() -> CmdResult<()> {
|
||||||
|
log::info!(target: "app", "重置UI就绪状态");
|
||||||
|
crate::utils::resolve::reset_ui_ready();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use super::CmdResult;
|
use super::CmdResult;
|
||||||
use crate::{config::*, core::*, feat, module::mihomo::MihomoManager, wrap_err};
|
use crate::{
|
||||||
|
config::*, core::*, feat, module::mihomo::MihomoManager, process::AsyncHandler, wrap_err,
|
||||||
|
};
|
||||||
use serde_yaml::Mapping;
|
use serde_yaml::Mapping;
|
||||||
|
|
||||||
/// 复制Clash环境变量
|
/// 复制Clash环境变量
|
||||||
@@ -24,7 +26,8 @@ pub async fn patch_clash_config(payload: Mapping) -> CmdResult {
|
|||||||
/// 修改Clash模式
|
/// 修改Clash模式
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn patch_clash_mode(payload: String) -> CmdResult {
|
pub async fn patch_clash_mode(payload: String) -> CmdResult {
|
||||||
Ok(feat::change_clash_mode(payload))
|
feat::change_clash_mode(payload);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 切换Clash核心
|
/// 切换Clash核心
|
||||||
@@ -37,10 +40,21 @@ pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>>
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
log::info!(target: "app", "core changed to {clash_core}");
|
// 切换内核后重启内核
|
||||||
handle::Handle::notice_message("config_core::change_success", &clash_core);
|
match CoreManager::global().restart_core().await {
|
||||||
handle::Handle::refresh_clash();
|
Ok(_) => {
|
||||||
Ok(None)
|
log::info!(target: "app", "core changed and restarted to {clash_core}");
|
||||||
|
handle::Handle::notice_message("config_core::change_success", &clash_core);
|
||||||
|
handle::Handle::refresh_clash();
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
let error_msg = format!("Core changed but failed to restart: {}", err);
|
||||||
|
log::error!(target: "app", "{}", error_msg);
|
||||||
|
handle::Handle::notice_message("config_core::change_error", &error_msg);
|
||||||
|
Ok(Some(error_msg))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let error_msg = err.to_string();
|
let error_msg = err.to_string();
|
||||||
@@ -51,6 +65,18 @@ pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 启动核心
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn start_core() -> CmdResult {
|
||||||
|
wrap_err!(CoreManager::global().start_core().await)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 关闭核心
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn stop_core() -> CmdResult {
|
||||||
|
wrap_err!(CoreManager::global().stop_core().await)
|
||||||
|
}
|
||||||
|
|
||||||
/// 重启核心
|
/// 重启核心
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn restart_core() -> CmdResult {
|
pub async fn restart_core() -> CmdResult {
|
||||||
@@ -98,13 +124,14 @@ pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
|
|||||||
/// 应用或撤销DNS配置
|
/// 应用或撤销DNS配置
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn apply_dns_config(apply: bool) -> CmdResult {
|
pub fn apply_dns_config(apply: bool) -> CmdResult {
|
||||||
use crate::config::Config;
|
use crate::{
|
||||||
use crate::core::{handle, CoreManager};
|
config::Config,
|
||||||
use crate::utils::dirs;
|
core::{handle, CoreManager},
|
||||||
use tauri::async_runtime;
|
utils::dirs,
|
||||||
|
};
|
||||||
|
|
||||||
// 使用spawn来处理异步操作
|
// 使用spawn来处理异步操作
|
||||||
async_runtime::spawn(async move {
|
AsyncHandler::spawn(move || async move {
|
||||||
if apply {
|
if apply {
|
||||||
// 读取DNS配置文件
|
// 读取DNS配置文件
|
||||||
let dns_path = match dirs::app_home_dir() {
|
let dns_path = match dirs::app_home_dir() {
|
||||||
@@ -218,3 +245,25 @@ pub async fn get_dns_config_content() -> CmdResult<String> {
|
|||||||
let content = fs::read_to_string(&dns_path).map_err(|e| e.to_string())?;
|
let content = fs::read_to_string(&dns_path).map_err(|e| e.to_string())?;
|
||||||
Ok(content)
|
Ok(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 验证DNS配置文件
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn validate_dns_config() -> CmdResult<(bool, String)> {
|
||||||
|
use crate::{core::CoreManager, utils::dirs};
|
||||||
|
|
||||||
|
let app_dir = dirs::app_home_dir().map_err(|e| e.to_string())?;
|
||||||
|
let dns_path = app_dir.join("dns_config.yaml");
|
||||||
|
let dns_path_str = dns_path.to_str().unwrap_or_default();
|
||||||
|
|
||||||
|
if !dns_path.exists() {
|
||||||
|
return Ok((false, "DNS config file not found".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
match CoreManager::global()
|
||||||
|
.validate_config_file(dns_path_str, None)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(result) => Ok(result),
|
||||||
|
Err(e) => Err(e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
15
src-tauri/src/cmd/lightweight.rs
Normal file
15
src-tauri/src/cmd/lightweight.rs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
use crate::module::lightweight;
|
||||||
|
|
||||||
|
use super::CmdResult;
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn entry_lightweight_mode() -> CmdResult {
|
||||||
|
lightweight::entry_lightweight_mode();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn exit_lightweight_mode() -> CmdResult {
|
||||||
|
lightweight::exit_lightweight_mode();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
1297
src-tauri/src/cmd/media_unlock_checker.rs
Normal file
1297
src-tauri/src/cmd/media_unlock_checker.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -4,29 +4,35 @@ use anyhow::Result;
|
|||||||
pub type CmdResult<T = ()> = Result<T, String>;
|
pub type CmdResult<T = ()> = Result<T, String>;
|
||||||
|
|
||||||
// Command modules
|
// Command modules
|
||||||
pub mod profile;
|
|
||||||
pub mod validate;
|
|
||||||
pub mod uwp;
|
|
||||||
pub mod webdav;
|
|
||||||
pub mod app;
|
pub mod app;
|
||||||
pub mod network;
|
|
||||||
pub mod clash;
|
pub mod clash;
|
||||||
pub mod verge;
|
pub mod lightweight;
|
||||||
|
pub mod media_unlock_checker;
|
||||||
|
pub mod network;
|
||||||
|
pub mod profile;
|
||||||
|
pub mod proxy;
|
||||||
pub mod runtime;
|
pub mod runtime;
|
||||||
pub mod save_profile;
|
pub mod save_profile;
|
||||||
|
pub mod service;
|
||||||
pub mod system;
|
pub mod system;
|
||||||
pub mod proxy;
|
pub mod uwp;
|
||||||
|
pub mod validate;
|
||||||
|
pub mod verge;
|
||||||
|
pub mod webdav;
|
||||||
|
|
||||||
// Re-export all command functions for backwards compatibility
|
// Re-export all command functions for backwards compatibility
|
||||||
pub use profile::*;
|
|
||||||
pub use validate::*;
|
|
||||||
pub use uwp::*;
|
|
||||||
pub use webdav::*;
|
|
||||||
pub use app::*;
|
pub use app::*;
|
||||||
pub use network::*;
|
|
||||||
pub use clash::*;
|
pub use clash::*;
|
||||||
pub use verge::*;
|
pub use lightweight::*;
|
||||||
|
pub use media_unlock_checker::*;
|
||||||
|
pub use network::*;
|
||||||
|
pub use profile::*;
|
||||||
|
pub use proxy::*;
|
||||||
pub use runtime::*;
|
pub use runtime::*;
|
||||||
pub use save_profile::*;
|
pub use save_profile::*;
|
||||||
|
pub use service::*;
|
||||||
pub use system::*;
|
pub use system::*;
|
||||||
pub use proxy::*;
|
pub use uwp::*;
|
||||||
|
pub use validate::*;
|
||||||
|
pub use verge::*;
|
||||||
|
pub use webdav::*;
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
use crate::wrap_err;
|
|
||||||
use super::CmdResult;
|
use super::CmdResult;
|
||||||
use sysproxy::{Autoproxy, Sysproxy};
|
use crate::wrap_err;
|
||||||
use serde_yaml::Mapping;
|
|
||||||
use network_interface::NetworkInterface;
|
use network_interface::NetworkInterface;
|
||||||
|
use serde_yaml::Mapping;
|
||||||
|
use sysproxy::{Autoproxy, Sysproxy};
|
||||||
|
use tokio::task::spawn_blocking;
|
||||||
|
|
||||||
/// get the system proxy
|
/// get the system proxy
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_sys_proxy() -> CmdResult<Mapping> {
|
pub async fn get_sys_proxy() -> CmdResult<Mapping> {
|
||||||
let current = wrap_err!(Sysproxy::get_system_proxy())?;
|
let current = spawn_blocking(Sysproxy::get_system_proxy)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to spawn blocking task for sysproxy: {}", e))?
|
||||||
|
.map_err(|e| format!("Failed to get system proxy: {}", e))?;
|
||||||
|
|
||||||
let mut map = Mapping::new();
|
let mut map = Mapping::new();
|
||||||
map.insert("enable".into(), current.enable.into());
|
map.insert("enable".into(), current.enable.into());
|
||||||
map.insert(
|
map.insert(
|
||||||
@@ -21,8 +26,11 @@ pub fn get_sys_proxy() -> CmdResult<Mapping> {
|
|||||||
|
|
||||||
/// get the system proxy
|
/// get the system proxy
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_auto_proxy() -> CmdResult<Mapping> {
|
pub async fn get_auto_proxy() -> CmdResult<Mapping> {
|
||||||
let current = wrap_err!(Autoproxy::get_auto_proxy())?;
|
let current = spawn_blocking(Autoproxy::get_auto_proxy)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to spawn blocking task for autoproxy: {}", e))?
|
||||||
|
.map_err(|e| format!("Failed to get auto proxy: {}", e))?;
|
||||||
|
|
||||||
let mut map = Mapping::new();
|
let mut map = Mapping::new();
|
||||||
map.insert("enable".into(), current.enable.into());
|
map.insert("enable".into(), current.enable.into());
|
||||||
@@ -31,6 +39,25 @@ pub fn get_auto_proxy() -> CmdResult<Mapping> {
|
|||||||
Ok(map)
|
Ok(map)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 获取系统主机名
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_system_hostname() -> CmdResult<String> {
|
||||||
|
use gethostname::gethostname;
|
||||||
|
|
||||||
|
// 获取系统主机名,处理可能的非UTF-8字符
|
||||||
|
let hostname = match gethostname().into_string() {
|
||||||
|
Ok(name) => name,
|
||||||
|
Err(os_string) => {
|
||||||
|
// 对于包含非UTF-8的主机名,使用调试格式化
|
||||||
|
let fallback = format!("{:?}", os_string);
|
||||||
|
// 去掉可能存在的引号
|
||||||
|
fallback.trim_matches('"').to_string()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(hostname)
|
||||||
|
}
|
||||||
|
|
||||||
/// 获取网络接口列表
|
/// 获取网络接口列表
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_network_interfaces() -> Vec<String> {
|
pub fn get_network_interfaces() -> Vec<String> {
|
||||||
@@ -46,8 +73,7 @@ pub fn get_network_interfaces() -> Vec<String> {
|
|||||||
/// 获取网络接口详细信息
|
/// 获取网络接口详细信息
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_network_interfaces_info() -> CmdResult<Vec<NetworkInterface>> {
|
pub fn get_network_interfaces_info() -> CmdResult<Vec<NetworkInterface>> {
|
||||||
use network_interface::NetworkInterface;
|
use network_interface::{NetworkInterface, NetworkInterfaceConfig};
|
||||||
use network_interface::NetworkInterfaceConfig;
|
|
||||||
|
|
||||||
let names = get_network_interfaces();
|
let names = get_network_interfaces();
|
||||||
let interfaces = wrap_err!(NetworkInterface::show())?;
|
let interfaces = wrap_err!(NetworkInterface::show())?;
|
||||||
|
|||||||
@@ -1,40 +1,110 @@
|
|||||||
use crate::{
|
|
||||||
config::*,
|
|
||||||
core::*,
|
|
||||||
feat,
|
|
||||||
utils::{dirs, help},
|
|
||||||
log_err, ret_err, wrap_err,
|
|
||||||
};
|
|
||||||
use super::CmdResult;
|
use super::CmdResult;
|
||||||
|
use crate::{
|
||||||
|
config::{Config, IProfiles, PrfItem, PrfOption},
|
||||||
|
core::{handle, timer::Timer, tray::Tray, CoreManager},
|
||||||
|
feat, logging, ret_err,
|
||||||
|
utils::{dirs, help, logging::Type},
|
||||||
|
wrap_err,
|
||||||
|
};
|
||||||
|
use std::time::Duration;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
/// 获取配置文件列表
|
// 添加全局互斥锁防止并发配置更新
|
||||||
|
static PROFILE_UPDATE_MUTEX: Mutex<()> = Mutex::const_new(());
|
||||||
|
|
||||||
|
/// 获取配置文件避免锁竞争
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_profiles() -> CmdResult<IProfiles> {
|
pub async fn get_profiles() -> CmdResult<IProfiles> {
|
||||||
let _ = tray::Tray::global().update_menu();
|
// 策略1: 尝试快速获取latest数据
|
||||||
Ok(Config::profiles().data().clone())
|
let latest_result = tokio::time::timeout(
|
||||||
|
Duration::from_millis(500),
|
||||||
|
tokio::task::spawn_blocking(move || {
|
||||||
|
let profiles = Config::profiles();
|
||||||
|
let latest = profiles.latest();
|
||||||
|
IProfiles {
|
||||||
|
current: latest.current.clone(),
|
||||||
|
items: latest.items.clone(),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match latest_result {
|
||||||
|
Ok(Ok(profiles)) => {
|
||||||
|
logging!(info, Type::Cmd, false, "快速获取配置列表成功");
|
||||||
|
return Ok(profiles);
|
||||||
|
}
|
||||||
|
Ok(Err(join_err)) => {
|
||||||
|
logging!(warn, Type::Cmd, true, "快速获取配置任务失败: {}", join_err);
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
logging!(warn, Type::Cmd, true, "快速获取配置超时(500ms)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 策略2: 如果快速获取失败,尝试获取data()
|
||||||
|
let data_result = tokio::time::timeout(
|
||||||
|
Duration::from_secs(2),
|
||||||
|
tokio::task::spawn_blocking(move || {
|
||||||
|
let profiles = Config::profiles();
|
||||||
|
let data = profiles.data();
|
||||||
|
IProfiles {
|
||||||
|
current: data.current.clone(),
|
||||||
|
items: data.items.clone(),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match data_result {
|
||||||
|
Ok(Ok(profiles)) => {
|
||||||
|
logging!(info, Type::Cmd, false, "获取draft配置列表成功");
|
||||||
|
return Ok(profiles);
|
||||||
|
}
|
||||||
|
Ok(Err(join_err)) => {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Cmd,
|
||||||
|
true,
|
||||||
|
"获取draft配置任务失败: {}",
|
||||||
|
join_err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
logging!(error, Type::Cmd, true, "获取draft配置超时(2秒)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 策略3: fallback,尝试重新创建配置
|
||||||
|
logging!(
|
||||||
|
warn,
|
||||||
|
Type::Cmd,
|
||||||
|
true,
|
||||||
|
"所有获取配置策略都失败,尝试fallback"
|
||||||
|
);
|
||||||
|
|
||||||
|
match tokio::task::spawn_blocking(IProfiles::new).await {
|
||||||
|
Ok(profiles) => {
|
||||||
|
logging!(info, Type::Cmd, true, "使用fallback配置成功");
|
||||||
|
Ok(profiles)
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
logging!(error, Type::Cmd, true, "fallback配置也失败: {}", err);
|
||||||
|
// 返回空配置避免崩溃
|
||||||
|
Ok(IProfiles {
|
||||||
|
current: None,
|
||||||
|
items: Some(vec![]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 增强配置文件
|
/// 增强配置文件
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn enhance_profiles() -> CmdResult {
|
pub async fn enhance_profiles() -> CmdResult {
|
||||||
match CoreManager::global().update_config().await {
|
wrap_err!(feat::enhance_profiles().await)?;
|
||||||
Ok((true, _)) => {
|
handle::Handle::refresh_clash();
|
||||||
println!("[enhance_profiles] 配置更新成功");
|
Ok(())
|
||||||
log_err!(tray::Tray::global().update_tooltip());
|
|
||||||
handle::Handle::refresh_clash();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Ok((false, error_msg)) => {
|
|
||||||
println!("[enhance_profiles] 配置验证失败: {}", error_msg);
|
|
||||||
handle::Handle::notice_message("config_validate::error", &error_msg);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
println!("[enhance_profiles] 更新过程发生错误: {}", e);
|
|
||||||
handle::Handle::notice_message("config_validate::process_terminated", &e.to_string());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 导入配置文件
|
/// 导入配置文件
|
||||||
@@ -60,13 +130,17 @@ pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResu
|
|||||||
/// 更新配置文件
|
/// 更新配置文件
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult {
|
pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult {
|
||||||
wrap_err!(feat::update_profile(index, option).await)
|
wrap_err!(feat::update_profile(index, option, Some(true)).await)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 删除配置文件
|
/// 删除配置文件
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn delete_profile(index: String) -> CmdResult {
|
pub async fn delete_profile(index: String) -> CmdResult {
|
||||||
let should_update = wrap_err!({ Config::profiles().data().delete_item(index) })?;
|
let should_update = wrap_err!({ Config::profiles().data().delete_item(index) })?;
|
||||||
|
|
||||||
|
// 删除后自动清理冗余文件
|
||||||
|
let _ = Config::profiles().latest().auto_cleanup();
|
||||||
|
|
||||||
if should_update {
|
if should_update {
|
||||||
wrap_err!(CoreManager::global().update_config().await)?;
|
wrap_err!(CoreManager::global().update_config().await)?;
|
||||||
handle::Handle::refresh_clash();
|
handle::Handle::refresh_clash();
|
||||||
@@ -76,36 +150,188 @@ pub async fn delete_profile(index: String) -> CmdResult {
|
|||||||
|
|
||||||
/// 修改profiles的配置
|
/// 修改profiles的配置
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn patch_profiles_config(
|
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||||
profiles: IProfiles
|
// 获取互斥锁,防止并发执行
|
||||||
) -> CmdResult<bool> {
|
let _guard = PROFILE_UPDATE_MUTEX.lock().await;
|
||||||
println!("[cmd配置patch] 开始修改配置文件");
|
|
||||||
|
logging!(info, Type::Cmd, true, "开始修改配置文件");
|
||||||
|
|
||||||
// 保存当前配置,以便在验证失败时恢复
|
// 保存当前配置,以便在验证失败时恢复
|
||||||
let current_profile = Config::profiles().latest().current.clone();
|
let current_profile = Config::profiles().latest().current.clone();
|
||||||
println!("[cmd配置patch] 当前配置: {:?}", current_profile);
|
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 config_file_result = {
|
||||||
|
let profiles_config = Config::profiles();
|
||||||
|
let profiles_data = profiles_config.latest();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 超时保护
|
||||||
|
let file_read_result = tokio::time::timeout(
|
||||||
|
Duration::from_secs(5),
|
||||||
|
tokio::fs::read_to_string(&file_path),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match file_read_result {
|
||||||
|
Ok(Ok(content)) => {
|
||||||
|
let yaml_parse_result = tokio::task::spawn_blocking(move || {
|
||||||
|
serde_yaml::from_str::<serde_yaml::Value>(&content)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match yaml_parse_result {
|
||||||
|
Ok(Ok(_)) => {
|
||||||
|
logging!(info, Type::Cmd, true, "目标配置文件语法正确");
|
||||||
|
}
|
||||||
|
Ok(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(join_err) => {
|
||||||
|
let error_msg = format!("YAML解析任务失败: {}", join_err);
|
||||||
|
logging!(error, Type::Cmd, true, "{}", error_msg);
|
||||||
|
handle::Handle::notice_message(
|
||||||
|
"config_validate::yaml_parse_error",
|
||||||
|
&error_msg,
|
||||||
|
);
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(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);
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
let error_msg = "读取配置文件超时(5秒)".to_string();
|
||||||
|
logging!(error, Type::Cmd, true, "{}", error_msg);
|
||||||
|
handle::Handle::notice_message(
|
||||||
|
"config_validate::file_read_timeout",
|
||||||
|
&error_msg,
|
||||||
|
);
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 更新profiles配置
|
// 更新profiles配置
|
||||||
println!("[cmd配置patch] 正在更新配置草稿");
|
logging!(info, Type::Cmd, true, "正在更新配置草稿");
|
||||||
wrap_err!({ Config::profiles().draft().patch_config(profiles) })?;
|
|
||||||
|
let current_value = profiles.current.clone();
|
||||||
|
|
||||||
|
let _ = Config::profiles().draft().patch_config(profiles);
|
||||||
|
|
||||||
|
// 为配置更新添加超时保护
|
||||||
|
let update_result = tokio::time::timeout(
|
||||||
|
Duration::from_secs(30), // 30秒超时
|
||||||
|
CoreManager::global().update_config(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
// 更新配置并进行验证
|
// 更新配置并进行验证
|
||||||
match CoreManager::global().update_config().await {
|
match update_result {
|
||||||
Ok((true, _)) => {
|
Ok(Ok((true, _))) => {
|
||||||
println!("[cmd配置patch] 配置更新成功");
|
logging!(info, Type::Cmd, true, "配置更新成功");
|
||||||
handle::Handle::refresh_clash();
|
|
||||||
let _ = tray::Tray::global().update_tooltip();
|
|
||||||
Config::profiles().apply();
|
Config::profiles().apply();
|
||||||
wrap_err!(Config::profiles().data().save_file())?;
|
handle::Handle::refresh_clash();
|
||||||
|
|
||||||
|
// 强制刷新代理缓存,确保profile切换后立即获取最新节点数据
|
||||||
|
crate::process::AsyncHandler::spawn(|| async move {
|
||||||
|
if let Err(e) = super::proxy::force_refresh_proxies().await {
|
||||||
|
log::warn!(target: "app", "强制刷新代理缓存失败: {}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
crate::process::AsyncHandler::spawn(|| async move {
|
||||||
|
if let Err(e) = Tray::global().update_tooltip() {
|
||||||
|
log::warn!(target: "app", "异步更新托盘提示失败: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = Tray::global().update_menu() {
|
||||||
|
log::warn!(target: "app", "异步更新托盘菜单失败: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存配置文件
|
||||||
|
if let Err(e) = Config::profiles().data().save_file() {
|
||||||
|
log::warn!(target: "app", "异步保存配置文件失败: {}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 立即通知前端配置变更
|
||||||
|
if let Some(current) = ¤t_value {
|
||||||
|
logging!(info, Type::Cmd, true, "向前端发送配置变更事件: {}", current);
|
||||||
|
handle::Handle::notify_profile_changed(current.clone());
|
||||||
|
}
|
||||||
|
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
Ok((false, error_msg)) => {
|
Ok(Ok((false, error_msg))) => {
|
||||||
println!("[cmd配置patch] 配置验证失败: {}", error_msg);
|
logging!(warn, Type::Cmd, true, "配置验证失败: {}", error_msg);
|
||||||
Config::profiles().discard();
|
Config::profiles().discard();
|
||||||
|
|
||||||
// 如果验证失败,恢复到之前的配置
|
// 如果验证失败,恢复到之前的配置
|
||||||
if let Some(prev_profile) = current_profile {
|
if let Some(prev_profile) = current_profile {
|
||||||
println!("[cmd配置patch] 尝试恢复到之前的配置: {}", prev_profile);
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Cmd,
|
||||||
|
true,
|
||||||
|
"尝试恢复到之前的配置: {}",
|
||||||
|
prev_profile
|
||||||
|
);
|
||||||
let restore_profiles = IProfiles {
|
let restore_profiles = IProfiles {
|
||||||
current: Some(prev_profile),
|
current: Some(prev_profile),
|
||||||
items: None,
|
items: None,
|
||||||
@@ -113,18 +339,49 @@ pub async fn patch_profiles_config(
|
|||||||
// 静默恢复,不触发验证
|
// 静默恢复,不触发验证
|
||||||
wrap_err!({ Config::profiles().draft().patch_config(restore_profiles) })?;
|
wrap_err!({ Config::profiles().draft().patch_config(restore_profiles) })?;
|
||||||
Config::profiles().apply();
|
Config::profiles().apply();
|
||||||
wrap_err!(Config::profiles().data().save_file())?;
|
|
||||||
println!("[cmd配置patch] 成功恢复到之前的配置");
|
crate::process::AsyncHandler::spawn(|| async move {
|
||||||
|
if let Err(e) = Config::profiles().data().save_file() {
|
||||||
|
log::warn!(target: "app", "异步保存恢复配置文件失败: {}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
logging!(info, Type::Cmd, true, "成功恢复到之前的配置");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送验证错误通知
|
// 发送验证错误通知
|
||||||
handle::Handle::notice_message("config_validate::error", &error_msg);
|
handle::Handle::notice_message("config_validate::error", &error_msg);
|
||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Ok(Err(e)) => {
|
||||||
println!("[cmd配置patch] 更新过程发生错误: {}", e);
|
logging!(warn, Type::Cmd, true, "更新过程发生错误: {}", e);
|
||||||
Config::profiles().discard();
|
Config::profiles().discard();
|
||||||
handle::Handle::notice_message("config_validate::boot_error", &e.to_string());
|
handle::Handle::notice_message("config_validate::boot_error", e.to_string());
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// 超时处理
|
||||||
|
let timeout_msg = "配置更新超时(30秒),可能是配置验证或核心通信阻塞";
|
||||||
|
logging!(error, Type::Cmd, true, "{}", timeout_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();
|
||||||
|
}
|
||||||
|
|
||||||
|
handle::Handle::notice_message("config_validate::timeout", timeout_msg);
|
||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,16 +391,47 @@ pub async fn patch_profiles_config(
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn patch_profiles_config_by_profile_index(
|
pub async fn patch_profiles_config_by_profile_index(
|
||||||
_app_handle: tauri::AppHandle,
|
_app_handle: tauri::AppHandle,
|
||||||
profile_index: String
|
profile_index: String,
|
||||||
) -> CmdResult<bool> {
|
) -> CmdResult<bool> {
|
||||||
let profiles = IProfiles{current: Some(profile_index), items: None};
|
logging!(info, Type::Cmd, true, "切换配置到: {}", profile_index);
|
||||||
|
|
||||||
|
let profiles = IProfiles {
|
||||||
|
current: Some(profile_index),
|
||||||
|
items: None,
|
||||||
|
};
|
||||||
patch_profiles_config(profiles).await
|
patch_profiles_config(profiles).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 修改某个profile item的
|
/// 修改某个profile item的
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
|
pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
|
||||||
wrap_err!(Config::profiles().data().patch_item(index, profile))?;
|
// 保存修改前检查是否有更新 update_interval
|
||||||
|
let update_interval_changed =
|
||||||
|
if let Ok(old_profile) = Config::profiles().latest().get_item(&index) {
|
||||||
|
let old_interval = old_profile.option.as_ref().and_then(|o| o.update_interval);
|
||||||
|
let new_interval = profile.option.as_ref().and_then(|o| o.update_interval);
|
||||||
|
old_interval != new_interval
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存修改
|
||||||
|
wrap_err!(Config::profiles().data().patch_item(index.clone(), profile))?;
|
||||||
|
|
||||||
|
// 如果更新间隔变更,异步刷新定时器
|
||||||
|
if update_interval_changed {
|
||||||
|
let index_clone = index.clone();
|
||||||
|
crate::process::AsyncHandler::spawn(move || async move {
|
||||||
|
logging!(info, Type::Timer, "定时器更新间隔已变更,正在刷新定时器...");
|
||||||
|
if let Err(e) = crate::core::Timer::global().refresh() {
|
||||||
|
logging!(error, Type::Timer, "刷新定时器失败: {}", e);
|
||||||
|
} else {
|
||||||
|
// 刷新成功后发送自定义事件,不触发配置重载
|
||||||
|
crate::core::handle::Handle::notify_timer_updated(index_clone);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,3 +462,11 @@ pub fn read_profile_file(index: String) -> CmdResult<String> {
|
|||||||
let data = wrap_err!(item.read_file())?;
|
let data = wrap_err!(item.read_file())?;
|
||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 获取下一次更新时间
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_next_update_time(uid: String) -> CmdResult<Option<i64>> {
|
||||||
|
let timer = Timer::global();
|
||||||
|
let next_time = timer.get_next_update_time(&uid);
|
||||||
|
Ok(next_time)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,16 +1,99 @@
|
|||||||
use super::CmdResult;
|
use super::CmdResult;
|
||||||
use crate::module::mihomo::MihomoManager;
|
use crate::{core::handle, module::mihomo::MihomoManager, state::proxy::CmdProxyState};
|
||||||
|
use std::{
|
||||||
|
sync::Mutex,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
use tauri::Manager;
|
||||||
|
|
||||||
|
const PROVIDERS_REFRESH_INTERVAL: Duration = Duration::from_secs(3);
|
||||||
|
const PROXIES_REFRESH_INTERVAL: Duration = Duration::from_secs(1);
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_proxies() -> CmdResult<serde_json::Value> {
|
pub async fn get_proxies() -> CmdResult<serde_json::Value> {
|
||||||
let mannager = MihomoManager::global();
|
let manager = MihomoManager::global();
|
||||||
mannager.refresh_proxies().await.unwrap();
|
|
||||||
Ok(mannager.get_proxies())
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||||
|
let cmd_proxy_state = app_handle.state::<Mutex<CmdProxyState>>();
|
||||||
|
|
||||||
|
let should_refresh = {
|
||||||
|
let mut state = cmd_proxy_state.lock().unwrap();
|
||||||
|
let now = Instant::now();
|
||||||
|
if now.duration_since(state.last_refresh_time) > PROXIES_REFRESH_INTERVAL {
|
||||||
|
state.need_refresh = true;
|
||||||
|
state.last_refresh_time = now;
|
||||||
|
}
|
||||||
|
state.need_refresh
|
||||||
|
};
|
||||||
|
|
||||||
|
if should_refresh {
|
||||||
|
let proxies = manager.get_refresh_proxies().await?;
|
||||||
|
{
|
||||||
|
let mut state = cmd_proxy_state.lock().unwrap();
|
||||||
|
state.proxies = Box::new(proxies);
|
||||||
|
state.need_refresh = false;
|
||||||
|
}
|
||||||
|
log::debug!(target: "app", "proxies刷新成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
let proxies = {
|
||||||
|
let state = cmd_proxy_state.lock().unwrap();
|
||||||
|
state.proxies.clone()
|
||||||
|
};
|
||||||
|
Ok(*proxies)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 强制刷新代理缓存用于profile切换
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn force_refresh_proxies() -> CmdResult<serde_json::Value> {
|
||||||
|
let manager = MihomoManager::global();
|
||||||
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||||
|
let cmd_proxy_state = app_handle.state::<Mutex<CmdProxyState>>();
|
||||||
|
|
||||||
|
log::debug!(target: "app", "强制刷新代理缓存");
|
||||||
|
|
||||||
|
let proxies = manager.get_refresh_proxies().await?;
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut state = cmd_proxy_state.lock().unwrap();
|
||||||
|
state.proxies = Box::new(proxies.clone());
|
||||||
|
state.need_refresh = false;
|
||||||
|
state.last_refresh_time = Instant::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
log::debug!(target: "app", "强制刷新代理缓存完成");
|
||||||
|
Ok(proxies)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
|
pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
|
||||||
let mannager = MihomoManager::global();
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||||
mannager.refresh_providers_proxies().await.unwrap();
|
let cmd_proxy_state = app_handle.state::<Mutex<CmdProxyState>>();
|
||||||
Ok(mannager.get_providers_proxies())
|
|
||||||
|
let should_refresh = {
|
||||||
|
let mut state = cmd_proxy_state.lock().unwrap();
|
||||||
|
let now = Instant::now();
|
||||||
|
if now.duration_since(state.last_refresh_time) > PROVIDERS_REFRESH_INTERVAL {
|
||||||
|
state.need_refresh = true;
|
||||||
|
state.last_refresh_time = now;
|
||||||
|
}
|
||||||
|
state.need_refresh
|
||||||
|
};
|
||||||
|
|
||||||
|
if should_refresh {
|
||||||
|
let manager = MihomoManager::global();
|
||||||
|
let providers = manager.get_providers_proxies().await?;
|
||||||
|
{
|
||||||
|
let mut state = cmd_proxy_state.lock().unwrap();
|
||||||
|
state.providers_proxies = Box::new(providers);
|
||||||
|
state.need_refresh = false;
|
||||||
|
}
|
||||||
|
log::debug!(target: "app", "providers_proxies刷新成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
let providers_proxies = {
|
||||||
|
let state = cmd_proxy_state.lock().unwrap();
|
||||||
|
state.providers_proxies.clone()
|
||||||
|
};
|
||||||
|
Ok(*providers_proxies)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
use crate::{
|
|
||||||
config::*,
|
|
||||||
wrap_err,
|
|
||||||
};
|
|
||||||
use super::CmdResult;
|
use super::CmdResult;
|
||||||
|
use crate::{config::*, wrap_err};
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use std::collections::HashMap;
|
|
||||||
use serde_yaml::Mapping;
|
use serde_yaml::Mapping;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
/// 获取运行时配置
|
/// 获取运行时配置
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
use super::CmdResult;
|
||||||
use crate::{
|
use crate::{
|
||||||
config::*,
|
config::*,
|
||||||
core::*,
|
core::*,
|
||||||
utils::dirs,
|
logging,
|
||||||
|
utils::{dirs, logging::Type},
|
||||||
wrap_err,
|
wrap_err,
|
||||||
};
|
};
|
||||||
use super::CmdResult;
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
/// 保存profiles的配置
|
/// 保存profiles的配置
|
||||||
@@ -20,7 +21,7 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|
|||||||
let profiles_guard = profiles.latest();
|
let profiles_guard = profiles.latest();
|
||||||
let item = wrap_err!(profiles_guard.get_item(&index))?;
|
let item = wrap_err!(profiles_guard.get_item(&index))?;
|
||||||
// 确定是否为merge类型文件
|
// 确定是否为merge类型文件
|
||||||
let is_merge = item.itype.as_ref().map_or(false, |t| t == "merge");
|
let is_merge = item.itype.as_ref().is_some_and(|t| t == "merge");
|
||||||
let content = wrap_err!(item.read_file())?;
|
let content = wrap_err!(item.read_file())?;
|
||||||
let path = item.file.clone().ok_or("file field is null")?;
|
let path = item.file.clone().ok_or("file field is null")?;
|
||||||
let profiles_dir = wrap_err!(dirs::app_profiles_dir())?;
|
let profiles_dir = wrap_err!(dirs::app_profiles_dir())?;
|
||||||
@@ -29,25 +30,56 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|
|||||||
|
|
||||||
// 保存新的配置文件
|
// 保存新的配置文件
|
||||||
wrap_err!(fs::write(&file_path, file_data.clone().unwrap()))?;
|
wrap_err!(fs::write(&file_path, file_data.clone().unwrap()))?;
|
||||||
|
|
||||||
let file_path_str = file_path.to_string_lossy().to_string();
|
let file_path_str = file_path.to_string_lossy().to_string();
|
||||||
println!("[cmd配置save] 开始验证配置文件: {}, 是否为merge文件: {}", file_path_str, is_merge_file);
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Config,
|
||||||
|
true,
|
||||||
|
"[cmd配置save] 开始验证配置文件: {}, 是否为merge文件: {}",
|
||||||
|
file_path_str,
|
||||||
|
is_merge_file
|
||||||
|
);
|
||||||
|
|
||||||
// 对于 merge 文件,只进行语法验证,不进行后续内核验证
|
// 对于 merge 文件,只进行语法验证,不进行后续内核验证
|
||||||
if is_merge_file {
|
if is_merge_file {
|
||||||
println!("[cmd配置save] 检测到merge文件,只进行语法验证");
|
logging!(
|
||||||
match CoreManager::global().validate_config_file(&file_path_str, Some(true)).await {
|
info,
|
||||||
|
Type::Config,
|
||||||
|
true,
|
||||||
|
"[cmd配置save] 检测到merge文件,只进行语法验证"
|
||||||
|
);
|
||||||
|
match CoreManager::global()
|
||||||
|
.validate_config_file(&file_path_str, Some(true))
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok((true, _)) => {
|
Ok((true, _)) => {
|
||||||
println!("[cmd配置save] merge文件语法验证通过");
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Config,
|
||||||
|
true,
|
||||||
|
"[cmd配置save] merge文件语法验证通过"
|
||||||
|
);
|
||||||
// 成功后尝试更新整体配置
|
// 成功后尝试更新整体配置
|
||||||
if let Err(e) = CoreManager::global().update_config().await {
|
if let Err(e) = CoreManager::global().update_config().await {
|
||||||
println!("[cmd配置save] 更新整体配置时发生错误: {}", e);
|
logging!(
|
||||||
log::warn!(target: "app", "更新整体配置时发生错误: {}", e);
|
warn,
|
||||||
|
Type::Config,
|
||||||
|
true,
|
||||||
|
"[cmd配置save] 更新整体配置时发生错误: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
Ok((false, error_msg)) => {
|
Ok((false, error_msg)) => {
|
||||||
println!("[cmd配置save] merge文件语法验证失败: {}", error_msg);
|
logging!(
|
||||||
|
warn,
|
||||||
|
Type::Config,
|
||||||
|
true,
|
||||||
|
"[cmd配置save] merge文件语法验证失败: {}",
|
||||||
|
error_msg
|
||||||
|
);
|
||||||
// 恢复原始配置文件
|
// 恢复原始配置文件
|
||||||
wrap_err!(fs::write(&file_path, original_content))?;
|
wrap_err!(fs::write(&file_path, original_content))?;
|
||||||
// 发送合并文件专用错误通知
|
// 发送合并文件专用错误通知
|
||||||
@@ -56,53 +88,75 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("[cmd配置save] 验证过程发生错误: {}", e);
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Config,
|
||||||
|
true,
|
||||||
|
"[cmd配置save] 验证过程发生错误: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
// 恢复原始配置文件
|
// 恢复原始配置文件
|
||||||
wrap_err!(fs::write(&file_path, original_content))?;
|
wrap_err!(fs::write(&file_path, original_content))?;
|
||||||
return Err(e.to_string());
|
return Err(e.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 非merge文件使用完整验证流程
|
// 非merge文件使用完整验证流程
|
||||||
match CoreManager::global().validate_config_file(&file_path_str, None).await {
|
match CoreManager::global()
|
||||||
|
.validate_config_file(&file_path_str, None)
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok((true, _)) => {
|
Ok((true, _)) => {
|
||||||
println!("[cmd配置save] 验证成功");
|
logging!(info, Type::Config, true, "[cmd配置save] 验证成功");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Ok((false, error_msg)) => {
|
Ok((false, error_msg)) => {
|
||||||
println!("[cmd配置save] 验证失败: {}", error_msg);
|
logging!(
|
||||||
|
warn,
|
||||||
|
Type::Config,
|
||||||
|
true,
|
||||||
|
"[cmd配置save] 验证失败: {}",
|
||||||
|
error_msg
|
||||||
|
);
|
||||||
// 恢复原始配置文件
|
// 恢复原始配置文件
|
||||||
wrap_err!(fs::write(&file_path, original_content))?;
|
wrap_err!(fs::write(&file_path, original_content))?;
|
||||||
|
|
||||||
// 智能判断错误类型
|
// 智能判断错误类型
|
||||||
let is_script_error = file_path_str.ends_with(".js") ||
|
let is_script_error = file_path_str.ends_with(".js")
|
||||||
error_msg.contains("Script syntax error") ||
|
|| error_msg.contains("Script syntax error")
|
||||||
error_msg.contains("Script must contain a main function") ||
|
|| error_msg.contains("Script must contain a main function")
|
||||||
error_msg.contains("Failed to read script file");
|
|| error_msg.contains("Failed to read script file");
|
||||||
|
|
||||||
if error_msg.contains("YAML syntax error") ||
|
if error_msg.contains("YAML syntax error")
|
||||||
error_msg.contains("Failed to read file:") ||
|
|| error_msg.contains("Failed to read file:")
|
||||||
(!file_path_str.ends_with(".js") && !is_script_error) {
|
|| (!file_path_str.ends_with(".js") && !is_script_error)
|
||||||
|
{
|
||||||
// 普通YAML错误使用YAML通知处理
|
// 普通YAML错误使用YAML通知处理
|
||||||
println!("[cmd配置save] YAML配置文件验证失败,发送通知");
|
log::info!(target: "app", "[cmd配置save] YAML配置文件验证失败,发送通知");
|
||||||
let result = (false, error_msg.clone());
|
let result = (false, error_msg.clone());
|
||||||
crate::cmd::validate::handle_yaml_validation_notice(&result, "YAML配置文件");
|
crate::cmd::validate::handle_yaml_validation_notice(&result, "YAML配置文件");
|
||||||
} else if is_script_error {
|
} else if is_script_error {
|
||||||
// 脚本错误使用专门的通知处理
|
// 脚本错误使用专门的通知处理
|
||||||
println!("[cmd配置save] 脚本文件验证失败,发送通知");
|
log::info!(target: "app", "[cmd配置save] 脚本文件验证失败,发送通知");
|
||||||
let result = (false, error_msg.clone());
|
let result = (false, error_msg.clone());
|
||||||
crate::cmd::validate::handle_script_validation_notice(&result, "脚本文件");
|
crate::cmd::validate::handle_script_validation_notice(&result, "脚本文件");
|
||||||
} else {
|
} else {
|
||||||
// 普通配置错误使用一般通知
|
// 普通配置错误使用一般通知
|
||||||
println!("[cmd配置save] 其他类型验证失败,发送一般通知");
|
log::info!(target: "app", "[cmd配置save] 其他类型验证失败,发送一般通知");
|
||||||
handle::Handle::notice_message("config_validate::error", &error_msg);
|
handle::Handle::notice_message("config_validate::error", &error_msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("[cmd配置save] 验证过程发生错误: {}", e);
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Config,
|
||||||
|
true,
|
||||||
|
"[cmd配置save] 验证过程发生错误: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
// 恢复原始配置文件
|
// 恢复原始配置文件
|
||||||
wrap_err!(fs::write(&file_path, original_content))?;
|
wrap_err!(fs::write(&file_path, original_content))?;
|
||||||
Err(e.to_string())
|
Err(e.to_string())
|
||||||
|
|||||||
48
src-tauri/src/cmd/service.rs
Normal file
48
src-tauri/src/cmd/service.rs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn is_service_available() -> CmdResult<bool> {
|
||||||
|
service::is_service_available()
|
||||||
|
.await
|
||||||
|
.map(|_| true)
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
@@ -1,35 +1,94 @@
|
|||||||
use super::CmdResult;
|
use super::CmdResult;
|
||||||
use crate::core::handle;
|
use crate::{
|
||||||
use crate::module::sysinfo::PlatformSpecification;
|
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;
|
use tauri_plugin_clipboard_manager::ClipboardExt;
|
||||||
use crate::{core::{self, CoreManager, service}, wrap_err};
|
|
||||||
|
// 存储应用启动时间的全局变量
|
||||||
|
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]
|
#[tauri::command]
|
||||||
pub async fn export_diagnostic_info() -> CmdResult<()> {
|
pub async fn export_diagnostic_info() -> CmdResult<()> {
|
||||||
let sysinfo = PlatformSpecification::new();
|
let sysinfo = PlatformSpecification::new_async().await;
|
||||||
let info = format!("{:?}", sysinfo);
|
let info = format!("{:?}", sysinfo);
|
||||||
|
|
||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||||
let cliboard = app_handle.clipboard();
|
let cliboard = app_handle.clipboard();
|
||||||
|
if cliboard.write_text(info).is_err() {
|
||||||
if let Err(_) = cliboard.write_text(info) {
|
|
||||||
log::error!(target: "app", "Failed to write to clipboard");
|
log::error!(target: "app", "Failed to write to clipboard");
|
||||||
}
|
}
|
||||||
Ok(())
|
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]
|
#[tauri::command]
|
||||||
pub async fn get_running_mode() -> Result<String, String> {
|
pub async fn get_running_mode() -> Result<String, String> {
|
||||||
match CoreManager::global().get_running_mode().await {
|
Ok(CoreManager::global().get_running_mode().await.to_string())
|
||||||
core::RunningMode::Service => Ok("service".to_string()),
|
|
||||||
core::RunningMode::Sidecar => Ok("sidecar".to_string()),
|
|
||||||
core::RunningMode::NotRunning => Ok("not_running".to_string()),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 安装/重装系统服务
|
/// 获取应用的运行时间(毫秒)
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn install_service() -> CmdResult {
|
pub fn get_app_uptime() -> CmdResult<i64> {
|
||||||
wrap_err!(service::reinstall_service().await)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ use super::CmdResult;
|
|||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
mod platform {
|
mod platform {
|
||||||
use super::CmdResult;
|
use super::CmdResult;
|
||||||
use crate::core::win_uwp;
|
use crate::{core::win_uwp, wrap_err};
|
||||||
use crate::wrap_err;
|
|
||||||
|
|
||||||
pub async fn invoke_uwp_tool() -> CmdResult {
|
pub async fn invoke_uwp_tool() -> CmdResult {
|
||||||
wrap_err!(win_uwp::invoke_uwptools().await)
|
wrap_err!(win_uwp::invoke_uwptools().await)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::core::*;
|
|
||||||
use super::CmdResult;
|
use super::CmdResult;
|
||||||
|
use crate::{core::*, logging, utils::logging::Type};
|
||||||
|
|
||||||
/// 发送脚本验证通知消息
|
/// 发送脚本验证通知消息
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -13,7 +13,7 @@ pub async fn script_validate_notice(status: String, msg: String) -> CmdResult {
|
|||||||
pub fn handle_script_validation_notice(result: &(bool, String), file_type: &str) {
|
pub fn handle_script_validation_notice(result: &(bool, String), file_type: &str) {
|
||||||
if !result.0 {
|
if !result.0 {
|
||||||
let error_msg = &result.1;
|
let error_msg = &result.1;
|
||||||
|
|
||||||
// 根据错误消息内容判断错误类型
|
// 根据错误消息内容判断错误类型
|
||||||
let status = if error_msg.starts_with("File not found:") {
|
let status = if error_msg.starts_with("File not found:") {
|
||||||
"config_validate::file_not_found"
|
"config_validate::file_not_found"
|
||||||
@@ -27,8 +27,15 @@ pub fn handle_script_validation_notice(result: &(bool, String), file_type: &str)
|
|||||||
// 如果是其他类型错误,作为一般脚本错误处理
|
// 如果是其他类型错误,作为一般脚本错误处理
|
||||||
"config_validate::script_error"
|
"config_validate::script_error"
|
||||||
};
|
};
|
||||||
|
|
||||||
log::warn!(target: "app", "{} 验证失败: {}", file_type, error_msg);
|
logging!(
|
||||||
|
warn,
|
||||||
|
Type::Config,
|
||||||
|
true,
|
||||||
|
"{} 验证失败: {}",
|
||||||
|
file_type,
|
||||||
|
error_msg
|
||||||
|
);
|
||||||
handle::Handle::notice_message(status, error_msg);
|
handle::Handle::notice_message(status, error_msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,16 +43,25 @@ pub fn handle_script_validation_notice(result: &(bool, String), file_type: &str)
|
|||||||
/// 验证指定脚本文件
|
/// 验证指定脚本文件
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn validate_script_file(file_path: String) -> CmdResult<bool> {
|
pub async fn validate_script_file(file_path: String) -> CmdResult<bool> {
|
||||||
log::info!(target: "app", "验证脚本文件: {}", file_path);
|
logging!(info, Type::Config, true, "验证脚本文件: {}", file_path);
|
||||||
|
|
||||||
match CoreManager::global().validate_config_file(&file_path, None).await {
|
match CoreManager::global()
|
||||||
|
.validate_config_file(&file_path, None)
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(result) => {
|
Ok(result) => {
|
||||||
handle_script_validation_notice(&result, "脚本文件");
|
handle_script_validation_notice(&result, "脚本文件");
|
||||||
Ok(result.0) // 返回验证结果布尔值
|
Ok(result.0) // 返回验证结果布尔值
|
||||||
},
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let error_msg = e.to_string();
|
let error_msg = e.to_string();
|
||||||
log::error!(target: "app", "验证脚本文件过程发生错误: {}", error_msg);
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Config,
|
||||||
|
true,
|
||||||
|
"验证脚本文件过程发生错误: {}",
|
||||||
|
error_msg
|
||||||
|
);
|
||||||
handle::Handle::notice_message("config_validate::process_terminated", &error_msg);
|
handle::Handle::notice_message("config_validate::process_terminated", &error_msg);
|
||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
@@ -57,11 +73,18 @@ pub async fn validate_script_file(file_path: String) -> CmdResult<bool> {
|
|||||||
pub fn handle_yaml_validation_notice(result: &(bool, String), file_type: &str) {
|
pub fn handle_yaml_validation_notice(result: &(bool, String), file_type: &str) {
|
||||||
if !result.0 {
|
if !result.0 {
|
||||||
let error_msg = &result.1;
|
let error_msg = &result.1;
|
||||||
println!("[通知] 处理{}验证错误: {}", file_type, error_msg);
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Config,
|
||||||
|
true,
|
||||||
|
"[通知] 处理{}验证错误: {}",
|
||||||
|
file_type,
|
||||||
|
error_msg
|
||||||
|
);
|
||||||
|
|
||||||
// 检查是否为merge文件
|
// 检查是否为merge文件
|
||||||
let is_merge_file = file_type.contains("合并");
|
let is_merge_file = file_type.contains("合并");
|
||||||
|
|
||||||
// 根据错误消息内容判断错误类型
|
// 根据错误消息内容判断错误类型
|
||||||
let status = if error_msg.starts_with("File not found:") {
|
let status = if error_msg.starts_with("File not found:") {
|
||||||
"config_validate::file_not_found"
|
"config_validate::file_not_found"
|
||||||
@@ -93,9 +116,23 @@ pub fn handle_yaml_validation_notice(result: &(bool, String), file_type: &str) {
|
|||||||
"config_validate::yaml_error"
|
"config_validate::yaml_error"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
log::warn!(target: "app", "{} 验证失败: {}", file_type, error_msg);
|
logging!(
|
||||||
println!("[通知] 发送通知: status={}, msg={}", status, error_msg);
|
warn,
|
||||||
|
Type::Config,
|
||||||
|
true,
|
||||||
|
"{} 验证失败: {}",
|
||||||
|
file_type,
|
||||||
|
error_msg
|
||||||
|
);
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Config,
|
||||||
|
true,
|
||||||
|
"[通知] 发送通知: status={}, msg={}",
|
||||||
|
status,
|
||||||
|
error_msg
|
||||||
|
);
|
||||||
handle::Handle::notice_message(status, error_msg);
|
handle::Handle::notice_message(status, error_msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
use crate::{
|
|
||||||
config::*,
|
|
||||||
feat,
|
|
||||||
wrap_err,
|
|
||||||
};
|
|
||||||
use super::CmdResult;
|
use super::CmdResult;
|
||||||
|
use crate::{config::*, feat, wrap_err};
|
||||||
|
|
||||||
/// 获取Verge配置
|
/// 获取Verge配置
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_verge_config() -> CmdResult<IVergeResponse> {
|
pub fn get_verge_config() -> CmdResult<IVergeResponse> {
|
||||||
let verge = Config::verge();
|
let verge = Config::verge();
|
||||||
let verge_data = verge.data().clone();
|
let verge_data = verge.data().clone();
|
||||||
Ok(IVergeResponse::from(verge_data))
|
Ok(IVergeResponse::from(*verge_data))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 修改Verge配置
|
/// 修改Verge配置
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
use crate::{
|
|
||||||
core,
|
|
||||||
config::*,
|
|
||||||
feat,
|
|
||||||
wrap_err,
|
|
||||||
};
|
|
||||||
use super::CmdResult;
|
use super::CmdResult;
|
||||||
|
use crate::{config::*, core, feat, wrap_err};
|
||||||
use reqwest_dav::list_cmd::ListFile;
|
use reqwest_dav::list_cmd::ListFile;
|
||||||
|
|
||||||
/// 保存 WebDAV 配置
|
/// 保存 WebDAV 配置
|
||||||
|
|||||||
@@ -20,6 +20,12 @@ impl IClashTemp {
|
|||||||
map.insert(key.clone(), template.0.get(key).unwrap().clone());
|
map.insert(key.clone(), template.0.get(key).unwrap().clone());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// 确保 secret 字段存在且不为空
|
||||||
|
if let Some(Value::String(s)) = map.get_mut("secret") {
|
||||||
|
if s.is_empty() {
|
||||||
|
*s = "set-your-secret".to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
Self(Self::guard(map))
|
Self(Self::guard(map))
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -32,13 +38,13 @@ impl IClashTemp {
|
|||||||
pub fn template() -> Self {
|
pub fn template() -> Self {
|
||||||
let mut map = Mapping::new();
|
let mut map = Mapping::new();
|
||||||
let mut tun = Mapping::new();
|
let mut tun = Mapping::new();
|
||||||
|
let mut cors_map = Mapping::new();
|
||||||
tun.insert("enable".into(), false.into());
|
tun.insert("enable".into(), false.into());
|
||||||
tun.insert("stack".into(), "mixed".into());
|
tun.insert("stack".into(), "gvisor".into());
|
||||||
tun.insert("auto-route".into(), true.into());
|
tun.insert("auto-route".into(), true.into());
|
||||||
tun.insert("strict-route".into(), false.into());
|
tun.insert("strict-route".into(), false.into());
|
||||||
tun.insert("auto-detect-interface".into(), true.into());
|
tun.insert("auto-detect-interface".into(), true.into());
|
||||||
tun.insert("dns-hijack".into(), vec!["any:53"].into());
|
tun.insert("dns-hijack".into(), vec!["any:53"].into());
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
map.insert("redir-port".into(), 7895.into());
|
map.insert("redir-port".into(), 7895.into());
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
@@ -46,14 +52,27 @@ impl IClashTemp {
|
|||||||
map.insert("mixed-port".into(), 7897.into());
|
map.insert("mixed-port".into(), 7897.into());
|
||||||
map.insert("socks-port".into(), 7898.into());
|
map.insert("socks-port".into(), 7898.into());
|
||||||
map.insert("port".into(), 7899.into());
|
map.insert("port".into(), 7899.into());
|
||||||
map.insert("log-level".into(), "info".into());
|
map.insert("log-level".into(), "warning".into());
|
||||||
map.insert("allow-lan".into(), false.into());
|
map.insert("allow-lan".into(), false.into());
|
||||||
|
map.insert("ipv6".into(), true.into());
|
||||||
map.insert("mode".into(), "rule".into());
|
map.insert("mode".into(), "rule".into());
|
||||||
map.insert("external-controller".into(), "127.0.0.1:9097".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-private-network".into(), true.into());
|
||||||
cors_map.insert("allow-origins".into(), vec!["*"].into());
|
cors_map.insert(
|
||||||
map.insert("secret".into(), "".into());
|
"allow-origins".into(),
|
||||||
|
vec![
|
||||||
|
"tauri://localhost",
|
||||||
|
"http://tauri.localhost",
|
||||||
|
// Only enable this in dev mode
|
||||||
|
#[cfg(feature = "verge-dev")]
|
||||||
|
"http://localhost:3000",
|
||||||
|
"https://yacd.metacubex.one",
|
||||||
|
"https://metacubex.github.io",
|
||||||
|
"https://board.zash.run.place",
|
||||||
|
]
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
map.insert("secret".into(), "set-your-secret".into());
|
||||||
map.insert("tun".into(), tun.into());
|
map.insert("tun".into(), tun.into());
|
||||||
map.insert("external-controller-cors".into(), cors_map.into());
|
map.insert("external-controller-cors".into(), cors_map.into());
|
||||||
map.insert("unified-delay".into(), true.into());
|
map.insert("unified-delay".into(), true.into());
|
||||||
@@ -77,6 +96,26 @@ impl IClashTemp {
|
|||||||
config.insert("socks-port".into(), socks_port.into());
|
config.insert("socks-port".into(), socks_port.into());
|
||||||
config.insert("port".into(), port.into());
|
config.insert("port".into(), port.into());
|
||||||
config.insert("external-controller".into(), ctrl.into());
|
config.insert("external-controller".into(), ctrl.into());
|
||||||
|
|
||||||
|
// 强制覆盖 external-controller-cors 字段,允许本地和 tauri 前端
|
||||||
|
let mut cors_map = Mapping::new();
|
||||||
|
cors_map.insert("allow-private-network".into(), true.into());
|
||||||
|
cors_map.insert(
|
||||||
|
"allow-origins".into(),
|
||||||
|
vec![
|
||||||
|
"tauri://localhost",
|
||||||
|
"http://tauri.localhost",
|
||||||
|
// Only enable this in dev mode
|
||||||
|
#[cfg(feature = "verge-dev")]
|
||||||
|
"http://localhost:3000",
|
||||||
|
"https://yacd.metacubex.one",
|
||||||
|
"https://metacubex.github.io",
|
||||||
|
"https://board.zash.run.place",
|
||||||
|
]
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
config.insert("external-controller-cors".into(), cors_map.into());
|
||||||
|
|
||||||
config
|
config
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,6 +355,13 @@ fn test_clash_info() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct IClashExternalControllerCors {
|
||||||
|
pub allow_origins: Option<Vec<String>>,
|
||||||
|
pub allow_private_network: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub struct IClash {
|
pub struct IClash {
|
||||||
@@ -329,6 +375,7 @@ pub struct IClash {
|
|||||||
pub dns: Option<IClashDNS>,
|
pub dns: Option<IClashDNS>,
|
||||||
pub tun: Option<IClashTUN>,
|
pub tun: Option<IClashTUN>,
|
||||||
pub interface_name: Option<String>,
|
pub interface_name: Option<String>,
|
||||||
|
pub external_controller_cors: Option<IClashExternalControllerCors>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
use super::{Draft, IClashTemp, IProfiles, IRuntime, IVerge};
|
use super::{Draft, IClashTemp, IProfiles, IRuntime, IVerge};
|
||||||
use crate::{
|
use crate::{
|
||||||
config::PrfItem,
|
config::PrfItem,
|
||||||
enhance,
|
|
||||||
utils::{dirs, help},
|
|
||||||
core::{handle, CoreManager},
|
core::{handle, CoreManager},
|
||||||
|
enhance, logging,
|
||||||
|
process::AsyncHandler,
|
||||||
|
utils::{dirs, help, logging::Type},
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
@@ -14,10 +15,10 @@ pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
|
|||||||
pub const CHECK_CONFIG: &str = "clash-verge-check.yaml";
|
pub const CHECK_CONFIG: &str = "clash-verge-check.yaml";
|
||||||
|
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
clash_config: Draft<IClashTemp>,
|
clash_config: Draft<Box<IClashTemp>>,
|
||||||
verge_config: Draft<IVerge>,
|
verge_config: Draft<Box<IVerge>>,
|
||||||
profiles_config: Draft<IProfiles>,
|
profiles_config: Draft<Box<IProfiles>>,
|
||||||
runtime_config: Draft<IRuntime>,
|
runtime_config: Draft<Box<IRuntime>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
@@ -25,26 +26,26 @@ impl Config {
|
|||||||
static CONFIG: OnceCell<Config> = OnceCell::new();
|
static CONFIG: OnceCell<Config> = OnceCell::new();
|
||||||
|
|
||||||
CONFIG.get_or_init(|| Config {
|
CONFIG.get_or_init(|| Config {
|
||||||
clash_config: Draft::from(IClashTemp::new()),
|
clash_config: Draft::from(Box::new(IClashTemp::new())),
|
||||||
verge_config: Draft::from(IVerge::new()),
|
verge_config: Draft::from(Box::new(IVerge::new())),
|
||||||
profiles_config: Draft::from(IProfiles::new()),
|
profiles_config: Draft::from(Box::new(IProfiles::new())),
|
||||||
runtime_config: Draft::from(IRuntime::new()),
|
runtime_config: Draft::from(Box::new(IRuntime::new())),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clash() -> Draft<IClashTemp> {
|
pub fn clash() -> Draft<Box<IClashTemp>> {
|
||||||
Self::global().clash_config.clone()
|
Self::global().clash_config.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn verge() -> Draft<IVerge> {
|
pub fn verge() -> Draft<Box<IVerge>> {
|
||||||
Self::global().verge_config.clone()
|
Self::global().verge_config.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn profiles() -> Draft<IProfiles> {
|
pub fn profiles() -> Draft<Box<IProfiles>> {
|
||||||
Self::global().profiles_config.clone()
|
Self::global().profiles_config.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn runtime() -> Draft<IRuntime> {
|
pub fn runtime() -> Draft<Box<IRuntime>> {
|
||||||
Self::global().runtime_config.clone()
|
Self::global().runtime_config.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,32 +67,41 @@ impl Config {
|
|||||||
let script_item = PrfItem::from_script(Some("Script".to_string()))?;
|
let script_item = PrfItem::from_script(Some("Script".to_string()))?;
|
||||||
Self::profiles().data().append_item(script_item.clone())?;
|
Self::profiles().data().append_item(script_item.clone())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成运行时配置
|
// 生成运行时配置
|
||||||
crate::log_err!(Self::generate().await);
|
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 config_result = Self::generate_file(ConfigType::Run);
|
||||||
|
|
||||||
let validation_result = if let Ok(_) = config_result {
|
let validation_result = if config_result.is_ok() {
|
||||||
// 验证配置文件
|
// 验证配置文件
|
||||||
println!("[首次启动] 开始验证配置");
|
logging!(info, Type::Config, true, "开始验证配置");
|
||||||
|
|
||||||
match CoreManager::global().validate_config().await {
|
match CoreManager::global().validate_config().await {
|
||||||
Ok((is_valid, error_msg)) => {
|
Ok((is_valid, error_msg)) => {
|
||||||
if !is_valid {
|
if !is_valid {
|
||||||
println!("[首次启动] 配置验证失败,使用默认最小配置启动: {}", error_msg);
|
logging!(
|
||||||
|
warn,
|
||||||
|
Type::Config,
|
||||||
|
true,
|
||||||
|
"[首次启动] 配置验证失败,使用默认最小配置启动: {}",
|
||||||
|
error_msg
|
||||||
|
);
|
||||||
CoreManager::global()
|
CoreManager::global()
|
||||||
.use_default_config("config_validate::boot_error", &error_msg)
|
.use_default_config("config_validate::boot_error", &error_msg)
|
||||||
.await?;
|
.await?;
|
||||||
Some(("config_validate::boot_error", error_msg))
|
Some(("config_validate::boot_error", error_msg))
|
||||||
} else {
|
} else {
|
||||||
println!("[首次启动] 配置验证成功");
|
logging!(info, Type::Config, true, "配置验证成功");
|
||||||
Some(("config_validate::success", String::new()))
|
Some(("config_validate::success", String::new()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
println!("[首次启动] 验证进程执行失败: {}", err);
|
logging!(warn, Type::Config, true, "验证进程执行失败: {}", err);
|
||||||
CoreManager::global()
|
CoreManager::global()
|
||||||
.use_default_config("config_validate::process_terminated", "")
|
.use_default_config("config_validate::process_terminated", "")
|
||||||
.await?;
|
.await?;
|
||||||
@@ -99,19 +109,16 @@ impl Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
println!("[首次启动] 生成配置文件失败,使用默认配置");
|
logging!(warn, Type::Config, true, "生成配置文件失败,使用默认配置");
|
||||||
CoreManager::global()
|
CoreManager::global()
|
||||||
.use_default_config(
|
.use_default_config("config_validate::error", "")
|
||||||
"config_validate::error",
|
|
||||||
"",
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
Some(("config_validate::error", String::new()))
|
Some(("config_validate::error", String::new()))
|
||||||
};
|
};
|
||||||
|
|
||||||
// 在单独的任务中发送通知
|
// 在单独的任务中发送通知
|
||||||
if let Some((msg_type, msg_content)) = validation_result {
|
if let Some((msg_type, msg_content)) = validation_result {
|
||||||
tauri::async_runtime::spawn(async move {
|
AsyncHandler::spawn(move || async move {
|
||||||
sleep(Duration::from_secs(2)).await;
|
sleep(Duration::from_secs(2)).await;
|
||||||
handle::Handle::notice_message(msg_type, &msg_content);
|
handle::Handle::notice_message(msg_type, &msg_content);
|
||||||
});
|
});
|
||||||
@@ -142,11 +149,11 @@ impl Config {
|
|||||||
pub async fn generate() -> Result<()> {
|
pub async fn generate() -> Result<()> {
|
||||||
let (config, exists_keys, logs) = enhance::enhance().await;
|
let (config, exists_keys, logs) = enhance::enhance().await;
|
||||||
|
|
||||||
*Config::runtime().draft() = IRuntime {
|
*Config::runtime().draft() = Box::new(IRuntime {
|
||||||
config: Some(config),
|
config: Some(config),
|
||||||
exists_keys,
|
exists_keys,
|
||||||
chain_logs: logs,
|
chain_logs: logs,
|
||||||
};
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -157,3 +164,42 @@ pub enum ConfigType {
|
|||||||
Run,
|
Run,
|
||||||
Check,
|
Check,
|
||||||
}
|
}
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::mem;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_prfitem_from_merge_size() {
|
||||||
|
let merge_item = PrfItem::from_merge(Some("Merge".to_string())).unwrap();
|
||||||
|
dbg!(&merge_item);
|
||||||
|
let prfitem_size = mem::size_of_val(&merge_item);
|
||||||
|
dbg!(prfitem_size);
|
||||||
|
// Boxed version
|
||||||
|
let boxed_merge_item = Box::new(merge_item);
|
||||||
|
let box_prfitem_size = mem::size_of_val(&boxed_merge_item);
|
||||||
|
dbg!(box_prfitem_size);
|
||||||
|
// The size of Box<T> is always pointer-sized (usually 8 bytes on 64-bit)
|
||||||
|
// assert_eq!(box_prfitem_size, mem::size_of::<Box<PrfItem>>());
|
||||||
|
assert!(box_prfitem_size < prfitem_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_draft_size_non_boxed() {
|
||||||
|
let draft = Draft::from(IRuntime::new());
|
||||||
|
let iruntime_size = std::mem::size_of_val(&draft);
|
||||||
|
dbg!(iruntime_size);
|
||||||
|
assert_eq!(iruntime_size, std::mem::size_of::<Draft<IRuntime>>());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_draft_size_boxed() {
|
||||||
|
let draft = Draft::from(Box::new(IRuntime::new()));
|
||||||
|
let box_iruntime_size = std::mem::size_of_val(&draft);
|
||||||
|
dbg!(box_iruntime_size);
|
||||||
|
assert_eq!(
|
||||||
|
box_iruntime_size,
|
||||||
|
std::mem::size_of::<Draft<Box<IRuntime>>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,13 +9,21 @@ pub struct Draft<T: Clone + ToOwned> {
|
|||||||
|
|
||||||
macro_rules! draft_define {
|
macro_rules! draft_define {
|
||||||
($id: ident) => {
|
($id: ident) => {
|
||||||
impl Draft<$id> {
|
impl From<$id> for Draft<$id> {
|
||||||
|
fn from(data: $id) -> Self {
|
||||||
|
Draft {
|
||||||
|
inner: Arc::new(Mutex::new((data, None))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Draft<Box<$id>> {
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
pub fn data(&self) -> MappedMutexGuard<$id> {
|
pub fn data(&self) -> MappedMutexGuard<Box<$id>> {
|
||||||
MutexGuard::map(self.inner.lock(), |guard| &mut guard.0)
|
MutexGuard::map(self.inner.lock(), |guard| &mut guard.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn latest(&self) -> MappedMutexGuard<$id> {
|
pub fn latest(&self) -> MappedMutexGuard<Box<$id>> {
|
||||||
MutexGuard::map(self.inner.lock(), |inner| {
|
MutexGuard::map(self.inner.lock(), |inner| {
|
||||||
if inner.1.is_none() {
|
if inner.1.is_none() {
|
||||||
&mut inner.0
|
&mut inner.0
|
||||||
@@ -25,7 +33,7 @@ macro_rules! draft_define {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn draft(&self) -> MappedMutexGuard<$id> {
|
pub fn draft(&self) -> MappedMutexGuard<Box<$id>> {
|
||||||
MutexGuard::map(self.inner.lock(), |inner| {
|
MutexGuard::map(self.inner.lock(), |inner| {
|
||||||
if inner.1.is_none() {
|
if inner.1.is_none() {
|
||||||
inner.1 = Some(inner.0.clone());
|
inner.1 = Some(inner.0.clone());
|
||||||
@@ -35,7 +43,7 @@ macro_rules! draft_define {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn apply(&self) -> Option<$id> {
|
pub fn apply(&self) -> Option<Box<$id>> {
|
||||||
let mut inner = self.inner.lock();
|
let mut inner = self.inner.lock();
|
||||||
|
|
||||||
match inner.1.take() {
|
match inner.1.take() {
|
||||||
@@ -48,14 +56,14 @@ macro_rules! draft_define {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn discard(&self) -> Option<$id> {
|
pub fn discard(&self) -> Option<Box<$id>> {
|
||||||
let mut inner = self.inner.lock();
|
let mut inner = self.inner.lock();
|
||||||
inner.1.take()
|
inner.1.take()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<$id> for Draft<$id> {
|
impl From<Box<$id>> for Draft<Box<$id>> {
|
||||||
fn from(data: $id) -> Self {
|
fn from(data: Box<$id>) -> Self {
|
||||||
Draft {
|
Draft {
|
||||||
inner: Arc::new(Mutex::new((data, None))),
|
inner: Arc::new(Mutex::new((data, None))),
|
||||||
}
|
}
|
||||||
@@ -71,12 +79,12 @@ draft_define!(IRuntime);
|
|||||||
draft_define!(IVerge);
|
draft_define!(IVerge);
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_draft() {
|
fn test_draft_box() {
|
||||||
let verge = IVerge {
|
let verge = Box::new(IVerge {
|
||||||
enable_auto_launch: Some(true),
|
enable_auto_launch: Some(true),
|
||||||
enable_tun_mode: Some(false),
|
enable_tun_mode: Some(false),
|
||||||
..IVerge::default()
|
..IVerge::default()
|
||||||
};
|
});
|
||||||
|
|
||||||
let draft = Draft::from(verge);
|
let draft = Draft::from(verge);
|
||||||
|
|
||||||
@@ -86,10 +94,11 @@ fn test_draft() {
|
|||||||
assert_eq!(draft.draft().enable_auto_launch, Some(true));
|
assert_eq!(draft.draft().enable_auto_launch, Some(true));
|
||||||
assert_eq!(draft.draft().enable_tun_mode, Some(false));
|
assert_eq!(draft.draft().enable_tun_mode, Some(false));
|
||||||
|
|
||||||
let mut d = draft.draft();
|
{
|
||||||
d.enable_auto_launch = Some(false);
|
let mut d = draft.draft();
|
||||||
d.enable_tun_mode = Some(true);
|
d.enable_auto_launch = Some(false);
|
||||||
drop(d);
|
d.enable_tun_mode = Some(true);
|
||||||
|
}
|
||||||
|
|
||||||
assert_eq!(draft.data().enable_auto_launch, Some(true));
|
assert_eq!(draft.data().enable_auto_launch, Some(true));
|
||||||
assert_eq!(draft.data().enable_tun_mode, Some(false));
|
assert_eq!(draft.data().enable_tun_mode, Some(false));
|
||||||
@@ -109,18 +118,17 @@ fn test_draft() {
|
|||||||
assert_eq!(draft.draft().enable_auto_launch, Some(false));
|
assert_eq!(draft.draft().enable_auto_launch, Some(false));
|
||||||
assert_eq!(draft.draft().enable_tun_mode, Some(true));
|
assert_eq!(draft.draft().enable_tun_mode, Some(true));
|
||||||
|
|
||||||
let mut d = draft.draft();
|
{
|
||||||
d.enable_auto_launch = Some(true);
|
let mut d = draft.draft();
|
||||||
drop(d);
|
d.enable_auto_launch = Some(true);
|
||||||
|
}
|
||||||
|
|
||||||
assert_eq!(draft.data().enable_auto_launch, Some(false));
|
assert_eq!(draft.data().enable_auto_launch, Some(false));
|
||||||
|
|
||||||
assert_eq!(draft.draft().enable_auto_launch, Some(true));
|
assert_eq!(draft.draft().enable_auto_launch, Some(true));
|
||||||
|
|
||||||
assert!(draft.discard().is_some());
|
assert!(draft.discard().is_some());
|
||||||
|
|
||||||
assert_eq!(draft.data().enable_auto_launch, Some(false));
|
assert_eq!(draft.data().enable_auto_launch, Some(false));
|
||||||
|
|
||||||
assert!(draft.discard().is_none());
|
assert!(draft.discard().is_none());
|
||||||
|
|
||||||
assert_eq!(draft.draft().enable_auto_launch, Some(false));
|
assert_eq!(draft.draft().enable_auto_launch, Some(false));
|
||||||
|
|||||||
@@ -8,14 +8,9 @@ mod profiles;
|
|||||||
mod runtime;
|
mod runtime;
|
||||||
mod verge;
|
mod verge;
|
||||||
|
|
||||||
pub use self::clash::*;
|
pub use self::{
|
||||||
pub use self::config::*;
|
clash::*, config::*, draft::*, encrypt::*, prfitem::*, profiles::*, runtime::*, verge::*,
|
||||||
pub use self::draft::*;
|
};
|
||||||
pub use self::encrypt::*;
|
|
||||||
pub use self::prfitem::*;
|
|
||||||
pub use self::profiles::*;
|
|
||||||
pub use self::runtime::*;
|
|
||||||
pub use self::verge::*;
|
|
||||||
|
|
||||||
pub const DEFAULT_PAC: &str = r#"function FindProxyForURL(url, host) {
|
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;";
|
return "PROXY 127.0.0.1:%mixed-port%; SOCKS5 127.0.0.1:%mixed-port%; DIRECT;";
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
use crate::utils::{dirs, help, resolve::VERSION, tmpl};
|
use crate::utils::{
|
||||||
|
dirs, help,
|
||||||
|
network::{NetworkManager, ProxyType},
|
||||||
|
tmpl,
|
||||||
|
};
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use reqwest::StatusCode;
|
use reqwest::StatusCode;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_yaml::Mapping;
|
use serde_yaml::Mapping;
|
||||||
use std::fs;
|
use std::{fs, time::Duration};
|
||||||
use sysproxy::Sysproxy;
|
|
||||||
|
|
||||||
use super::Config;
|
use super::Config;
|
||||||
|
|
||||||
@@ -89,6 +92,12 @@ pub struct PrfOption {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub update_interval: Option<u64>,
|
pub update_interval: Option<u64>,
|
||||||
|
|
||||||
|
/// for `remote` profile
|
||||||
|
/// HTTP request timeout in seconds
|
||||||
|
/// default is 60 seconds
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub timeout_seconds: Option<u64>,
|
||||||
|
|
||||||
/// for `remote` profile
|
/// for `remote` profile
|
||||||
/// disable certificate validation
|
/// disable certificate validation
|
||||||
/// default is `false`
|
/// default is `false`
|
||||||
@@ -122,6 +131,7 @@ impl PrfOption {
|
|||||||
a.rules = b.rules.or(a.rules);
|
a.rules = b.rules.or(a.rules);
|
||||||
a.proxies = b.proxies.or(a.proxies);
|
a.proxies = b.proxies.or(a.proxies);
|
||||||
a.groups = b.groups.or(a.groups);
|
a.groups = b.groups.or(a.groups);
|
||||||
|
a.timeout_seconds = b.timeout_seconds.or(a.timeout_seconds);
|
||||||
Some(a)
|
Some(a)
|
||||||
}
|
}
|
||||||
t => t.0.or(t.1),
|
t => t.0.or(t.1),
|
||||||
@@ -234,64 +244,45 @@ impl PrfItem {
|
|||||||
option: Option<PrfOption>,
|
option: Option<PrfOption>,
|
||||||
) -> Result<PrfItem> {
|
) -> Result<PrfItem> {
|
||||||
let opt_ref = option.as_ref();
|
let opt_ref = option.as_ref();
|
||||||
let with_proxy = opt_ref.map_or(false, |o| o.with_proxy.unwrap_or(false));
|
let with_proxy = opt_ref.is_some_and(|o| o.with_proxy.unwrap_or(false));
|
||||||
let self_proxy = opt_ref.map_or(false, |o| o.self_proxy.unwrap_or(false));
|
let self_proxy = opt_ref.is_some_and(|o| o.self_proxy.unwrap_or(false));
|
||||||
let accept_invalid_certs =
|
let accept_invalid_certs =
|
||||||
opt_ref.map_or(false, |o| o.danger_accept_invalid_certs.unwrap_or(false));
|
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 user_agent = opt_ref.and_then(|o| o.user_agent.clone());
|
||||||
let update_interval = opt_ref.and_then(|o| o.update_interval);
|
let update_interval = opt_ref.and_then(|o| o.update_interval);
|
||||||
|
let timeout = opt_ref.and_then(|o| o.timeout_seconds).unwrap_or(20);
|
||||||
let mut merge = opt_ref.and_then(|o| o.merge.clone());
|
let mut merge = opt_ref.and_then(|o| o.merge.clone());
|
||||||
let mut script = opt_ref.and_then(|o| o.script.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 rules = opt_ref.and_then(|o| o.rules.clone());
|
||||||
let mut proxies = opt_ref.and_then(|o| o.proxies.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 groups = opt_ref.and_then(|o| o.groups.clone());
|
||||||
let mut builder = reqwest::ClientBuilder::new().use_rustls_tls().no_proxy();
|
|
||||||
|
|
||||||
// 使用软件自己的代理
|
// 选择代理类型
|
||||||
if self_proxy {
|
let proxy_type = if self_proxy {
|
||||||
let port = Config::verge()
|
ProxyType::Localhost
|
||||||
.latest()
|
} else if with_proxy {
|
||||||
.verge_mixed_port
|
ProxyType::System
|
||||||
.unwrap_or(Config::clash().data().get_mixed_port());
|
} else {
|
||||||
|
ProxyType::None
|
||||||
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 = match NetworkManager::global()
|
||||||
|
.get_with_interrupt(
|
||||||
let resp = builder.build()?.get(url).send().await?;
|
url,
|
||||||
|
proxy_type,
|
||||||
|
Some(timeout),
|
||||||
|
user_agent.clone(),
|
||||||
|
accept_invalid_certs,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => {
|
||||||
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||||
|
bail!("failed to fetch remote profile: {}", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let status_code = resp.status();
|
let status_code = resp.status();
|
||||||
if !StatusCode::is_success(&status_code) {
|
if !StatusCode::is_success(&status_code) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use crate::utils::{dirs, help};
|
|||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_yaml::Mapping;
|
use serde_yaml::Mapping;
|
||||||
use std::{fs, io::Write};
|
use std::{collections::HashSet, fs, io::Write};
|
||||||
|
|
||||||
/// Define the `profiles.yaml` schema
|
/// Define the `profiles.yaml` schema
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||||
@@ -15,6 +15,14 @@ pub struct IProfiles {
|
|||||||
pub items: Option<Vec<PrfItem>>,
|
pub items: Option<Vec<PrfItem>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 清理结果
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CleanupResult {
|
||||||
|
pub total_files: usize,
|
||||||
|
pub deleted_files: Vec<String>,
|
||||||
|
pub failed_deletions: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
macro_rules! patch {
|
macro_rules! patch {
|
||||||
($lv: expr, $rv: expr, $key: tt) => {
|
($lv: expr, $rv: expr, $key: tt) => {
|
||||||
if ($rv.$key).is_some() {
|
if ($rv.$key).is_some() {
|
||||||
@@ -472,15 +480,210 @@ impl IProfiles {
|
|||||||
|
|
||||||
/// 获取所有的profiles(uid,名称)
|
/// 获取所有的profiles(uid,名称)
|
||||||
pub fn all_profile_uid_and_name(&self) -> Option<Vec<(String, String)>> {
|
pub fn all_profile_uid_and_name(&self) -> Option<Vec<(String, String)>> {
|
||||||
match self.items.as_ref() {
|
self.items.as_ref().map(|items| {
|
||||||
Some(items) => Some(items.iter().filter_map(|e| {
|
items
|
||||||
if let (Some(uid), Some(name)) = (e.uid.clone(), e.name.clone()) {
|
.iter()
|
||||||
Some((uid, name))
|
.filter_map(|e| {
|
||||||
} else {
|
if let (Some(uid), Some(name)) = (e.uid.clone(), e.name.clone()) {
|
||||||
None
|
Some((uid, name))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 以 app 中的 profile 列表为准,删除不再需要的文件
|
||||||
|
pub fn cleanup_orphaned_files(&self) -> Result<CleanupResult> {
|
||||||
|
let profiles_dir = dirs::app_profiles_dir()?;
|
||||||
|
|
||||||
|
if !profiles_dir.exists() {
|
||||||
|
return Ok(CleanupResult {
|
||||||
|
total_files: 0,
|
||||||
|
deleted_files: vec![],
|
||||||
|
failed_deletions: vec![],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有 active profile 的文件名集合
|
||||||
|
let active_files = self.get_all_active_files();
|
||||||
|
|
||||||
|
// 添加全局扩展配置文件到保护列表
|
||||||
|
let protected_files = self.get_protected_global_files();
|
||||||
|
|
||||||
|
// 扫描 profiles 目录下的所有文件
|
||||||
|
let mut total_files = 0;
|
||||||
|
let mut deleted_files = vec![];
|
||||||
|
let mut failed_deletions = vec![];
|
||||||
|
|
||||||
|
for entry in std::fs::read_dir(&profiles_dir)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let path = entry.path();
|
||||||
|
|
||||||
|
if !path.is_file() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
total_files += 1;
|
||||||
|
|
||||||
|
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
|
||||||
|
if Self::is_profile_file(file_name) {
|
||||||
|
// 检查是否为全局扩展文件
|
||||||
|
if protected_files.contains(file_name) {
|
||||||
|
log::debug!(target: "app", "保护全局扩展配置文件: {}", file_name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否为活跃文件
|
||||||
|
if !active_files.contains(file_name) {
|
||||||
|
match std::fs::remove_file(&path) {
|
||||||
|
Ok(_) => {
|
||||||
|
deleted_files.push(file_name.to_string());
|
||||||
|
log::info!(target: "app", "已清理冗余文件: {}", file_name);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
failed_deletions.push(format!("{}: {}", file_name, e));
|
||||||
|
log::warn!(target: "app", "清理文件失败: {} - {}", file_name, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}).collect()),
|
}
|
||||||
None => None,
|
}
|
||||||
|
|
||||||
|
let result = CleanupResult {
|
||||||
|
total_files,
|
||||||
|
deleted_files,
|
||||||
|
failed_deletions,
|
||||||
|
};
|
||||||
|
|
||||||
|
log::info!(
|
||||||
|
target: "app",
|
||||||
|
"Profile 文件清理完成: 总文件数={}, 删除文件数={}, 失败数={}",
|
||||||
|
result.total_files,
|
||||||
|
result.deleted_files.len(),
|
||||||
|
result.failed_deletions.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 不删除全局扩展配置
|
||||||
|
fn get_protected_global_files(&self) -> HashSet<String> {
|
||||||
|
let mut protected_files = HashSet::new();
|
||||||
|
|
||||||
|
protected_files.insert("Merge.yaml".to_string());
|
||||||
|
protected_files.insert("Script.js".to_string());
|
||||||
|
|
||||||
|
protected_files
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取所有 active profile 关联的文件名
|
||||||
|
fn get_all_active_files(&self) -> HashSet<String> {
|
||||||
|
let mut active_files = HashSet::new();
|
||||||
|
|
||||||
|
if let Some(items) = &self.items {
|
||||||
|
for item in items {
|
||||||
|
// 收集所有类型 profile 的文件
|
||||||
|
if let Some(file) = &item.file {
|
||||||
|
active_files.insert(file.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对于主 profile 类型(remote/local),还需要收集其关联的扩展文件
|
||||||
|
if let Some(itype) = &item.itype {
|
||||||
|
if itype == "remote" || itype == "local" {
|
||||||
|
if let Some(option) = &item.option {
|
||||||
|
// 收集关联的扩展文件
|
||||||
|
if let Some(merge_uid) = &option.merge {
|
||||||
|
if let Ok(merge_item) = self.get_item(merge_uid) {
|
||||||
|
if let Some(file) = &merge_item.file {
|
||||||
|
active_files.insert(file.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(script_uid) = &option.script {
|
||||||
|
if let Ok(script_item) = self.get_item(script_uid) {
|
||||||
|
if let Some(file) = &script_item.file {
|
||||||
|
active_files.insert(file.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(rules_uid) = &option.rules {
|
||||||
|
if let Ok(rules_item) = self.get_item(rules_uid) {
|
||||||
|
if let Some(file) = &rules_item.file {
|
||||||
|
active_files.insert(file.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(proxies_uid) = &option.proxies {
|
||||||
|
if let Ok(proxies_item) = self.get_item(proxies_uid) {
|
||||||
|
if let Some(file) = &proxies_item.file {
|
||||||
|
active_files.insert(file.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(groups_uid) = &option.groups {
|
||||||
|
if let Ok(groups_item) = self.get_item(groups_uid) {
|
||||||
|
if let Some(file) = &groups_item.file {
|
||||||
|
active_files.insert(file.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
active_files
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 检查文件名是否符合 profile 文件的命名规则
|
||||||
|
fn is_profile_file(filename: &str) -> bool {
|
||||||
|
// 匹配各种 profile 文件格式
|
||||||
|
// R12345678.yaml (remote)
|
||||||
|
// L12345678.yaml (local)
|
||||||
|
// m12345678.yaml (merge)
|
||||||
|
// s12345678.js (script)
|
||||||
|
// r12345678.yaml (rules)
|
||||||
|
// p12345678.yaml (proxies)
|
||||||
|
// g12345678.yaml (groups)
|
||||||
|
|
||||||
|
let patterns = [
|
||||||
|
r"^[RL][a-zA-Z0-9]+\.yaml$", // Remote/Local profiles
|
||||||
|
r"^m[a-zA-Z0-9]+\.yaml$", // Merge files
|
||||||
|
r"^s[a-zA-Z0-9]+\.js$", // Script files
|
||||||
|
r"^[rpg][a-zA-Z0-9]+\.yaml$", // Rules/Proxies/Groups files
|
||||||
|
];
|
||||||
|
|
||||||
|
patterns.iter().any(|pattern| {
|
||||||
|
regex::Regex::new(pattern)
|
||||||
|
.map(|re| re.is_match(filename))
|
||||||
|
.unwrap_or(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn auto_cleanup(&self) -> Result<()> {
|
||||||
|
match self.cleanup_orphaned_files() {
|
||||||
|
Ok(result) => {
|
||||||
|
if !result.deleted_files.is_empty() {
|
||||||
|
log::info!(
|
||||||
|
target: "app",
|
||||||
|
"自动清理完成,删除了 {} 个冗余文件",
|
||||||
|
result.deleted_files.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!(target: "app", "自动清理失败: {}", e);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
use crate::config::DEFAULT_PAC;
|
use crate::{
|
||||||
use crate::config::{deserialize_encrypted, serialize_encrypted};
|
config::{deserialize_encrypted, serialize_encrypted, DEFAULT_PAC},
|
||||||
use crate::utils::i18n;
|
logging,
|
||||||
use crate::utils::{dirs, help};
|
utils::{dirs, help, i18n, logging::Type},
|
||||||
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use log::LevelFilter;
|
use log::LevelFilter;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -88,6 +89,9 @@ pub struct IVerge {
|
|||||||
/// pac script content
|
/// pac script content
|
||||||
pub pac_file_content: Option<String>,
|
pub pac_file_content: Option<String>,
|
||||||
|
|
||||||
|
/// proxy host address
|
||||||
|
pub proxy_host: Option<String>,
|
||||||
|
|
||||||
/// theme setting
|
/// theme setting
|
||||||
pub theme_setting: Option<IVergeTheme>,
|
pub theme_setting: Option<IVergeTheme>,
|
||||||
|
|
||||||
@@ -101,10 +105,14 @@ pub struct IVerge {
|
|||||||
/// hotkey map
|
/// hotkey map
|
||||||
/// format: {func},{key}
|
/// format: {func},{key}
|
||||||
pub hotkeys: Option<Vec<String>>,
|
pub hotkeys: Option<Vec<String>>,
|
||||||
|
|
||||||
/// enable global hotkey
|
/// enable global hotkey
|
||||||
pub enable_global_hotkey: Option<bool>,
|
pub enable_global_hotkey: Option<bool>,
|
||||||
|
|
||||||
|
/// 首页卡片设置
|
||||||
|
/// 控制首页各个卡片的显示和隐藏
|
||||||
|
pub home_cards: Option<serde_json::Value>,
|
||||||
|
|
||||||
/// 切换代理时自动关闭连接
|
/// 切换代理时自动关闭连接
|
||||||
pub auto_close_connection: Option<bool>,
|
pub auto_close_connection: Option<bool>,
|
||||||
|
|
||||||
@@ -127,7 +135,7 @@ pub struct IVerge {
|
|||||||
pub test_list: Option<Vec<IVergeTestItem>>,
|
pub test_list: Option<Vec<IVergeTestItem>>,
|
||||||
|
|
||||||
/// 日志清理
|
/// 日志清理
|
||||||
/// 0: 不清理; 1: 7天; 2: 30天; 3: 90天
|
/// 0: 不清理; 1: 1天;2: 7天; 3: 30天; 4: 90天
|
||||||
pub auto_log_clean: Option<i32>,
|
pub auto_log_clean: Option<i32>,
|
||||||
|
|
||||||
/// 是否启用随机端口
|
/// 是否启用随机端口
|
||||||
@@ -185,8 +193,19 @@ pub struct IVerge {
|
|||||||
|
|
||||||
pub enable_tray_speed: Option<bool>,
|
pub enable_tray_speed: Option<bool>,
|
||||||
|
|
||||||
/// 轻量模式 - 只保留内核运行
|
pub enable_tray_icon: Option<bool>,
|
||||||
pub enable_lite_mode: Option<bool>,
|
|
||||||
|
/// 自动进入轻量模式
|
||||||
|
pub enable_auto_light_weight_mode: Option<bool>,
|
||||||
|
|
||||||
|
/// 自动进入轻量模式的延迟(分钟)
|
||||||
|
pub auto_light_weight_minutes: Option<u64>,
|
||||||
|
|
||||||
|
/// 启用代理页面自动滚动
|
||||||
|
pub enable_hover_jump_navigator: Option<bool>,
|
||||||
|
|
||||||
|
/// 服务状态跟踪
|
||||||
|
pub service_state: Option<crate::core::service::ServiceState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||||
@@ -214,6 +233,93 @@ pub struct IVergeTheme {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl IVerge {
|
impl IVerge {
|
||||||
|
/// 有效的clash核心名称
|
||||||
|
pub const VALID_CLASH_CORES: &'static [&'static str] = &["verge-mihomo", "verge-mihomo-alpha"];
|
||||||
|
|
||||||
|
/// 验证并修正配置文件中的clash_core值
|
||||||
|
pub fn validate_and_fix_config() -> Result<()> {
|
||||||
|
let config_path = dirs::verge_path()?;
|
||||||
|
let mut config = match help::read_yaml::<IVerge>(&config_path) {
|
||||||
|
Ok(config) => config,
|
||||||
|
Err(_) => Self::template(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut needs_fix = false;
|
||||||
|
|
||||||
|
if let Some(ref core) = config.clash_core {
|
||||||
|
let core_str = core.trim();
|
||||||
|
if core_str.is_empty() || !Self::VALID_CLASH_CORES.contains(&core_str) {
|
||||||
|
logging!(
|
||||||
|
warn,
|
||||||
|
Type::Config,
|
||||||
|
true,
|
||||||
|
"启动时发现无效的clash_core配置: '{}', 将自动修正为 'verge-mihomo'",
|
||||||
|
core
|
||||||
|
);
|
||||||
|
config.clash_core = Some("verge-mihomo".to_string());
|
||||||
|
needs_fix = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Config,
|
||||||
|
true,
|
||||||
|
"启动时发现未配置clash_core, 将设置为默认值 'verge-mihomo'"
|
||||||
|
);
|
||||||
|
config.clash_core = Some("verge-mihomo".to_string());
|
||||||
|
needs_fix = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修正后保存配置
|
||||||
|
if needs_fix {
|
||||||
|
logging!(info, Type::Config, true, "正在保存修正后的配置文件...");
|
||||||
|
help::save_yaml(&config_path, &config, Some("# Clash Verge Config"))?;
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Config,
|
||||||
|
true,
|
||||||
|
"配置文件修正完成,需要重新加载配置"
|
||||||
|
);
|
||||||
|
|
||||||
|
Self::reload_config_after_fix(config)?;
|
||||||
|
} else {
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Config,
|
||||||
|
true,
|
||||||
|
"clash_core配置验证通过: {:?}",
|
||||||
|
config.clash_core
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 配置修正后重新加载配置
|
||||||
|
fn reload_config_after_fix(updated_config: IVerge) -> Result<()> {
|
||||||
|
use crate::config::Config;
|
||||||
|
|
||||||
|
let config_draft = Config::verge();
|
||||||
|
*config_draft.draft() = Box::new(updated_config.clone());
|
||||||
|
config_draft.apply();
|
||||||
|
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Config,
|
||||||
|
true,
|
||||||
|
"内存配置已强制更新,新的clash_core: {:?}",
|
||||||
|
updated_config.clash_core
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_valid_clash_core(&self) -> String {
|
||||||
|
self.clash_core
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| "verge-mihomo".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
fn get_system_language() -> String {
|
fn get_system_language() -> String {
|
||||||
let sys_lang = sys_locale::get_locale()
|
let sys_lang = sys_locale::get_locale()
|
||||||
.unwrap_or_else(|| String::from("en"))
|
.unwrap_or_else(|| String::from("en"))
|
||||||
@@ -248,7 +354,7 @@ impl IVerge {
|
|||||||
env_type: Some("bash".into()),
|
env_type: Some("bash".into()),
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
env_type: Some("powershell".into()),
|
env_type: Some("powershell".into()),
|
||||||
start_page: Some("/".into()),
|
start_page: Some("/home".into()),
|
||||||
traffic_graph: Some(true),
|
traffic_graph: Some(true),
|
||||||
enable_memory_usage: Some(true),
|
enable_memory_usage: Some(true),
|
||||||
enable_group_icon: Some(true),
|
enable_group_icon: Some(true),
|
||||||
@@ -260,9 +366,11 @@ impl IVerge {
|
|||||||
tun_tray_icon: Some(false),
|
tun_tray_icon: Some(false),
|
||||||
enable_auto_launch: Some(false),
|
enable_auto_launch: Some(false),
|
||||||
enable_silent_start: Some(false),
|
enable_silent_start: Some(false),
|
||||||
|
enable_hover_jump_navigator: Some(true),
|
||||||
enable_system_proxy: Some(false),
|
enable_system_proxy: Some(false),
|
||||||
proxy_auto_config: Some(false),
|
proxy_auto_config: Some(false),
|
||||||
pac_file_content: Some(DEFAULT_PAC.into()),
|
pac_file_content: Some(DEFAULT_PAC.into()),
|
||||||
|
proxy_host: Some("127.0.0.1".into()),
|
||||||
enable_random_port: Some(false),
|
enable_random_port: Some(false),
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
verge_redir_port: Some(7895),
|
verge_redir_port: Some(7895),
|
||||||
@@ -287,10 +395,14 @@ impl IVerge {
|
|||||||
webdav_url: None,
|
webdav_url: None,
|
||||||
webdav_username: None,
|
webdav_username: None,
|
||||||
webdav_password: None,
|
webdav_password: None,
|
||||||
enable_tray_speed: Some(true),
|
enable_tray_speed: Some(false),
|
||||||
|
enable_tray_icon: Some(true),
|
||||||
enable_global_hotkey: Some(true),
|
enable_global_hotkey: Some(true),
|
||||||
enable_lite_mode: Some(false),
|
enable_auto_light_weight_mode: Some(false),
|
||||||
enable_dns_settings: Some(true),
|
auto_light_weight_minutes: Some(10),
|
||||||
|
enable_dns_settings: Some(false),
|
||||||
|
home_cards: None,
|
||||||
|
service_state: None,
|
||||||
..Self::default()
|
..Self::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -331,6 +443,7 @@ impl IVerge {
|
|||||||
patch!(enable_tun_mode);
|
patch!(enable_tun_mode);
|
||||||
patch!(enable_auto_launch);
|
patch!(enable_auto_launch);
|
||||||
patch!(enable_silent_start);
|
patch!(enable_silent_start);
|
||||||
|
patch!(enable_hover_jump_navigator);
|
||||||
patch!(enable_random_port);
|
patch!(enable_random_port);
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
patch!(verge_redir_port);
|
patch!(verge_redir_port);
|
||||||
@@ -352,7 +465,7 @@ impl IVerge {
|
|||||||
patch!(proxy_guard_duration);
|
patch!(proxy_guard_duration);
|
||||||
patch!(proxy_auto_config);
|
patch!(proxy_auto_config);
|
||||||
patch!(pac_file_content);
|
patch!(pac_file_content);
|
||||||
|
patch!(proxy_host);
|
||||||
patch!(theme_setting);
|
patch!(theme_setting);
|
||||||
patch!(web_ui_list);
|
patch!(web_ui_list);
|
||||||
patch!(clash_core);
|
patch!(clash_core);
|
||||||
@@ -372,8 +485,12 @@ impl IVerge {
|
|||||||
patch!(webdav_username);
|
patch!(webdav_username);
|
||||||
patch!(webdav_password);
|
patch!(webdav_password);
|
||||||
patch!(enable_tray_speed);
|
patch!(enable_tray_speed);
|
||||||
patch!(enable_lite_mode);
|
patch!(enable_tray_icon);
|
||||||
|
patch!(enable_auto_light_weight_mode);
|
||||||
|
patch!(auto_light_weight_minutes);
|
||||||
patch!(enable_dns_settings);
|
patch!(enable_dns_settings);
|
||||||
|
patch!(home_cards);
|
||||||
|
patch!(service_state);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 在初始化前尝试拿到单例端口的值
|
/// 在初始化前尝试拿到单例端口的值
|
||||||
@@ -432,6 +549,7 @@ pub struct IVergeResponse {
|
|||||||
pub proxy_guard_duration: Option<u64>,
|
pub proxy_guard_duration: Option<u64>,
|
||||||
pub proxy_auto_config: Option<bool>,
|
pub proxy_auto_config: Option<bool>,
|
||||||
pub pac_file_content: Option<String>,
|
pub pac_file_content: Option<String>,
|
||||||
|
pub proxy_host: Option<String>,
|
||||||
pub theme_setting: Option<IVergeTheme>,
|
pub theme_setting: Option<IVergeTheme>,
|
||||||
pub web_ui_list: Option<Vec<String>>,
|
pub web_ui_list: Option<Vec<String>>,
|
||||||
pub clash_core: Option<String>,
|
pub clash_core: Option<String>,
|
||||||
@@ -462,12 +580,19 @@ pub struct IVergeResponse {
|
|||||||
pub webdav_username: Option<String>,
|
pub webdav_username: Option<String>,
|
||||||
pub webdav_password: Option<String>,
|
pub webdav_password: Option<String>,
|
||||||
pub enable_tray_speed: Option<bool>,
|
pub enable_tray_speed: Option<bool>,
|
||||||
pub enable_lite_mode: Option<bool>,
|
pub enable_tray_icon: Option<bool>,
|
||||||
|
pub enable_auto_light_weight_mode: Option<bool>,
|
||||||
|
pub auto_light_weight_minutes: Option<u64>,
|
||||||
pub enable_dns_settings: Option<bool>,
|
pub enable_dns_settings: Option<bool>,
|
||||||
|
pub home_cards: Option<serde_json::Value>,
|
||||||
|
pub enable_hover_jump_navigator: Option<bool>,
|
||||||
|
pub service_state: Option<crate::core::service::ServiceState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<IVerge> for IVergeResponse {
|
impl From<IVerge> for IVergeResponse {
|
||||||
fn from(verge: IVerge) -> Self {
|
fn from(verge: IVerge) -> Self {
|
||||||
|
// 先获取验证后的clash_core值,避免后续借用冲突
|
||||||
|
let valid_clash_core = verge.get_valid_clash_core();
|
||||||
Self {
|
Self {
|
||||||
app_log_level: verge.app_log_level,
|
app_log_level: verge.app_log_level,
|
||||||
language: verge.language,
|
language: verge.language,
|
||||||
@@ -496,9 +621,10 @@ impl From<IVerge> for IVergeResponse {
|
|||||||
proxy_guard_duration: verge.proxy_guard_duration,
|
proxy_guard_duration: verge.proxy_guard_duration,
|
||||||
proxy_auto_config: verge.proxy_auto_config,
|
proxy_auto_config: verge.proxy_auto_config,
|
||||||
pac_file_content: verge.pac_file_content,
|
pac_file_content: verge.pac_file_content,
|
||||||
|
proxy_host: verge.proxy_host,
|
||||||
theme_setting: verge.theme_setting,
|
theme_setting: verge.theme_setting,
|
||||||
web_ui_list: verge.web_ui_list,
|
web_ui_list: verge.web_ui_list,
|
||||||
clash_core: verge.clash_core,
|
clash_core: Some(valid_clash_core),
|
||||||
hotkeys: verge.hotkeys,
|
hotkeys: verge.hotkeys,
|
||||||
auto_close_connection: verge.auto_close_connection,
|
auto_close_connection: verge.auto_close_connection,
|
||||||
auto_check_update: verge.auto_check_update,
|
auto_check_update: verge.auto_check_update,
|
||||||
@@ -526,8 +652,13 @@ impl From<IVerge> for IVergeResponse {
|
|||||||
webdav_username: verge.webdav_username,
|
webdav_username: verge.webdav_username,
|
||||||
webdav_password: verge.webdav_password,
|
webdav_password: verge.webdav_password,
|
||||||
enable_tray_speed: verge.enable_tray_speed,
|
enable_tray_speed: verge.enable_tray_speed,
|
||||||
enable_lite_mode: verge.enable_lite_mode,
|
enable_tray_icon: verge.enable_tray_icon,
|
||||||
|
enable_auto_light_weight_mode: verge.enable_auto_light_weight_mode,
|
||||||
|
auto_light_weight_minutes: verge.auto_light_weight_minutes,
|
||||||
enable_dns_settings: verge.enable_dns_settings,
|
enable_dns_settings: verge.enable_dns_settings,
|
||||||
|
home_cards: verge.home_cards,
|
||||||
|
enable_hover_jump_navigator: verge.enable_hover_jump_navigator,
|
||||||
|
service_state: verge.service_state,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,23 @@
|
|||||||
use crate::config::Config;
|
use crate::{config::Config, utils::dirs};
|
||||||
use crate::utils::dirs;
|
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use reqwest_dav::list_cmd::{ListEntity, ListFile};
|
use reqwest_dav::list_cmd::{ListEntity, ListFile};
|
||||||
use std::collections::HashMap;
|
use std::{
|
||||||
use std::env::{consts::OS, temp_dir};
|
collections::HashMap,
|
||||||
use std::fs;
|
env::{consts::OS, temp_dir},
|
||||||
use std::io::Write;
|
fs,
|
||||||
use std::path::PathBuf;
|
io::Write,
|
||||||
use std::sync::Arc;
|
path::PathBuf,
|
||||||
use std::time::Duration;
|
sync::Arc,
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
use tokio::time::timeout;
|
use tokio::time::timeout;
|
||||||
use zip::write::SimpleFileOptions;
|
use zip::write::SimpleFileOptions;
|
||||||
|
|
||||||
|
// 应用版本常量,来自 tauri.conf.json
|
||||||
|
const APP_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
const TIMEOUT_UPLOAD: u64 = 300; // 上传超时 5 分钟
|
const TIMEOUT_UPLOAD: u64 = 300; // 上传超时 5 分钟
|
||||||
const TIMEOUT_DOWNLOAD: u64 = 300; // 下载超时 5 分钟
|
const TIMEOUT_DOWNLOAD: u64 = 300; // 下载超时 5 分钟
|
||||||
const TIMEOUT_LIST: u64 = 3; // 列表超时 30 秒
|
const TIMEOUT_LIST: u64 = 3; // 列表超时 30 秒
|
||||||
@@ -104,6 +108,18 @@ impl WebDavClient {
|
|||||||
reqwest::Client::builder()
|
reqwest::Client::builder()
|
||||||
.danger_accept_invalid_certs(true)
|
.danger_accept_invalid_certs(true)
|
||||||
.timeout(Duration::from_secs(op.timeout()))
|
.timeout(Duration::from_secs(op.timeout()))
|
||||||
|
.user_agent(format!(
|
||||||
|
"clash-verge/{} ({} WebDAV-Client)",
|
||||||
|
APP_VERSION, OS
|
||||||
|
))
|
||||||
|
.redirect(reqwest::redirect::Policy::custom(|attempt| {
|
||||||
|
// 允许所有请求类型的重定向,包括PUT
|
||||||
|
if attempt.previous().len() >= 5 {
|
||||||
|
attempt.error("重定向次数过多")
|
||||||
|
} else {
|
||||||
|
attempt.follow()
|
||||||
|
}
|
||||||
|
}))
|
||||||
.build()
|
.build()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
@@ -111,12 +127,13 @@ impl WebDavClient {
|
|||||||
.set_auth(reqwest_dav::Auth::Basic(config.username, config.password))
|
.set_auth(reqwest_dav::Auth::Basic(config.username, config.password))
|
||||||
.build()?;
|
.build()?;
|
||||||
|
|
||||||
// 确保备份目录存在
|
// 尝试检查目录是否存在,如果不存在尝试创建,但创建失败不报错
|
||||||
let list_result = client
|
if client
|
||||||
.list(dirs::BACKUP_DIR, reqwest_dav::Depth::Number(0))
|
.list(dirs::BACKUP_DIR, reqwest_dav::Depth::Number(0))
|
||||||
.await;
|
.await
|
||||||
if list_result.is_err() {
|
.is_err()
|
||||||
client.mkcol(dirs::BACKUP_DIR).await?;
|
{
|
||||||
|
let _ = client.mkcol(dirs::BACKUP_DIR).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 缓存客户端
|
// 缓存客户端
|
||||||
@@ -136,9 +153,41 @@ impl WebDavClient {
|
|||||||
pub async fn upload(&self, file_path: PathBuf, file_name: String) -> Result<(), Error> {
|
pub async fn upload(&self, file_path: PathBuf, file_name: String) -> Result<(), Error> {
|
||||||
let client = self.get_client(Operation::Upload).await?;
|
let client = self.get_client(Operation::Upload).await?;
|
||||||
let webdav_path: String = format!("{}/{}", dirs::BACKUP_DIR, file_name);
|
let webdav_path: String = format!("{}/{}", dirs::BACKUP_DIR, file_name);
|
||||||
let fut = client.put(webdav_path.as_ref(), fs::read(file_path)?);
|
|
||||||
timeout(Duration::from_secs(TIMEOUT_UPLOAD), fut).await??;
|
// 读取文件并上传,如果失败尝试一次重试
|
||||||
Ok(())
|
let file_content = fs::read(&file_path)?;
|
||||||
|
|
||||||
|
// 添加超时保护
|
||||||
|
let upload_result = timeout(
|
||||||
|
Duration::from_secs(TIMEOUT_UPLOAD),
|
||||||
|
client.put(&webdav_path, file_content.clone()),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match upload_result {
|
||||||
|
Err(_) => {
|
||||||
|
log::warn!("Upload timed out, retrying once");
|
||||||
|
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||||
|
timeout(
|
||||||
|
Duration::from_secs(TIMEOUT_UPLOAD),
|
||||||
|
client.put(&webdav_path, file_content),
|
||||||
|
)
|
||||||
|
.await??;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Err(e)) => {
|
||||||
|
log::warn!("Upload failed, retrying once: {}", e);
|
||||||
|
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||||
|
timeout(
|
||||||
|
Duration::from_secs(TIMEOUT_UPLOAD),
|
||||||
|
client.put(&webdav_path, file_content),
|
||||||
|
)
|
||||||
|
.await??;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Ok(Ok(_)) => Ok(()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn download(&self, filename: String, storage_path: PathBuf) -> Result<(), Error> {
|
pub async fn download(&self, filename: String, storage_path: PathBuf) -> Result<(), Error> {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,31 +1,294 @@
|
|||||||
use crate::log_err;
|
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use std::sync::Arc;
|
use std::{
|
||||||
|
sync::{
|
||||||
|
atomic::{AtomicU64, Ordering},
|
||||||
|
mpsc, Arc,
|
||||||
|
},
|
||||||
|
thread,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
use tauri::{AppHandle, Emitter, Manager, WebviewWindow};
|
use tauri::{AppHandle, Emitter, Manager, WebviewWindow};
|
||||||
use tauri_plugin_shell::process::CommandChild;
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone)]
|
use crate::{logging, utils::logging::Type};
|
||||||
|
|
||||||
|
/// 不同类型的前端通知
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
enum FrontendEvent {
|
||||||
|
RefreshClash,
|
||||||
|
RefreshVerge,
|
||||||
|
NoticeMessage { status: String, message: String },
|
||||||
|
ProfileChanged { current_profile_id: String },
|
||||||
|
TimerUpdated { profile_index: String },
|
||||||
|
StartupCompleted,
|
||||||
|
ProfileUpdateStarted { uid: String },
|
||||||
|
ProfileUpdateCompleted { uid: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 事件发送统计和监控
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct EventStats {
|
||||||
|
total_sent: AtomicU64,
|
||||||
|
total_errors: AtomicU64,
|
||||||
|
last_error_time: RwLock<Option<Instant>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 存储启动期间的错误消息
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct ErrorMessage {
|
||||||
|
status: String,
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 全局前端通知系统
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct NotificationSystem {
|
||||||
|
sender: Option<mpsc::Sender<FrontendEvent>>,
|
||||||
|
worker_handle: Option<thread::JoinHandle<()>>,
|
||||||
|
is_running: bool,
|
||||||
|
stats: EventStats,
|
||||||
|
last_emit_time: RwLock<Instant>,
|
||||||
|
/// 当通知系统失败超过阈值时,进入紧急模式
|
||||||
|
emergency_mode: RwLock<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for NotificationSystem {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NotificationSystem {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
sender: None,
|
||||||
|
worker_handle: None,
|
||||||
|
is_running: false,
|
||||||
|
stats: EventStats::default(),
|
||||||
|
last_emit_time: RwLock::new(Instant::now()),
|
||||||
|
emergency_mode: RwLock::new(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 启动通知处理线程
|
||||||
|
fn start(&mut self) {
|
||||||
|
if self.is_running {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (tx, rx) = mpsc::channel();
|
||||||
|
self.sender = Some(tx);
|
||||||
|
self.is_running = true;
|
||||||
|
|
||||||
|
*self.last_emit_time.write() = Instant::now();
|
||||||
|
|
||||||
|
self.worker_handle = Some(
|
||||||
|
thread::Builder::new()
|
||||||
|
.name("frontend-notifier".into())
|
||||||
|
.spawn(move || {
|
||||||
|
let handle = Handle::global();
|
||||||
|
|
||||||
|
while !handle.is_exiting() {
|
||||||
|
match rx.recv_timeout(Duration::from_millis(100)) {
|
||||||
|
Ok(event) => {
|
||||||
|
let system_guard = handle.notification_system.read();
|
||||||
|
if system_guard.as_ref().is_none() {
|
||||||
|
log::warn!("NotificationSystem not found in handle while processing event.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let system = system_guard.as_ref().unwrap();
|
||||||
|
|
||||||
|
let is_emergency = *system.emergency_mode.read();
|
||||||
|
|
||||||
|
if is_emergency {
|
||||||
|
if let FrontendEvent::NoticeMessage { ref status, .. } = event {
|
||||||
|
if status == "info" {
|
||||||
|
log::warn!(
|
||||||
|
"Emergency mode active, skipping info message"
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(window) = handle.get_window() {
|
||||||
|
*system.last_emit_time.write() = Instant::now();
|
||||||
|
|
||||||
|
let (event_name_str, payload_result) = match event {
|
||||||
|
FrontendEvent::RefreshClash => {
|
||||||
|
("verge://refresh-clash-config", Ok(serde_json::json!("yes")))
|
||||||
|
}
|
||||||
|
FrontendEvent::RefreshVerge => {
|
||||||
|
("verge://refresh-verge-config", Ok(serde_json::json!("yes")))
|
||||||
|
}
|
||||||
|
FrontendEvent::NoticeMessage { status, message } => {
|
||||||
|
match serde_json::to_value((status, message)) {
|
||||||
|
Ok(p) => ("verge://notice-message", Ok(p)),
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to serialize NoticeMessage payload: {}", e);
|
||||||
|
("verge://notice-message", Err(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FrontendEvent::ProfileChanged { current_profile_id } => {
|
||||||
|
("profile-changed", Ok(serde_json::json!(current_profile_id)))
|
||||||
|
}
|
||||||
|
FrontendEvent::TimerUpdated { profile_index } => {
|
||||||
|
("verge://timer-updated", Ok(serde_json::json!(profile_index)))
|
||||||
|
}
|
||||||
|
FrontendEvent::StartupCompleted => {
|
||||||
|
("verge://startup-completed", Ok(serde_json::json!(null)))
|
||||||
|
}
|
||||||
|
FrontendEvent::ProfileUpdateStarted { uid } => {
|
||||||
|
("profile-update-started", Ok(serde_json::json!({ "uid": uid })))
|
||||||
|
}
|
||||||
|
FrontendEvent::ProfileUpdateCompleted { uid } => {
|
||||||
|
("profile-update-completed", Ok(serde_json::json!({ "uid": uid })))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Ok(payload) = payload_result {
|
||||||
|
match window.emit(event_name_str, payload) {
|
||||||
|
Ok(_) => {
|
||||||
|
system.stats.total_sent.fetch_add(1, Ordering::SeqCst);
|
||||||
|
// 记录成功发送的事件
|
||||||
|
if log::log_enabled!(log::Level::Debug) {
|
||||||
|
log::debug!("Successfully emitted event: {}", event_name_str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("Failed to emit event {}: {}", event_name_str, e);
|
||||||
|
system.stats.total_errors.fetch_add(1, Ordering::SeqCst);
|
||||||
|
*system.stats.last_error_time.write() = Some(Instant::now());
|
||||||
|
|
||||||
|
let errors = system.stats.total_errors.load(Ordering::SeqCst);
|
||||||
|
const EMIT_ERROR_THRESHOLD: u64 = 10;
|
||||||
|
if errors > EMIT_ERROR_THRESHOLD && !*system.emergency_mode.read() {
|
||||||
|
log::warn!(
|
||||||
|
"Reached {} emit errors, entering emergency mode",
|
||||||
|
EMIT_ERROR_THRESHOLD
|
||||||
|
);
|
||||||
|
*system.emergency_mode.write() = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
system.stats.total_errors.fetch_add(1, Ordering::SeqCst);
|
||||||
|
*system.stats.last_error_time.write() = Some(Instant::now());
|
||||||
|
log::warn!("Skipped emitting event due to payload serialization error for {}", event_name_str);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log::warn!("No window found, skipping event emit.");
|
||||||
|
}
|
||||||
|
thread::sleep(Duration::from_millis(20));
|
||||||
|
}
|
||||||
|
Err(mpsc::RecvTimeoutError::Timeout) => {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Err(mpsc::RecvTimeoutError::Disconnected) => {
|
||||||
|
log::info!(
|
||||||
|
"Notification channel disconnected, exiting worker thread"
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!("Notification worker thread exiting");
|
||||||
|
})
|
||||||
|
.expect("Failed to start notification worker thread"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 发送事件到队列
|
||||||
|
fn send_event(&self, event: FrontendEvent) -> bool {
|
||||||
|
if *self.emergency_mode.read() {
|
||||||
|
if let FrontendEvent::NoticeMessage { ref status, .. } = event {
|
||||||
|
if status == "info" {
|
||||||
|
log::info!("Skipping info message in emergency mode");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(sender) = &self.sender {
|
||||||
|
match sender.send(event) {
|
||||||
|
Ok(_) => true,
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("Failed to send event to notification queue: {:?}", e);
|
||||||
|
self.stats.total_errors.fetch_add(1, Ordering::SeqCst);
|
||||||
|
*self.stats.last_error_time.write() = Some(Instant::now());
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log::warn!("Notification system not started, can't send event");
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shutdown(&mut self) {
|
||||||
|
log::info!("NotificationSystem shutdown initiated");
|
||||||
|
self.is_running = false;
|
||||||
|
|
||||||
|
// 先关闭发送端,让接收端知道不会再有新消息
|
||||||
|
if let Some(sender) = self.sender.take() {
|
||||||
|
drop(sender);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置超时避免无限等待
|
||||||
|
if let Some(handle) = self.worker_handle.take() {
|
||||||
|
match handle.join() {
|
||||||
|
Ok(_) => {
|
||||||
|
log::info!("NotificationSystem worker thread joined successfully");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("NotificationSystem worker thread join failed: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!("NotificationSystem shutdown completed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub struct Handle {
|
pub struct Handle {
|
||||||
pub app_handle: Arc<RwLock<Option<AppHandle>>>,
|
pub app_handle: Arc<RwLock<Option<AppHandle>>>,
|
||||||
pub is_exiting: Arc<RwLock<bool>>,
|
pub is_exiting: Arc<RwLock<bool>>,
|
||||||
pub core_process: Arc<RwLock<Option<CommandChild>>>,
|
startup_errors: Arc<RwLock<Vec<ErrorMessage>>>,
|
||||||
|
startup_completed: Arc<RwLock<bool>>,
|
||||||
|
notification_system: Arc<RwLock<Option<NotificationSystem>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Handle {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
app_handle: Arc::new(RwLock::new(None)),
|
||||||
|
is_exiting: Arc::new(RwLock::new(false)),
|
||||||
|
startup_errors: Arc::new(RwLock::new(Vec::new())),
|
||||||
|
startup_completed: Arc::new(RwLock::new(false)),
|
||||||
|
notification_system: Arc::new(RwLock::new(Some(NotificationSystem::new()))),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Handle {
|
impl Handle {
|
||||||
pub fn global() -> &'static Handle {
|
pub fn global() -> &'static Handle {
|
||||||
static HANDLE: OnceCell<Handle> = OnceCell::new();
|
static HANDLE: OnceCell<Handle> = OnceCell::new();
|
||||||
|
HANDLE.get_or_init(Handle::default)
|
||||||
HANDLE.get_or_init(|| Handle {
|
|
||||||
app_handle: Arc::new(RwLock::new(None)),
|
|
||||||
is_exiting: Arc::new(RwLock::new(false)),
|
|
||||||
core_process: Arc::new(RwLock::new(None)),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(&self, app_handle: &AppHandle) {
|
pub fn init(&self, app_handle: &AppHandle) {
|
||||||
let mut handle = self.app_handle.write();
|
{
|
||||||
*handle = Some(app_handle.clone());
|
let mut handle = self.app_handle.write();
|
||||||
|
*handle = Some(app_handle.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut system_opt = self.notification_system.write();
|
||||||
|
if let Some(system) = system_opt.as_mut() {
|
||||||
|
system.start();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn app_handle(&self) -> Option<AppHandle> {
|
pub fn app_handle(&self) -> Option<AppHandle> {
|
||||||
@@ -33,7 +296,7 @@ impl Handle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_window(&self) -> Option<WebviewWindow> {
|
pub fn get_window(&self) -> Option<WebviewWindow> {
|
||||||
let app_handle = self.app_handle().unwrap();
|
let app_handle = self.app_handle()?;
|
||||||
let window: Option<WebviewWindow> = app_handle.get_webview_window("main");
|
let window: Option<WebviewWindow> = app_handle.get_webview_window("main");
|
||||||
if window.is_none() {
|
if window.is_none() {
|
||||||
log::debug!(target:"app", "main window not found");
|
log::debug!(target:"app", "main window not found");
|
||||||
@@ -42,48 +305,213 @@ impl Handle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn refresh_clash() {
|
pub fn refresh_clash() {
|
||||||
if let Some(window) = Self::global().get_window() {
|
let handle = Self::global();
|
||||||
log_err!(window.emit("verge://refresh-clash-config", "yes"));
|
if handle.is_exiting() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let system_opt = handle.notification_system.read();
|
||||||
|
if let Some(system) = system_opt.as_ref() {
|
||||||
|
system.send_event(FrontendEvent::RefreshClash);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn refresh_verge() {
|
pub fn refresh_verge() {
|
||||||
if let Some(window) = Self::global().get_window() {
|
let handle = Self::global();
|
||||||
log_err!(window.emit("verge://refresh-verge-config", "yes"));
|
if handle.is_exiting() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let system_opt = handle.notification_system.read();
|
||||||
|
if let Some(system) = system_opt.as_ref() {
|
||||||
|
system.send_event(FrontendEvent::RefreshVerge);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused)]
|
pub fn notify_profile_changed(profile_id: String) {
|
||||||
pub fn refresh_profiles() {
|
let handle = Self::global();
|
||||||
if let Some(window) = Self::global().get_window() {
|
if handle.is_exiting() {
|
||||||
log_err!(window.emit("verge://refresh-profiles-config", "yes"));
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let system_opt = handle.notification_system.read();
|
||||||
|
if let Some(system) = system_opt.as_ref() {
|
||||||
|
system.send_event(FrontendEvent::ProfileChanged {
|
||||||
|
current_profile_id: profile_id,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
log::warn!(
|
||||||
|
"Notification system not initialized when trying to send ProfileChanged event."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn notify_timer_updated(profile_index: String) {
|
||||||
|
let handle = Self::global();
|
||||||
|
if handle.is_exiting() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let system_opt = handle.notification_system.read();
|
||||||
|
if let Some(system) = system_opt.as_ref() {
|
||||||
|
system.send_event(FrontendEvent::TimerUpdated { profile_index });
|
||||||
|
} else {
|
||||||
|
log::warn!(
|
||||||
|
"Notification system not initialized when trying to send TimerUpdated event."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn notify_startup_completed() {
|
||||||
|
let handle = Self::global();
|
||||||
|
if handle.is_exiting() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let system_opt = handle.notification_system.read();
|
||||||
|
if let Some(system) = system_opt.as_ref() {
|
||||||
|
system.send_event(FrontendEvent::StartupCompleted);
|
||||||
|
} else {
|
||||||
|
log::warn!(
|
||||||
|
"Notification system not initialized when trying to send StartupCompleted event."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn notify_profile_update_started(uid: String) {
|
||||||
|
let handle = Self::global();
|
||||||
|
if handle.is_exiting() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let system_opt = handle.notification_system.read();
|
||||||
|
if let Some(system) = system_opt.as_ref() {
|
||||||
|
system.send_event(FrontendEvent::ProfileUpdateStarted { uid });
|
||||||
|
} else {
|
||||||
|
log::warn!("Notification system not initialized when trying to send ProfileUpdateStarted event.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn notify_profile_update_completed(uid: String) {
|
||||||
|
let handle = Self::global();
|
||||||
|
if handle.is_exiting() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let system_opt = handle.notification_system.read();
|
||||||
|
if let Some(system) = system_opt.as_ref() {
|
||||||
|
system.send_event(FrontendEvent::ProfileUpdateCompleted { uid });
|
||||||
|
} else {
|
||||||
|
log::warn!("Notification system not initialized when trying to send ProfileUpdateCompleted event.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 通知前端显示消息队列
|
||||||
pub fn notice_message<S: Into<String>, M: Into<String>>(status: S, msg: M) {
|
pub fn notice_message<S: Into<String>, M: Into<String>>(status: S, msg: M) {
|
||||||
if let Some(window) = Self::global().get_window() {
|
let handle = Self::global();
|
||||||
log_err!(window.emit("verge://notice-message", (status.into(), msg.into())));
|
let status_str = status.into();
|
||||||
|
let msg_str = msg.into();
|
||||||
|
|
||||||
|
if !*handle.startup_completed.read() {
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Frontend,
|
||||||
|
true,
|
||||||
|
"启动过程中发现错误,加入消息队列: {} - {}",
|
||||||
|
status_str,
|
||||||
|
msg_str
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut errors = handle.startup_errors.write();
|
||||||
|
errors.push(ErrorMessage {
|
||||||
|
status: status_str,
|
||||||
|
message: msg_str,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if handle.is_exiting() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let system_opt = handle.notification_system.read();
|
||||||
|
if let Some(system) = system_opt.as_ref() {
|
||||||
|
system.send_event(FrontendEvent::NoticeMessage {
|
||||||
|
status: status_str,
|
||||||
|
message: msg_str,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mark_startup_completed(&self) {
|
||||||
|
{
|
||||||
|
let mut completed = self.startup_completed.write();
|
||||||
|
*completed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.send_startup_errors();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 发送启动时累积的所有错误消息
|
||||||
|
fn send_startup_errors(&self) {
|
||||||
|
let errors = {
|
||||||
|
let mut errors = self.startup_errors.write();
|
||||||
|
std::mem::take(&mut *errors)
|
||||||
|
};
|
||||||
|
|
||||||
|
if errors.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Frontend,
|
||||||
|
true,
|
||||||
|
"发送{}条启动时累积的错误消息",
|
||||||
|
errors.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
// 启动单独线程处理启动错误,避免阻塞主线程
|
||||||
|
let thread_result = thread::Builder::new()
|
||||||
|
.name("startup-errors-sender".into())
|
||||||
|
.spawn(move || {
|
||||||
|
thread::sleep(Duration::from_secs(2));
|
||||||
|
|
||||||
|
let handle = Handle::global();
|
||||||
|
if handle.is_exiting() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let system_opt = handle.notification_system.read();
|
||||||
|
if let Some(system) = system_opt.as_ref() {
|
||||||
|
for error in errors {
|
||||||
|
if handle.is_exiting() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
system.send_event(FrontendEvent::NoticeMessage {
|
||||||
|
status: error.status,
|
||||||
|
message: error.message,
|
||||||
|
});
|
||||||
|
|
||||||
|
thread::sleep(Duration::from_millis(300));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Err(e) = thread_result {
|
||||||
|
log::error!("Failed to spawn startup errors thread: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_is_exiting(&self) {
|
pub fn set_is_exiting(&self) {
|
||||||
let mut is_exiting = self.is_exiting.write();
|
let mut is_exiting = self.is_exiting.write();
|
||||||
*is_exiting = true;
|
*is_exiting = true;
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_core_process(&self, process: CommandChild) {
|
let mut system_opt = self.notification_system.write();
|
||||||
let mut core_process = self.core_process.write();
|
if let Some(system) = system_opt.as_mut() {
|
||||||
*core_process = Some(process);
|
system.shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn take_core_process(&self) -> Option<CommandChild> {
|
|
||||||
let mut core_process = self.core_process.write();
|
|
||||||
core_process.take()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 检查是否有运行中的核心进程
|
|
||||||
pub fn has_core_process(&self) -> bool {
|
|
||||||
self.core_process.read().is_some()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_exiting(&self) -> bool {
|
pub fn is_exiting(&self) -> bool {
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
use crate::core::handle;
|
use crate::{
|
||||||
use crate::{config::Config, feat, log_err};
|
config::Config,
|
||||||
use crate::utils::resolve;
|
core::handle,
|
||||||
|
feat, logging, logging_error,
|
||||||
|
module::lightweight::entry_lightweight_mode,
|
||||||
|
process::AsyncHandler,
|
||||||
|
utils::{logging::Type, resolve},
|
||||||
|
};
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use std::{collections::HashMap, sync::Arc};
|
use std::{collections::HashMap, sync::Arc};
|
||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState};
|
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState};
|
||||||
use tauri::async_runtime;
|
|
||||||
|
|
||||||
pub struct Hotkey {
|
pub struct Hotkey {
|
||||||
current: Arc<Mutex<Vec<String>>>, // 保存当前的热键设置
|
current: Arc<Mutex<Vec<String>>>, // 保存当前的热键设置
|
||||||
@@ -26,19 +30,27 @@ impl Hotkey {
|
|||||||
let verge = Config::verge();
|
let verge = Config::verge();
|
||||||
let enable_global_hotkey = verge.latest().enable_global_hotkey.unwrap_or(true);
|
let enable_global_hotkey = verge.latest().enable_global_hotkey.unwrap_or(true);
|
||||||
|
|
||||||
println!("Initializing hotkeys, global hotkey enabled: {}", enable_global_hotkey);
|
logging!(
|
||||||
log::info!(target: "app", "Initializing hotkeys, global hotkey enabled: {}", enable_global_hotkey);
|
debug,
|
||||||
|
Type::Hotkey,
|
||||||
|
true,
|
||||||
|
"Initializing global hotkeys: {}",
|
||||||
|
enable_global_hotkey
|
||||||
|
);
|
||||||
|
|
||||||
// 如果全局热键被禁用,则不注册热键
|
// 如果全局热键被禁用,则不注册热键
|
||||||
if !enable_global_hotkey {
|
if !enable_global_hotkey {
|
||||||
println!("Global hotkey is disabled, skipping registration");
|
|
||||||
log::info!(target: "app", "Global hotkey is disabled, skipping registration");
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(hotkeys) = verge.latest().hotkeys.as_ref() {
|
if let Some(hotkeys) = verge.latest().hotkeys.as_ref() {
|
||||||
println!("Found {} hotkeys to register", hotkeys.len());
|
logging!(
|
||||||
log::info!(target: "app", "Found {} hotkeys to register", hotkeys.len());
|
debug,
|
||||||
|
Type::Hotkey,
|
||||||
|
true,
|
||||||
|
"Has {} hotkeys need to register",
|
||||||
|
hotkeys.len()
|
||||||
|
);
|
||||||
|
|
||||||
for hotkey in hotkeys.iter() {
|
for hotkey in hotkeys.iter() {
|
||||||
let mut iter = hotkey.split(',');
|
let mut iter = hotkey.split(',');
|
||||||
@@ -47,28 +59,51 @@ impl Hotkey {
|
|||||||
|
|
||||||
match (key, func) {
|
match (key, func) {
|
||||||
(Some(key), Some(func)) => {
|
(Some(key), Some(func)) => {
|
||||||
println!("Registering hotkey: {} -> {}", key, func);
|
logging!(
|
||||||
log::info!(target: "app", "Registering hotkey: {} -> {}", key, func);
|
debug,
|
||||||
|
Type::Hotkey,
|
||||||
|
true,
|
||||||
|
"Registering hotkey: {} -> {}",
|
||||||
|
key,
|
||||||
|
func
|
||||||
|
);
|
||||||
if let Err(e) = self.register(key, func) {
|
if let Err(e) = self.register(key, func) {
|
||||||
println!("Failed to register hotkey {} -> {}: {:?}", key, func, e);
|
logging!(
|
||||||
log::error!(target: "app", "Failed to register hotkey {} -> {}: {:?}", key, func, e);
|
error,
|
||||||
|
Type::Hotkey,
|
||||||
|
true,
|
||||||
|
"Failed to register hotkey {} -> {}: {:?}",
|
||||||
|
key,
|
||||||
|
func,
|
||||||
|
e
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
println!("Successfully registered hotkey {} -> {}", key, func);
|
logging!(
|
||||||
log::info!(target: "app", "Successfully registered hotkey {} -> {}", key, func);
|
debug,
|
||||||
|
Type::Hotkey,
|
||||||
|
"Successfully registered hotkey {} -> {}",
|
||||||
|
key,
|
||||||
|
func
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let key = key.unwrap_or("None");
|
let key = key.unwrap_or("None");
|
||||||
let func = func.unwrap_or("None");
|
let func = func.unwrap_or("None");
|
||||||
println!("Invalid hotkey configuration: `{key}`:`{func}`");
|
logging!(
|
||||||
log::error!(target: "app", "Invalid hotkey configuration: `{key}`:`{func}`");
|
error,
|
||||||
|
Type::Hotkey,
|
||||||
|
true,
|
||||||
|
"Invalid hotkey configuration: `{}`:`{}`",
|
||||||
|
key,
|
||||||
|
func
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.current.lock().clone_from(hotkeys);
|
self.current.lock().clone_from(hotkeys);
|
||||||
} else {
|
} else {
|
||||||
println!("No hotkeys configured");
|
logging!(debug, Type::Hotkey, "No hotkeys configured");
|
||||||
log::info!(target: "app", "No hotkeys configured");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -85,67 +120,129 @@ impl Hotkey {
|
|||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||||
let manager = app_handle.global_shortcut();
|
let manager = app_handle.global_shortcut();
|
||||||
|
|
||||||
println!("Attempting to register hotkey: {} for function: {}", hotkey, func);
|
logging!(
|
||||||
log::info!(target: "app", "Attempting to register hotkey: {} for function: {}", hotkey, func);
|
debug,
|
||||||
|
Type::Hotkey,
|
||||||
|
"Attempting to register hotkey: {} for function: {}",
|
||||||
|
hotkey,
|
||||||
|
func
|
||||||
|
);
|
||||||
|
|
||||||
if manager.is_registered(hotkey) {
|
if manager.is_registered(hotkey) {
|
||||||
println!("Hotkey {} was already registered, unregistering first", hotkey);
|
logging!(
|
||||||
log::info!(target: "app", "Hotkey {} was already registered, unregistering first", hotkey);
|
debug,
|
||||||
|
Type::Hotkey,
|
||||||
|
"Hotkey {} was already registered, unregistering first",
|
||||||
|
hotkey
|
||||||
|
);
|
||||||
manager.unregister(hotkey)?;
|
manager.unregister(hotkey)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let f = match func.trim() {
|
let f = match func.trim() {
|
||||||
"open_or_close_dashboard" => {
|
"open_or_close_dashboard" => {
|
||||||
println!("Registering open_or_close_dashboard function");
|
logging!(
|
||||||
log::info!(target: "app", "Registering open_or_close_dashboard function");
|
debug,
|
||||||
|
Type::Hotkey,
|
||||||
|
"Registering open_or_close_dashboard function"
|
||||||
|
);
|
||||||
|| {
|
|| {
|
||||||
println!("=== Hotkey Dashboard Window Operation Start ===");
|
logging!(
|
||||||
log::info!(target: "app", "=== Hotkey Dashboard Window Operation Start ===");
|
debug,
|
||||||
|
Type::Hotkey,
|
||||||
// 使用 spawn_blocking 来确保在正确的线程上执行
|
true,
|
||||||
async_runtime::spawn_blocking(|| {
|
"=== Hotkey Dashboard Window Operation Start ==="
|
||||||
println!("Toggle dashboard window visibility");
|
);
|
||||||
log::info!(target: "app", "Toggle dashboard window visibility");
|
|
||||||
|
// 检查是否在轻量模式下,如果是,需要同步处理
|
||||||
// 检查窗口是否存在
|
if crate::module::lightweight::is_in_lightweight_mode() {
|
||||||
if let Some(window) = handle::Handle::global().get_window() {
|
logging!(
|
||||||
// 如果窗口可见,则隐藏它
|
info,
|
||||||
if window.is_visible().unwrap_or(false) {
|
Type::Hotkey,
|
||||||
println!("Window is visible, hiding it");
|
true,
|
||||||
log::info!(target: "app", "Window is visible, hiding it");
|
"In lightweight mode, calling open_or_close_dashboard directly"
|
||||||
let _ = window.hide();
|
);
|
||||||
} else {
|
crate::feat::open_or_close_dashboard();
|
||||||
// 如果窗口不可见,则显示它
|
} else {
|
||||||
println!("Window is hidden, showing it");
|
AsyncHandler::spawn(move || async move {
|
||||||
log::info!(target: "app", "Window is hidden, showing it");
|
logging!(
|
||||||
if window.is_minimized().unwrap_or(false) {
|
debug,
|
||||||
let _ = window.unminimize();
|
Type::Hotkey,
|
||||||
|
true,
|
||||||
|
"Toggle dashboard window visibility (async)"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 检查窗口是否存在
|
||||||
|
if let Some(window) = handle::Handle::global().get_window() {
|
||||||
|
// 如果窗口可见,则隐藏
|
||||||
|
match window.is_visible() {
|
||||||
|
Ok(visible) => {
|
||||||
|
if visible {
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Window,
|
||||||
|
true,
|
||||||
|
"Window is visible, hiding it"
|
||||||
|
);
|
||||||
|
let _ = window.hide();
|
||||||
|
} else {
|
||||||
|
// 如果窗口不可见,则显示
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Window,
|
||||||
|
true,
|
||||||
|
"Window is hidden, showing it"
|
||||||
|
);
|
||||||
|
if window.is_minimized().unwrap_or(false) {
|
||||||
|
let _ = window.unminimize();
|
||||||
|
}
|
||||||
|
let _ = window.show();
|
||||||
|
let _ = window.set_focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
logging!(
|
||||||
|
warn,
|
||||||
|
Type::Window,
|
||||||
|
true,
|
||||||
|
"Failed to check window visibility: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
let _ = window.show();
|
||||||
|
let _ = window.set_focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let _ = window.show();
|
} else {
|
||||||
let _ = window.set_focus();
|
// 如果窗口不存在,创建一个新窗口
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Window,
|
||||||
|
true,
|
||||||
|
"Window does not exist, creating a new one"
|
||||||
|
);
|
||||||
|
resolve::create_window(true);
|
||||||
}
|
}
|
||||||
} else {
|
});
|
||||||
// 如果窗口不存在,创建一个新窗口
|
}
|
||||||
println!("Window does not exist, creating a new one");
|
|
||||||
log::info!(target: "app", "Window does not exist, creating a new one");
|
logging!(
|
||||||
resolve::create_window();
|
debug,
|
||||||
}
|
Type::Hotkey,
|
||||||
});
|
"=== Hotkey Dashboard Window Operation End ==="
|
||||||
|
);
|
||||||
println!("=== Hotkey Dashboard Window Operation End ===");
|
|
||||||
log::info!(target: "app", "=== Hotkey Dashboard Window Operation End ===");
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"clash_mode_rule" => || feat::change_clash_mode("rule".into()),
|
"clash_mode_rule" => || feat::change_clash_mode("rule".into()),
|
||||||
"clash_mode_global" => || feat::change_clash_mode("global".into()),
|
"clash_mode_global" => || feat::change_clash_mode("global".into()),
|
||||||
"clash_mode_direct" => || feat::change_clash_mode("direct".into()),
|
"clash_mode_direct" => || feat::change_clash_mode("direct".into()),
|
||||||
"toggle_system_proxy" => || feat::toggle_system_proxy(),
|
"toggle_system_proxy" => || feat::toggle_system_proxy(),
|
||||||
"toggle_tun_mode" => || feat::toggle_tun_mode(None),
|
"toggle_tun_mode" => || feat::toggle_tun_mode(None),
|
||||||
"quit" => || feat::quit(Some(0)),
|
"entry_lightweight_mode" => || entry_lightweight_mode(),
|
||||||
|
"quit" => || feat::quit(),
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
"hide" => || feat::hide(),
|
||||||
|
|
||||||
_ => {
|
_ => {
|
||||||
println!("Invalid function: {}", func);
|
logging!(error, Type::Hotkey, "Invalid function: {}", func);
|
||||||
log::error!(target: "app", "Invalid function: {}", func);
|
|
||||||
bail!("invalid function \"{func}\"");
|
bail!("invalid function \"{func}\"");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -154,34 +251,33 @@ impl Hotkey {
|
|||||||
|
|
||||||
let _ = manager.on_shortcut(hotkey, move |app_handle, hotkey, event| {
|
let _ = manager.on_shortcut(hotkey, move |app_handle, hotkey, event| {
|
||||||
if event.state == ShortcutState::Pressed {
|
if event.state == ShortcutState::Pressed {
|
||||||
println!("Hotkey pressed: {:?}", hotkey);
|
logging!(debug, Type::Hotkey, "Hotkey pressed: {:?}", hotkey);
|
||||||
log::info!(target: "app", "Hotkey pressed: {:?}", hotkey);
|
|
||||||
|
|
||||||
if hotkey.key == Code::KeyQ && is_quit {
|
if hotkey.key == Code::KeyQ && is_quit {
|
||||||
if let Some(window) = app_handle.get_webview_window("main") {
|
if let Some(window) = app_handle.get_webview_window("main") {
|
||||||
if window.is_focused().unwrap_or(false) {
|
if window.is_focused().unwrap_or(false) {
|
||||||
println!("Executing quit function");
|
logging!(debug, Type::Hotkey, "Executing quit function");
|
||||||
log::info!(target: "app", "Executing quit function");
|
|
||||||
f();
|
f();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 直接执行函数,不做任何状态检查
|
// 直接执行函数,不做任何状态检查
|
||||||
println!("Executing function directly");
|
logging!(debug, Type::Hotkey, "Executing function directly");
|
||||||
log::info!(target: "app", "Executing function directly");
|
|
||||||
|
// 获取全局热键状态
|
||||||
// 获取轻量模式状态和全局热键状态
|
let is_enable_global_hotkey = Config::verge()
|
||||||
let is_lite_mode = Config::verge().latest().enable_lite_mode.unwrap_or(false);
|
.latest()
|
||||||
let is_enable_global_hotkey = Config::verge().latest().enable_global_hotkey.unwrap_or(true);
|
.enable_global_hotkey
|
||||||
|
.unwrap_or(true);
|
||||||
// 在轻量模式下或配置了全局热键时,始终执行热键功能
|
|
||||||
if is_lite_mode || is_enable_global_hotkey {
|
if is_enable_global_hotkey {
|
||||||
f();
|
f();
|
||||||
} else if let Some(window) = app_handle.get_webview_window("main") {
|
} else {
|
||||||
|
use crate::utils::window_manager::WindowManager;
|
||||||
// 非轻量模式且未启用全局热键时,只在窗口可见且有焦点的情况下响应热键
|
// 非轻量模式且未启用全局热键时,只在窗口可见且有焦点的情况下响应热键
|
||||||
let is_visible = window.is_visible().unwrap_or(false);
|
let is_visible = WindowManager::is_main_window_visible();
|
||||||
let is_focused = window.is_focused().unwrap_or(false);
|
let is_focused = WindowManager::is_main_window_focused();
|
||||||
|
|
||||||
if is_focused && is_visible {
|
if is_focused && is_visible {
|
||||||
f();
|
f();
|
||||||
}
|
}
|
||||||
@@ -190,8 +286,13 @@ impl Hotkey {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
println!("Successfully registered hotkey {} for {}", hotkey, func);
|
logging!(
|
||||||
log::info!(target: "app", "Successfully registered hotkey {} for {}", hotkey, func);
|
debug,
|
||||||
|
Type::Hotkey,
|
||||||
|
"Successfully registered hotkey {} for {}",
|
||||||
|
hotkey,
|
||||||
|
func
|
||||||
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,7 +300,7 @@ impl Hotkey {
|
|||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||||
let manager = app_handle.global_shortcut();
|
let manager = app_handle.global_shortcut();
|
||||||
manager.unregister(hotkey)?;
|
manager.unregister(hotkey)?;
|
||||||
log::debug!(target: "app", "unregister hotkey {hotkey}");
|
logging!(debug, Type::Hotkey, "Unregister hotkey {}", hotkey);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,7 +316,7 @@ impl Hotkey {
|
|||||||
});
|
});
|
||||||
|
|
||||||
add.iter().for_each(|(key, func)| {
|
add.iter().for_each(|(key, func)| {
|
||||||
log_err!(self.register(key, func));
|
logging_error!(Type::Hotkey, self.register(key, func));
|
||||||
});
|
});
|
||||||
|
|
||||||
*current = new_hotkeys;
|
*current = new_hotkeys;
|
||||||
@@ -230,9 +331,9 @@ impl Hotkey {
|
|||||||
let func = iter.next();
|
let func = iter.next();
|
||||||
let key = iter.next();
|
let key = iter.next();
|
||||||
|
|
||||||
if func.is_some() && key.is_some() {
|
if let (Some(func), Some(key)) = (func, key) {
|
||||||
let func = func.unwrap().trim();
|
let func = func.trim();
|
||||||
let key = key.unwrap().trim();
|
let key = key.trim();
|
||||||
map.insert(key, func);
|
map.insert(key, func);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -272,7 +373,13 @@ impl Drop for Hotkey {
|
|||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||||
if let Err(e) = app_handle.global_shortcut().unregister_all() {
|
if let Err(e) = app_handle.global_shortcut().unregister_all() {
|
||||||
log::error!(target:"app", "Error unregistering all hotkeys: {:?}", e);
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Hotkey,
|
||||||
|
true,
|
||||||
|
"Error unregistering all hotkeys: {:?}",
|
||||||
|
e
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ mod core;
|
|||||||
pub mod handle;
|
pub mod handle;
|
||||||
pub mod hotkey;
|
pub mod hotkey;
|
||||||
pub mod service;
|
pub mod service;
|
||||||
|
pub mod service_ipc;
|
||||||
pub mod sysopt;
|
pub mod sysopt;
|
||||||
pub mod timer;
|
pub mod timer;
|
||||||
pub mod tray;
|
pub mod tray;
|
||||||
pub mod win_uwp;
|
pub mod win_uwp;
|
||||||
|
|
||||||
pub use self::core::*;
|
pub use self::{core::*, timer::Timer};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
340
src-tauri/src/core/service_ipc.rs
Normal file
340
src-tauri/src/core/service_ipc.rs
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
use crate::{logging, utils::logging::Type};
|
||||||
|
use anyhow::{bail, Context, Result};
|
||||||
|
use hmac::{Hmac, Mac};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
const IPC_SOCKET_NAME: &str = if cfg!(windows) {
|
||||||
|
r"\\.\pipe\clash-verge-service"
|
||||||
|
} else {
|
||||||
|
"/tmp/clash-verge-service.sock"
|
||||||
|
};
|
||||||
|
|
||||||
|
// 定义命令类型
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum IpcCommand {
|
||||||
|
GetClash,
|
||||||
|
GetVersion,
|
||||||
|
StartClash,
|
||||||
|
StopClash,
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPC消息格式
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct IpcRequest {
|
||||||
|
pub id: String,
|
||||||
|
pub timestamp: u64,
|
||||||
|
pub command: IpcCommand,
|
||||||
|
pub payload: serde_json::Value,
|
||||||
|
pub signature: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct IpcResponse {
|
||||||
|
pub id: String,
|
||||||
|
pub success: bool,
|
||||||
|
pub data: Option<serde_json::Value>,
|
||||||
|
pub error: Option<String>,
|
||||||
|
pub signature: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 密钥派生函数
|
||||||
|
fn derive_secret_key() -> Vec<u8> {
|
||||||
|
// to do
|
||||||
|
// 从系统安全存储中获取或从程序安装时生成的密钥文件中读取
|
||||||
|
let unique_app_id = "clash-verge-app-secret-fuck-me-until-daylight";
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(unique_app_id.as_bytes());
|
||||||
|
hasher.finalize().to_vec()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建带签名的请求
|
||||||
|
pub fn create_signed_request(
|
||||||
|
command: IpcCommand,
|
||||||
|
payload: serde_json::Value,
|
||||||
|
) -> Result<IpcRequest> {
|
||||||
|
let id = nanoid::nanoid!(32);
|
||||||
|
let timestamp = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_secs();
|
||||||
|
|
||||||
|
let unsigned_request = IpcRequest {
|
||||||
|
id: id.clone(),
|
||||||
|
timestamp,
|
||||||
|
command: command.clone(),
|
||||||
|
payload: payload.clone(),
|
||||||
|
signature: String::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let unsigned_json = serde_json::to_string(&unsigned_request)?;
|
||||||
|
let signature = sign_message(&unsigned_json)?;
|
||||||
|
|
||||||
|
Ok(IpcRequest {
|
||||||
|
id,
|
||||||
|
timestamp,
|
||||||
|
command,
|
||||||
|
payload,
|
||||||
|
signature,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 签名消息
|
||||||
|
fn sign_message(message: &str) -> Result<String> {
|
||||||
|
type HmacSha256 = Hmac<Sha256>;
|
||||||
|
|
||||||
|
let secret_key = derive_secret_key();
|
||||||
|
let mut mac = HmacSha256::new_from_slice(&secret_key).context("HMAC初始化失败")?;
|
||||||
|
|
||||||
|
mac.update(message.as_bytes());
|
||||||
|
let result = mac.finalize();
|
||||||
|
let signature = hex::encode(result.into_bytes());
|
||||||
|
|
||||||
|
Ok(signature)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证响应签名
|
||||||
|
pub fn verify_response_signature(response: &IpcResponse) -> Result<bool> {
|
||||||
|
let verification_response = IpcResponse {
|
||||||
|
id: response.id.clone(),
|
||||||
|
success: response.success,
|
||||||
|
data: response.data.clone(),
|
||||||
|
error: response.error.clone(),
|
||||||
|
signature: String::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let message = serde_json::to_string(&verification_response)?;
|
||||||
|
let expected_signature = sign_message(&message)?;
|
||||||
|
|
||||||
|
Ok(expected_signature == response.signature)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPC连接管理-win
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub async fn send_ipc_request(
|
||||||
|
command: IpcCommand,
|
||||||
|
payload: serde_json::Value,
|
||||||
|
) -> Result<IpcResponse> {
|
||||||
|
use std::{
|
||||||
|
ffi::CString,
|
||||||
|
fs::File,
|
||||||
|
io::{Read, Write},
|
||||||
|
os::windows::io::{FromRawHandle, RawHandle},
|
||||||
|
ptr,
|
||||||
|
};
|
||||||
|
use winapi::um::{
|
||||||
|
fileapi::{CreateFileA, OPEN_EXISTING},
|
||||||
|
handleapi::INVALID_HANDLE_VALUE,
|
||||||
|
winnt::{FILE_SHARE_READ, FILE_SHARE_WRITE, GENERIC_READ, GENERIC_WRITE},
|
||||||
|
};
|
||||||
|
|
||||||
|
logging!(info, Type::Service, true, "正在连接服务 (Windows)...");
|
||||||
|
|
||||||
|
let command_type = format!("{:?}", command);
|
||||||
|
|
||||||
|
let request = match create_signed_request(command, payload) {
|
||||||
|
Ok(req) => req,
|
||||||
|
Err(e) => {
|
||||||
|
logging!(error, Type::Service, true, "创建签名请求失败: {}", e);
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let request_json = serde_json::to_string(&request)?;
|
||||||
|
|
||||||
|
let result = tokio::task::spawn_blocking(move || -> Result<IpcResponse> {
|
||||||
|
let c_pipe_name = match CString::new(IPC_SOCKET_NAME) {
|
||||||
|
Ok(name) => name,
|
||||||
|
Err(e) => {
|
||||||
|
logging!(error, Type::Service, true, "创建CString失败: {}", e);
|
||||||
|
return Err(anyhow::anyhow!("创建CString失败: {}", e));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let handle = unsafe {
|
||||||
|
CreateFileA(
|
||||||
|
c_pipe_name.as_ptr(),
|
||||||
|
GENERIC_READ | GENERIC_WRITE,
|
||||||
|
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||||
|
ptr::null_mut(),
|
||||||
|
OPEN_EXISTING,
|
||||||
|
0,
|
||||||
|
ptr::null_mut(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
if handle == INVALID_HANDLE_VALUE {
|
||||||
|
let error = std::io::Error::last_os_error();
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Service,
|
||||||
|
true,
|
||||||
|
"连接到服务命名管道失败: {}",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
return Err(anyhow::anyhow!("无法连接到服务命名管道: {}", error));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut pipe = unsafe { File::from_raw_handle(handle as RawHandle) };
|
||||||
|
logging!(info, Type::Service, true, "服务连接成功 (Windows)");
|
||||||
|
|
||||||
|
let request_bytes = request_json.as_bytes();
|
||||||
|
let len_bytes = (request_bytes.len() as u32).to_be_bytes();
|
||||||
|
|
||||||
|
if let Err(e) = pipe.write_all(&len_bytes) {
|
||||||
|
logging!(error, Type::Service, true, "写入请求长度失败: {}", e);
|
||||||
|
return Err(anyhow::anyhow!("写入请求长度失败: {}", e));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = pipe.write_all(request_bytes) {
|
||||||
|
logging!(error, Type::Service, true, "写入请求内容失败: {}", e);
|
||||||
|
return Err(anyhow::anyhow!("写入请求内容失败: {}", e));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = pipe.flush() {
|
||||||
|
logging!(error, Type::Service, true, "刷新管道失败: {}", e);
|
||||||
|
return Err(anyhow::anyhow!("刷新管道失败: {}", e));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut response_len_bytes = [0u8; 4];
|
||||||
|
if let Err(e) = pipe.read_exact(&mut response_len_bytes) {
|
||||||
|
logging!(error, Type::Service, true, "读取响应长度失败: {}", e);
|
||||||
|
return Err(anyhow::anyhow!("读取响应长度失败: {}", e));
|
||||||
|
}
|
||||||
|
|
||||||
|
let response_len = u32::from_be_bytes(response_len_bytes) as usize;
|
||||||
|
|
||||||
|
let mut response_bytes = vec![0u8; response_len];
|
||||||
|
if let Err(e) = pipe.read_exact(&mut response_bytes) {
|
||||||
|
logging!(error, Type::Service, true, "读取响应内容失败: {}", e);
|
||||||
|
return Err(anyhow::anyhow!("读取响应内容失败: {}", e));
|
||||||
|
}
|
||||||
|
|
||||||
|
let response: IpcResponse = match serde_json::from_slice::<IpcResponse>(&response_bytes) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => {
|
||||||
|
logging!(error, Type::Service, true, "服务响应解析失败: {}", e);
|
||||||
|
return Err(anyhow::anyhow!("解析响应失败: {}", e));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match verify_response_signature(&response) {
|
||||||
|
Ok(valid) => {
|
||||||
|
if !valid {
|
||||||
|
logging!(error, Type::Service, true, "服务响应签名验证失败");
|
||||||
|
bail!("服务响应签名验证失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
logging!(error, Type::Service, true, "验证响应签名时出错: {}", e);
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Service,
|
||||||
|
true,
|
||||||
|
"IPC请求完成: 命令={}, 成功={}",
|
||||||
|
command_type,
|
||||||
|
response.success
|
||||||
|
);
|
||||||
|
Ok(response)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPC连接管理-unix
|
||||||
|
#[cfg(target_family = "unix")]
|
||||||
|
pub async fn send_ipc_request(
|
||||||
|
command: IpcCommand,
|
||||||
|
payload: serde_json::Value,
|
||||||
|
) -> Result<IpcResponse> {
|
||||||
|
use std::os::unix::net::UnixStream;
|
||||||
|
|
||||||
|
logging!(info, Type::Service, true, "正在连接服务 (Unix)...");
|
||||||
|
|
||||||
|
let command_type = format!("{:?}", command);
|
||||||
|
|
||||||
|
let request = match create_signed_request(command, payload) {
|
||||||
|
Ok(req) => req,
|
||||||
|
Err(e) => {
|
||||||
|
logging!(error, Type::Service, true, "创建签名请求失败: {}", e);
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let request_json = serde_json::to_string(&request)?;
|
||||||
|
|
||||||
|
let mut stream = match UnixStream::connect(IPC_SOCKET_NAME) {
|
||||||
|
Ok(s) => {
|
||||||
|
logging!(info, Type::Service, true, "服务连接成功 (Unix)");
|
||||||
|
s
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
logging!(error, Type::Service, true, "连接到Unix套接字失败: {}", e);
|
||||||
|
return Err(anyhow::anyhow!("无法连接到服务Unix套接字: {}", e));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let request_bytes = request_json.as_bytes();
|
||||||
|
let len_bytes = (request_bytes.len() as u32).to_be_bytes();
|
||||||
|
|
||||||
|
if let Err(e) = std::io::Write::write_all(&mut stream, &len_bytes) {
|
||||||
|
logging!(error, Type::Service, true, "写入请求长度失败: {}", e);
|
||||||
|
return Err(anyhow::anyhow!("写入请求长度失败: {}", e));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = std::io::Write::write_all(&mut stream, request_bytes) {
|
||||||
|
logging!(error, Type::Service, true, "写入请求内容失败: {}", e);
|
||||||
|
return Err(anyhow::anyhow!("写入请求内容失败: {}", e));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut response_len_bytes = [0u8; 4];
|
||||||
|
if let Err(e) = std::io::Read::read_exact(&mut stream, &mut response_len_bytes) {
|
||||||
|
logging!(error, Type::Service, true, "读取响应长度失败: {}", e);
|
||||||
|
return Err(anyhow::anyhow!("读取响应长度失败: {}", e));
|
||||||
|
}
|
||||||
|
|
||||||
|
let response_len = u32::from_be_bytes(response_len_bytes) as usize;
|
||||||
|
|
||||||
|
let mut response_bytes = vec![0u8; response_len];
|
||||||
|
if let Err(e) = std::io::Read::read_exact(&mut stream, &mut response_bytes) {
|
||||||
|
logging!(error, Type::Service, true, "读取响应内容失败: {}", e);
|
||||||
|
return Err(anyhow::anyhow!("读取响应内容失败: {}", e));
|
||||||
|
}
|
||||||
|
|
||||||
|
let response: IpcResponse = match serde_json::from_slice::<IpcResponse>(&response_bytes) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => {
|
||||||
|
logging!(error, Type::Service, true, "服务响应解析失败: {}", e,);
|
||||||
|
return Err(anyhow::anyhow!("解析响应失败: {}", e));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match verify_response_signature(&response) {
|
||||||
|
Ok(valid) => {
|
||||||
|
if !valid {
|
||||||
|
logging!(error, Type::Service, true, "服务响应签名验证失败");
|
||||||
|
bail!("服务响应签名验证失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
logging!(error, Type::Service, true, "验证响应签名时出错: {}", e);
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Service,
|
||||||
|
true,
|
||||||
|
"IPC请求完成: 命令={}, 成功={}",
|
||||||
|
command_type,
|
||||||
|
response.success
|
||||||
|
);
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
use crate::core::handle::Handle;
|
#[cfg(target_os = "windows")]
|
||||||
|
use crate::utils::autostart as startup_shortcut;
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{Config, IVerge},
|
config::{Config, IVerge},
|
||||||
log_err,
|
core::handle::Handle,
|
||||||
|
logging, logging_error,
|
||||||
|
process::AsyncHandler,
|
||||||
|
utils::logging::Type,
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
@@ -15,8 +19,6 @@ use tokio::time::{sleep, Duration};
|
|||||||
pub struct Sysopt {
|
pub struct Sysopt {
|
||||||
update_sysproxy: Arc<TokioMutex<bool>>,
|
update_sysproxy: Arc<TokioMutex<bool>>,
|
||||||
reset_sysproxy: Arc<TokioMutex<bool>>,
|
reset_sysproxy: Arc<TokioMutex<bool>>,
|
||||||
/// helps to auto launch the app
|
|
||||||
auto_launch: Arc<Mutex<bool>>,
|
|
||||||
/// record whether the guard async is running or not
|
/// record whether the guard async is running or not
|
||||||
guard_state: Arc<Mutex<bool>>,
|
guard_state: Arc<Mutex<bool>>,
|
||||||
}
|
}
|
||||||
@@ -57,7 +59,6 @@ impl Sysopt {
|
|||||||
SYSOPT.get_or_init(|| Sysopt {
|
SYSOPT.get_or_init(|| Sysopt {
|
||||||
update_sysproxy: Arc::new(TokioMutex::new(false)),
|
update_sysproxy: Arc::new(TokioMutex::new(false)),
|
||||||
reset_sysproxy: Arc::new(TokioMutex::new(false)),
|
reset_sysproxy: Arc::new(TokioMutex::new(false)),
|
||||||
auto_launch: Arc::new(Mutex::new(false)),
|
|
||||||
guard_state: Arc::new(false.into()),
|
guard_state: Arc::new(false.into()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -77,12 +78,16 @@ impl Sysopt {
|
|||||||
.unwrap_or(Config::clash().data().get_mixed_port());
|
.unwrap_or(Config::clash().data().get_mixed_port());
|
||||||
let pac_port = IVerge::get_singleton_port();
|
let pac_port = IVerge::get_singleton_port();
|
||||||
|
|
||||||
let (sys_enable, pac_enable) = {
|
let (sys_enable, pac_enable, proxy_host) = {
|
||||||
let verge = Config::verge();
|
let verge = Config::verge();
|
||||||
let verge = verge.latest();
|
let verge = verge.latest();
|
||||||
(
|
(
|
||||||
verge.enable_system_proxy.unwrap_or(false),
|
verge.enable_system_proxy.unwrap_or(false),
|
||||||
verge.proxy_auto_config.unwrap_or(false),
|
verge.proxy_auto_config.unwrap_or(false),
|
||||||
|
verge
|
||||||
|
.proxy_host
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| String::from("127.0.0.1")),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -90,13 +95,13 @@ impl Sysopt {
|
|||||||
{
|
{
|
||||||
let mut sys = Sysproxy {
|
let mut sys = Sysproxy {
|
||||||
enable: false,
|
enable: false,
|
||||||
host: String::from("127.0.0.1"),
|
host: proxy_host.clone(),
|
||||||
port,
|
port,
|
||||||
bypass: get_bypass(),
|
bypass: get_bypass(),
|
||||||
};
|
};
|
||||||
let mut auto = Autoproxy {
|
let mut auto = Autoproxy {
|
||||||
enable: false,
|
enable: false,
|
||||||
url: format!("http://127.0.0.1:{pac_port}/commands/pac"),
|
url: format!("http://{}:{}/commands/pac", proxy_host, pac_port),
|
||||||
};
|
};
|
||||||
|
|
||||||
if !sys_enable {
|
if !sys_enable {
|
||||||
@@ -126,8 +131,7 @@ impl Sysopt {
|
|||||||
if !sys_enable {
|
if !sys_enable {
|
||||||
return self.reset_sysproxy().await;
|
return self.reset_sysproxy().await;
|
||||||
}
|
}
|
||||||
use crate::core::handle::Handle;
|
use crate::{core::handle::Handle, utils::dirs};
|
||||||
use crate::utils::dirs;
|
|
||||||
use anyhow::bail;
|
use anyhow::bail;
|
||||||
use tauri_plugin_shell::ShellExt;
|
use tauri_plugin_shell::ShellExt;
|
||||||
|
|
||||||
@@ -141,7 +145,7 @@ impl Sysopt {
|
|||||||
|
|
||||||
let shell = app_handle.shell();
|
let shell = app_handle.shell();
|
||||||
let output = if pac_enable {
|
let output = if pac_enable {
|
||||||
let address = format!("http://{}:{}/commands/pac", "127.0.0.1", pac_port);
|
let address = format!("http://{}:{}/commands/pac", proxy_host, pac_port);
|
||||||
let output = shell
|
let output = shell
|
||||||
.command(sysproxy_exe.as_path().to_str().unwrap())
|
.command(sysproxy_exe.as_path().to_str().unwrap())
|
||||||
.args(["pac", address.as_str()])
|
.args(["pac", address.as_str()])
|
||||||
@@ -150,7 +154,7 @@ impl Sysopt {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
output
|
output
|
||||||
} else {
|
} else {
|
||||||
let address = format!("{}:{}", "127.0.0.1", port);
|
let address = format!("{}:{}", proxy_host, port);
|
||||||
let bypass = get_bypass();
|
let bypass = get_bypass();
|
||||||
let output = shell
|
let output = shell
|
||||||
.command(sysproxy_exe.as_path().to_str().unwrap())
|
.command(sysproxy_exe.as_path().to_str().unwrap())
|
||||||
@@ -185,8 +189,7 @@ impl Sysopt {
|
|||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
use crate::core::handle::Handle;
|
use crate::{core::handle::Handle, utils::dirs};
|
||||||
use crate::utils::dirs;
|
|
||||||
use anyhow::bail;
|
use anyhow::bail;
|
||||||
use tauri_plugin_shell::ShellExt;
|
use tauri_plugin_shell::ShellExt;
|
||||||
|
|
||||||
@@ -217,31 +220,93 @@ impl Sysopt {
|
|||||||
|
|
||||||
/// update the startup
|
/// update the startup
|
||||||
pub fn update_launch(&self) -> Result<()> {
|
pub fn update_launch(&self) -> Result<()> {
|
||||||
let _lock = self.auto_launch.lock();
|
let enable_auto_launch = { Config::verge().latest().enable_auto_launch };
|
||||||
let enable = { Config::verge().latest().enable_auto_launch };
|
let is_enable = enable_auto_launch.unwrap_or(false);
|
||||||
let enable = enable.unwrap_or(false);
|
logging!(info, true, "Setting auto-launch state to: {:?}", is_enable);
|
||||||
let app_handle = Handle::global().app_handle().unwrap();
|
|
||||||
let autostart_manager = app_handle.autolaunch();
|
// 首先尝试使用快捷方式方法
|
||||||
println!("enable: {}", enable);
|
#[cfg(target_os = "windows")]
|
||||||
match enable {
|
{
|
||||||
true => log_err!(autostart_manager.enable()),
|
if is_enable {
|
||||||
false => log_err!(autostart_manager.disable()),
|
if let Err(e) = startup_shortcut::create_shortcut() {
|
||||||
};
|
log::error!(target: "app", "创建启动快捷方式失败: {}", e);
|
||||||
|
// 如果快捷方式创建失败,回退到原来的方法
|
||||||
|
self.try_original_autostart_method(is_enable);
|
||||||
|
} else {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
} else if let Err(e) = startup_shortcut::remove_shortcut() {
|
||||||
|
log::error!(target: "app", "删除启动快捷方式失败: {}", e);
|
||||||
|
self.try_original_autostart_method(is_enable);
|
||||||
|
} else {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
{
|
||||||
|
// 非Windows平台使用原来的方法
|
||||||
|
self.try_original_autostart_method(is_enable);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 尝试使用原来的自启动方法
|
||||||
|
fn try_original_autostart_method(&self, is_enable: bool) {
|
||||||
|
let app_handle = Handle::global().app_handle().unwrap();
|
||||||
|
let autostart_manager = app_handle.autolaunch();
|
||||||
|
|
||||||
|
if is_enable {
|
||||||
|
logging_error!(Type::System, true, "{:?}", autostart_manager.enable());
|
||||||
|
} else {
|
||||||
|
logging_error!(Type::System, true, "{:?}", autostart_manager.disable());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取当前自启动的实际状态
|
||||||
|
pub fn get_launch_status(&self) -> Result<bool> {
|
||||||
|
// 首先尝试检查快捷方式是否存在
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
match startup_shortcut::is_shortcut_enabled() {
|
||||||
|
Ok(enabled) => {
|
||||||
|
log::info!(target: "app", "快捷方式自启动状态: {}", enabled);
|
||||||
|
return Ok(enabled);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!(target: "app", "检查快捷方式失败,尝试原来的方法: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 回退到原来的方法
|
||||||
|
let app_handle = Handle::global().app_handle().unwrap();
|
||||||
|
let autostart_manager = app_handle.autolaunch();
|
||||||
|
|
||||||
|
match autostart_manager.is_enabled() {
|
||||||
|
Ok(status) => {
|
||||||
|
log::info!(target: "app", "Auto launch status: {}", status);
|
||||||
|
Ok(status)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!(target: "app", "Failed to get auto launch status: {}", e);
|
||||||
|
Err(anyhow::anyhow!("Failed to get auto launch status: {}", e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn guard_proxy(&self) {
|
fn guard_proxy(&self) {
|
||||||
let _lock = self.guard_state.lock();
|
let _lock = self.guard_state.lock();
|
||||||
|
|
||||||
tauri::async_runtime::spawn(async move {
|
AsyncHandler::spawn(move || async move {
|
||||||
// default duration is 10s
|
// default duration is 10s
|
||||||
let mut wait_secs = 10u64;
|
let mut wait_secs = 10u64;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
sleep(Duration::from_secs(wait_secs)).await;
|
sleep(Duration::from_secs(wait_secs)).await;
|
||||||
|
|
||||||
let (enable, guard, guard_duration, pac) = {
|
let (enable, guard, guard_duration, pac, proxy_host) = {
|
||||||
let verge = Config::verge();
|
let verge = Config::verge();
|
||||||
let verge = verge.latest();
|
let verge = verge.latest();
|
||||||
(
|
(
|
||||||
@@ -249,6 +314,10 @@ impl Sysopt {
|
|||||||
verge.enable_proxy_guard.unwrap_or(false),
|
verge.enable_proxy_guard.unwrap_or(false),
|
||||||
verge.proxy_guard_duration.unwrap_or(10),
|
verge.proxy_guard_duration.unwrap_or(10),
|
||||||
verge.proxy_auto_config.unwrap_or(false),
|
verge.proxy_auto_config.unwrap_or(false),
|
||||||
|
verge
|
||||||
|
.proxy_host
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| String::from("127.0.0.1")),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -262,86 +331,130 @@ impl Sysopt {
|
|||||||
|
|
||||||
log::debug!(target: "app", "try to guard the system proxy");
|
log::debug!(target: "app", "try to guard the system proxy");
|
||||||
|
|
||||||
let sysproxy = Sysproxy::get_system_proxy();
|
// 获取期望的代理端口
|
||||||
let autoproxy = Autoproxy::get_auto_proxy();
|
let port = Config::verge()
|
||||||
if sysproxy.is_err() || autoproxy.is_err() {
|
.latest()
|
||||||
log::error!(target: "app", "failed to get the system proxy");
|
.verge_mixed_port
|
||||||
continue;
|
.unwrap_or(Config::clash().data().get_mixed_port());
|
||||||
}
|
|
||||||
|
|
||||||
let sysproxy_enable = sysproxy.ok().map(|s| s.enable).unwrap_or(false);
|
|
||||||
let autoproxy_enable = autoproxy.ok().map(|s| s.enable).unwrap_or(false);
|
|
||||||
|
|
||||||
if sysproxy_enable || autoproxy_enable {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let port = {
|
|
||||||
Config::verge()
|
|
||||||
.latest()
|
|
||||||
.verge_mixed_port
|
|
||||||
.unwrap_or(Config::clash().data().get_mixed_port())
|
|
||||||
};
|
|
||||||
let pac_port = IVerge::get_singleton_port();
|
let pac_port = IVerge::get_singleton_port();
|
||||||
#[cfg(not(target_os = "windows"))]
|
let bypass = get_bypass();
|
||||||
{
|
|
||||||
if pac {
|
|
||||||
let autoproxy = Autoproxy {
|
|
||||||
enable: true,
|
|
||||||
url: format!("http://127.0.0.1:{pac_port}/commands/pac"),
|
|
||||||
};
|
|
||||||
log_err!(autoproxy.set_auto_proxy());
|
|
||||||
} else {
|
|
||||||
let sysproxy = Sysproxy {
|
|
||||||
enable: true,
|
|
||||||
host: "127.0.0.1".into(),
|
|
||||||
port,
|
|
||||||
bypass: get_bypass(),
|
|
||||||
};
|
|
||||||
|
|
||||||
log_err!(sysproxy.set_system_proxy());
|
// 检查系统代理配置
|
||||||
|
if pac {
|
||||||
|
// 检查 PAC 代理设置
|
||||||
|
let expected_url = format!("http://{}:{}/commands/pac", proxy_host, pac_port);
|
||||||
|
let autoproxy = match Autoproxy::get_auto_proxy() {
|
||||||
|
Ok(ap) => ap,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!(target: "app", "failed to get the auto proxy: {}", e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检查自动代理是否启用且URL是否正确
|
||||||
|
if !autoproxy.enable || autoproxy.url != expected_url {
|
||||||
|
log::info!(target: "app", "auto proxy settings changed, restoring...");
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
{
|
||||||
|
let new_autoproxy = Autoproxy {
|
||||||
|
enable: true,
|
||||||
|
url: expected_url,
|
||||||
|
};
|
||||||
|
logging_error!(Type::System, true, new_autoproxy.set_auto_proxy());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
use crate::{core::handle::Handle, utils::dirs};
|
||||||
|
use tauri_plugin_shell::ShellExt;
|
||||||
|
|
||||||
|
let app_handle = Handle::global().app_handle().unwrap();
|
||||||
|
let binary_path = match dirs::service_path() {
|
||||||
|
Ok(path) => path,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!(target: "app", "failed to get service path: {}", e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
|
||||||
|
if !sysproxy_exe.exists() {
|
||||||
|
log::error!(target: "app", "sysproxy.exe not found");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let shell = app_handle.shell();
|
||||||
|
let output = shell
|
||||||
|
.command(sysproxy_exe.as_path().to_str().unwrap())
|
||||||
|
.args(["pac", expected_url.as_str()])
|
||||||
|
.output()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
log::error!(target: "app", "failed to set auto proxy");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 检查常规系统代理设置
|
||||||
|
let sysproxy = match Sysproxy::get_system_proxy() {
|
||||||
|
Ok(sp) => sp,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!(target: "app", "failed to get the system proxy: {}", e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检查系统代理是否启用且配置是否匹配
|
||||||
|
if !sysproxy.enable || sysproxy.host != proxy_host || sysproxy.port != port {
|
||||||
|
log::info!(target: "app", "system proxy settings changed, restoring...");
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
{
|
||||||
|
let new_sysproxy = Sysproxy {
|
||||||
|
enable: true,
|
||||||
|
host: proxy_host.clone(),
|
||||||
|
port,
|
||||||
|
bypass: bypass.clone(),
|
||||||
|
};
|
||||||
|
logging_error!(Type::System, true, new_sysproxy.set_system_proxy());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
use crate::{core::handle::Handle, utils::dirs};
|
||||||
|
use tauri_plugin_shell::ShellExt;
|
||||||
|
|
||||||
|
let app_handle = Handle::global().app_handle().unwrap();
|
||||||
|
let binary_path = match dirs::service_path() {
|
||||||
|
Ok(path) => path,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!(target: "app", "failed to get service path: {}", e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
|
||||||
|
if !sysproxy_exe.exists() {
|
||||||
|
log::error!(target: "app", "sysproxy.exe not found");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let address = format!("{}:{}", proxy_host, port);
|
||||||
|
let shell = app_handle.shell();
|
||||||
|
let output = shell
|
||||||
|
.command(sysproxy_exe.as_path().to_str().unwrap())
|
||||||
|
.args(["global", address.as_str(), bypass.as_ref()])
|
||||||
|
.output()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
log::error!(target: "app", "failed to set system proxy");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{
|
|
||||||
use crate::core::handle::Handle;
|
|
||||||
use crate::utils::dirs;
|
|
||||||
use tauri_plugin_shell::ShellExt;
|
|
||||||
|
|
||||||
let app_handle = Handle::global().app_handle().unwrap();
|
|
||||||
|
|
||||||
let binary_path = dirs::service_path().unwrap();
|
|
||||||
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
|
|
||||||
if !sysproxy_exe.exists() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let shell = app_handle.shell();
|
|
||||||
let output = if pac {
|
|
||||||
let address = format!("http://{}:{}/commands/pac", "127.0.0.1", pac_port);
|
|
||||||
|
|
||||||
shell
|
|
||||||
.command(sysproxy_exe.as_path().to_str().unwrap())
|
|
||||||
.args(["pac", address.as_str()])
|
|
||||||
.output()
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
} else {
|
|
||||||
let address = format!("{}:{}", "127.0.0.1", port);
|
|
||||||
let bypass = get_bypass();
|
|
||||||
|
|
||||||
shell
|
|
||||||
.command(sysproxy_exe.as_path().to_str().unwrap())
|
|
||||||
.args(["global", address.as_str(), bypass.as_ref()])
|
|
||||||
.output()
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
};
|
|
||||||
if !output.status.success() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,32 @@
|
|||||||
use crate::config::Config;
|
use crate::{config::Config, feat, logging, logging_error, utils::logging::Type};
|
||||||
use crate::feat;
|
|
||||||
use crate::core::CoreManager;
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use delay_timer::prelude::{DelayTimer, DelayTimerBuilder, TaskBuilder};
|
use delay_timer::prelude::{DelayTimer, DelayTimerBuilder, TaskBuilder};
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::{Mutex, RwLock};
|
||||||
use std::collections::HashMap;
|
use std::{collections::HashMap, sync::Arc};
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
type TaskID = u64;
|
type TaskID = u64;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct TimerTask {
|
||||||
|
pub task_id: TaskID,
|
||||||
|
pub interval_minutes: u64,
|
||||||
|
#[allow(unused)]
|
||||||
|
pub last_run: i64, // Timestamp of last execution
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Timer {
|
pub struct Timer {
|
||||||
/// cron manager
|
/// cron manager
|
||||||
delay_timer: Arc<Mutex<DelayTimer>>,
|
pub delay_timer: Arc<RwLock<DelayTimer>>,
|
||||||
|
|
||||||
/// save the current state
|
/// save the current state - using RwLock for better read concurrency
|
||||||
timer_map: Arc<Mutex<HashMap<String, (TaskID, u64)>>>,
|
pub timer_map: Arc<RwLock<HashMap<String, TimerTask>>>,
|
||||||
|
|
||||||
/// increment id
|
/// increment id - kept as mutex since it's just a counter
|
||||||
timer_count: Arc<Mutex<TaskID>>,
|
pub timer_count: Arc<Mutex<TaskID>>,
|
||||||
|
|
||||||
|
/// Flag to mark if timer is initialized - atomic for better performance
|
||||||
|
pub initialized: Arc<std::sync::atomic::AtomicBool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Timer {
|
impl Timer {
|
||||||
@@ -26,68 +34,190 @@ impl Timer {
|
|||||||
static TIMER: OnceCell<Timer> = OnceCell::new();
|
static TIMER: OnceCell<Timer> = OnceCell::new();
|
||||||
|
|
||||||
TIMER.get_or_init(|| Timer {
|
TIMER.get_or_init(|| Timer {
|
||||||
delay_timer: Arc::new(Mutex::new(DelayTimerBuilder::default().build())),
|
delay_timer: Arc::new(RwLock::new(DelayTimerBuilder::default().build())),
|
||||||
timer_map: Arc::new(Mutex::new(HashMap::new())),
|
timer_map: Arc::new(RwLock::new(HashMap::new())),
|
||||||
timer_count: Arc::new(Mutex::new(1)),
|
timer_count: Arc::new(Mutex::new(1)),
|
||||||
|
initialized: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// restore timer
|
/// Initialize timer with better error handling and atomic operations
|
||||||
pub fn init(&self) -> Result<()> {
|
pub fn init(&self) -> Result<()> {
|
||||||
self.refresh()?;
|
// Use compare_exchange for thread-safe initialization check
|
||||||
|
if self
|
||||||
|
.initialized
|
||||||
|
.compare_exchange(
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
std::sync::atomic::Ordering::SeqCst,
|
||||||
|
std::sync::atomic::Ordering::SeqCst,
|
||||||
|
)
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
logging!(debug, Type::Timer, "Timer already initialized, skipping...");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
logging!(info, Type::Timer, true, "Initializing timer...");
|
||||||
|
|
||||||
|
// Initialize timer tasks
|
||||||
|
if let Err(e) = self.refresh() {
|
||||||
|
// Reset initialization flag on error
|
||||||
|
self.initialized
|
||||||
|
.store(false, std::sync::atomic::Ordering::SeqCst);
|
||||||
|
logging_error!(Type::Timer, false, "Failed to initialize timer: {}", e);
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
let timer_map = self.timer_map.read();
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Timer,
|
||||||
|
"已注册的定时任务数量: {}",
|
||||||
|
timer_map.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
for (uid, task) in timer_map.iter() {
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Timer,
|
||||||
|
"注册了定时任务 - uid={}, interval={}min, task_id={}",
|
||||||
|
uid,
|
||||||
|
task.interval_minutes,
|
||||||
|
task.task_id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let cur_timestamp = chrono::Local::now().timestamp();
|
let cur_timestamp = chrono::Local::now().timestamp();
|
||||||
|
|
||||||
let timer_map = self.timer_map.lock();
|
// Collect profiles that need immediate update
|
||||||
let delay_timer = self.delay_timer.lock();
|
let profiles_to_update = if let Some(items) = Config::profiles().latest().get_items() {
|
||||||
|
|
||||||
if let Some(items) = Config::profiles().latest().get_items() {
|
|
||||||
items
|
items
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|item| {
|
.filter_map(|item| {
|
||||||
// mins to seconds
|
let interval = item.option.as_ref()?.update_interval? as i64;
|
||||||
let interval = ((item.option.as_ref()?.update_interval?) as i64) * 60;
|
|
||||||
let updated = item.updated? as i64;
|
let updated = item.updated? as i64;
|
||||||
|
let uid = item.uid.as_ref()?;
|
||||||
|
|
||||||
if interval > 0 && cur_timestamp - updated >= interval {
|
if interval > 0 && cur_timestamp - updated >= interval * 60 {
|
||||||
Some(item)
|
logging!(info, Type::Timer, "需要立即更新的配置: uid={}", uid);
|
||||||
|
Some(uid.clone())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.for_each(|item| {
|
.collect::<Vec<String>>()
|
||||||
if let Some(uid) = item.uid.as_ref() {
|
} else {
|
||||||
if let Some((task_id, _)) = timer_map.get(uid) {
|
Vec::new()
|
||||||
crate::log_err!(delay_timer.advance_task(*task_id));
|
};
|
||||||
}
|
|
||||||
|
// Advance tasks outside of locks to minimize lock contention
|
||||||
|
if !profiles_to_update.is_empty() {
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Timer,
|
||||||
|
"需要立即更新的配置数量: {}",
|
||||||
|
profiles_to_update.len()
|
||||||
|
);
|
||||||
|
let timer_map = self.timer_map.read();
|
||||||
|
let delay_timer = self.delay_timer.write();
|
||||||
|
|
||||||
|
for uid in profiles_to_update {
|
||||||
|
if let Some(task) = timer_map.get(&uid) {
|
||||||
|
logging!(info, Type::Timer, "立即执行任务: uid={}", uid);
|
||||||
|
if let Err(e) = delay_timer.advance_task(task.task_id) {
|
||||||
|
logging!(warn, Type::Timer, "Failed to advance task {}: {}", uid, e);
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logging!(info, Type::Timer, "Timer initialization completed");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Correctly update all cron tasks
|
/// Refresh timer tasks with better error handling
|
||||||
pub fn refresh(&self) -> Result<()> {
|
pub fn refresh(&self) -> Result<()> {
|
||||||
|
// Generate diff outside of lock to minimize lock contention
|
||||||
let diff_map = self.gen_diff();
|
let diff_map = self.gen_diff();
|
||||||
|
|
||||||
let mut timer_map = self.timer_map.lock();
|
if diff_map.is_empty() {
|
||||||
let mut delay_timer = self.delay_timer.lock();
|
logging!(debug, Type::Timer, "No timer changes needed");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
for (uid, diff) in diff_map.into_iter() {
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Timer,
|
||||||
|
"Refreshing {} timer tasks",
|
||||||
|
diff_map.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Apply changes while holding locks
|
||||||
|
let mut timer_map = self.timer_map.write();
|
||||||
|
let mut delay_timer = self.delay_timer.write();
|
||||||
|
|
||||||
|
for (uid, diff) in diff_map {
|
||||||
match diff {
|
match diff {
|
||||||
DiffFlag::Del(tid) => {
|
DiffFlag::Del(tid) => {
|
||||||
let _ = timer_map.remove(&uid);
|
timer_map.remove(&uid);
|
||||||
crate::log_err!(delay_timer.remove_task(tid));
|
if let Err(e) = delay_timer.remove_task(tid) {
|
||||||
|
logging!(
|
||||||
|
warn,
|
||||||
|
Type::Timer,
|
||||||
|
"Failed to remove task {} for uid {}: {}",
|
||||||
|
tid,
|
||||||
|
uid,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logging!(debug, Type::Timer, "Removed task {} for uid {}", tid, uid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
DiffFlag::Add(tid, val) => {
|
DiffFlag::Add(tid, interval) => {
|
||||||
let _ = timer_map.insert(uid.clone(), (tid, val));
|
let task = TimerTask {
|
||||||
crate::log_err!(self.add_task(&mut delay_timer, uid, tid, val));
|
task_id: tid,
|
||||||
|
interval_minutes: interval,
|
||||||
|
last_run: chrono::Local::now().timestamp(),
|
||||||
|
};
|
||||||
|
|
||||||
|
timer_map.insert(uid.clone(), task);
|
||||||
|
|
||||||
|
if let Err(e) = self.add_task(&mut delay_timer, uid.clone(), tid, interval) {
|
||||||
|
logging_error!(Type::Timer, "Failed to add task for uid {}: {}", uid, e);
|
||||||
|
timer_map.remove(&uid); // Rollback on failure
|
||||||
|
} else {
|
||||||
|
logging!(debug, Type::Timer, "Added task {} for uid {}", tid, uid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
DiffFlag::Mod(tid, val) => {
|
DiffFlag::Mod(tid, interval) => {
|
||||||
let _ = timer_map.insert(uid.clone(), (tid, val));
|
// Remove old task first
|
||||||
crate::log_err!(delay_timer.remove_task(tid));
|
if let Err(e) = delay_timer.remove_task(tid) {
|
||||||
crate::log_err!(self.add_task(&mut delay_timer, uid, tid, val));
|
logging!(
|
||||||
|
warn,
|
||||||
|
Type::Timer,
|
||||||
|
"Failed to remove old task {} for uid {}: {}",
|
||||||
|
tid,
|
||||||
|
uid,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then add the new one
|
||||||
|
let task = TimerTask {
|
||||||
|
task_id: tid,
|
||||||
|
interval_minutes: interval,
|
||||||
|
last_run: chrono::Local::now().timestamp(),
|
||||||
|
};
|
||||||
|
|
||||||
|
timer_map.insert(uid.clone(), task);
|
||||||
|
|
||||||
|
if let Err(e) = self.add_task(&mut delay_timer, uid.clone(), tid, interval) {
|
||||||
|
logging_error!(Type::Timer, "Failed to update task for uid {}: {}", uid, e);
|
||||||
|
timer_map.remove(&uid); // Rollback on failure
|
||||||
|
} else {
|
||||||
|
logging!(debug, Type::Timer, "Updated task {} for uid {}", tid, uid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,59 +225,106 @@ impl Timer {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// generate a uid -> update_interval map
|
/// Generate map of profile UIDs to update intervals
|
||||||
fn gen_map(&self) -> HashMap<String, u64> {
|
fn gen_map(&self) -> HashMap<String, u64> {
|
||||||
let mut new_map = HashMap::new();
|
let mut new_map = HashMap::new();
|
||||||
|
|
||||||
if let Some(items) = Config::profiles().latest().get_items() {
|
if let Some(items) = Config::profiles().latest().get_items() {
|
||||||
for item in items.iter() {
|
for item in items.iter() {
|
||||||
if item.option.is_some() {
|
if let Some(option) = item.option.as_ref() {
|
||||||
let option = item.option.as_ref().unwrap();
|
if let (Some(interval), Some(uid)) = (option.update_interval, &item.uid) {
|
||||||
let interval = option.update_interval.unwrap_or(0);
|
if interval > 0 {
|
||||||
|
logging!(
|
||||||
if interval > 0 {
|
debug,
|
||||||
new_map.insert(item.uid.clone().unwrap(), interval);
|
Type::Timer,
|
||||||
|
"找到定时更新配置: uid={}, interval={}min",
|
||||||
|
uid,
|
||||||
|
interval
|
||||||
|
);
|
||||||
|
new_map.insert(uid.clone(), interval);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logging!(
|
||||||
|
debug,
|
||||||
|
Type::Timer,
|
||||||
|
"生成的定时更新配置数量: {}",
|
||||||
|
new_map.len()
|
||||||
|
);
|
||||||
new_map
|
new_map
|
||||||
}
|
}
|
||||||
|
|
||||||
/// generate the diff map for refresh
|
/// Generate differences between current and new timer configuration
|
||||||
fn gen_diff(&self) -> HashMap<String, DiffFlag> {
|
fn gen_diff(&self) -> HashMap<String, DiffFlag> {
|
||||||
let mut diff_map = HashMap::new();
|
let mut diff_map = HashMap::new();
|
||||||
|
|
||||||
let timer_map = self.timer_map.lock();
|
|
||||||
|
|
||||||
let new_map = self.gen_map();
|
let new_map = self.gen_map();
|
||||||
let cur_map = &timer_map;
|
|
||||||
|
|
||||||
cur_map.iter().for_each(|(uid, (tid, val))| {
|
// Read lock for comparing current state
|
||||||
let new_val = new_map.get(uid).unwrap_or(&0);
|
let timer_map = self.timer_map.read();
|
||||||
|
logging!(
|
||||||
|
debug,
|
||||||
|
Type::Timer,
|
||||||
|
"当前 timer_map 大小: {}",
|
||||||
|
timer_map.len()
|
||||||
|
);
|
||||||
|
|
||||||
if *new_val == 0 {
|
// Find tasks to modify or delete
|
||||||
diff_map.insert(uid.clone(), DiffFlag::Del(*tid));
|
for (uid, task) in timer_map.iter() {
|
||||||
} else if new_val != val {
|
match new_map.get(uid) {
|
||||||
diff_map.insert(uid.clone(), DiffFlag::Mod(*tid, *new_val));
|
Some(&interval) if interval != task.interval_minutes => {
|
||||||
|
// Task exists but interval changed
|
||||||
|
logging!(
|
||||||
|
debug,
|
||||||
|
Type::Timer,
|
||||||
|
"定时任务间隔变更: uid={}, 旧={}, 新={}",
|
||||||
|
uid,
|
||||||
|
task.interval_minutes,
|
||||||
|
interval
|
||||||
|
);
|
||||||
|
diff_map.insert(uid.clone(), DiffFlag::Mod(task.task_id, interval));
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// Task no longer needed
|
||||||
|
logging!(debug, Type::Timer, "定时任务已删除: uid={}", uid);
|
||||||
|
diff_map.insert(uid.clone(), DiffFlag::Del(task.task_id));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Task exists with same interval, no change needed
|
||||||
|
logging!(debug, Type::Timer, "定时任务保持不变: uid={}", uid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
let mut count = self.timer_count.lock();
|
// Find new tasks to add
|
||||||
|
let mut next_id = *self.timer_count.lock();
|
||||||
|
|
||||||
new_map.iter().for_each(|(uid, val)| {
|
for (uid, &interval) in new_map.iter() {
|
||||||
if cur_map.get(uid).is_none() {
|
if !timer_map.contains_key(uid) {
|
||||||
diff_map.insert(uid.clone(), DiffFlag::Add(*count, *val));
|
logging!(
|
||||||
|
debug,
|
||||||
*count += 1;
|
Type::Timer,
|
||||||
|
"新增定时任务: uid={}, interval={}min",
|
||||||
|
uid,
|
||||||
|
interval
|
||||||
|
);
|
||||||
|
diff_map.insert(uid.clone(), DiffFlag::Add(next_id, interval));
|
||||||
|
next_id += 1;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
// Update counter only if we added new tasks
|
||||||
|
if next_id > *self.timer_count.lock() {
|
||||||
|
*self.timer_count.lock() = next_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
logging!(debug, Type::Timer, "定时任务变更数量: {}", diff_map.len());
|
||||||
diff_map
|
diff_map
|
||||||
}
|
}
|
||||||
|
|
||||||
/// add a cron task
|
/// Add a timer task with better error handling
|
||||||
fn add_task(
|
fn add_task(
|
||||||
&self,
|
&self,
|
||||||
delay_timer: &mut DelayTimer,
|
delay_timer: &mut DelayTimer,
|
||||||
@@ -155,12 +332,26 @@ impl Timer {
|
|||||||
tid: TaskID,
|
tid: TaskID,
|
||||||
minutes: u64,
|
minutes: u64,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Timer,
|
||||||
|
"Adding task: uid={}, id={}, interval={}min",
|
||||||
|
uid,
|
||||||
|
tid,
|
||||||
|
minutes
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create a task with reasonable retries and backoff
|
||||||
let task = TaskBuilder::default()
|
let task = TaskBuilder::default()
|
||||||
.set_task_id(tid)
|
.set_task_id(tid)
|
||||||
.set_maximum_parallel_runnable_num(1)
|
.set_maximum_parallel_runnable_num(1)
|
||||||
.set_frequency_repeated_by_minutes(minutes)
|
.set_frequency_repeated_by_minutes(minutes)
|
||||||
// .set_frequency_repeated_by_seconds(minutes) // for test
|
.spawn_async_routine(move || {
|
||||||
.spawn_async_routine(move || Self::async_task(uid.to_owned()))
|
let uid = uid.clone();
|
||||||
|
async move {
|
||||||
|
Self::async_task(uid).await;
|
||||||
|
}
|
||||||
|
})
|
||||||
.context("failed to create timer task")?;
|
.context("failed to create timer task")?;
|
||||||
|
|
||||||
delay_timer
|
delay_timer
|
||||||
@@ -170,20 +361,118 @@ impl Timer {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// the task runner
|
/// Get next update time for a profile
|
||||||
|
pub fn get_next_update_time(&self, uid: &str) -> Option<i64> {
|
||||||
|
logging!(info, Type::Timer, "获取下次更新时间,uid={}", uid);
|
||||||
|
|
||||||
|
let timer_map = self.timer_map.read();
|
||||||
|
let task = match timer_map.get(uid) {
|
||||||
|
Some(t) => t,
|
||||||
|
None => {
|
||||||
|
logging!(warn, Type::Timer, "找不到对应的定时任务,uid={}", uid);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the profile updated timestamp
|
||||||
|
let profiles_config = Config::profiles();
|
||||||
|
let profiles = profiles_config.latest();
|
||||||
|
let items = match profiles.get_items() {
|
||||||
|
Some(i) => i,
|
||||||
|
None => {
|
||||||
|
logging!(warn, Type::Timer, "获取配置列表失败");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let profile = match items.iter().find(|item| item.uid.as_deref() == Some(uid)) {
|
||||||
|
Some(p) => p,
|
||||||
|
None => {
|
||||||
|
logging!(warn, Type::Timer, "找不到对应的配置,uid={}", uid);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let updated = profile.updated.unwrap_or(0) as i64;
|
||||||
|
|
||||||
|
// Calculate next update time
|
||||||
|
if updated > 0 && task.interval_minutes > 0 {
|
||||||
|
let next_time = updated + (task.interval_minutes as i64 * 60);
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Timer,
|
||||||
|
"计算得到下次更新时间: {}, uid={}",
|
||||||
|
next_time,
|
||||||
|
uid
|
||||||
|
);
|
||||||
|
Some(next_time)
|
||||||
|
} else {
|
||||||
|
logging!(
|
||||||
|
warn,
|
||||||
|
Type::Timer,
|
||||||
|
"更新时间或间隔无效,updated={}, interval={}",
|
||||||
|
updated,
|
||||||
|
task.interval_minutes
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emit update events for frontend notification
|
||||||
|
fn emit_update_event(_uid: &str, _is_start: bool) {
|
||||||
|
#[cfg(any(feature = "verge-dev", feature = "default"))]
|
||||||
|
{
|
||||||
|
if _is_start {
|
||||||
|
super::handle::Handle::notify_profile_update_started(_uid.to_string());
|
||||||
|
} else {
|
||||||
|
super::handle::Handle::notify_profile_update_completed(_uid.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Async task with better error handling and logging
|
||||||
async fn async_task(uid: String) {
|
async fn async_task(uid: String) {
|
||||||
log::info!(target: "app", "running timer task `{uid}`");
|
let task_start = std::time::Instant::now();
|
||||||
|
logging!(info, Type::Timer, "Running timer task for profile: {}", uid);
|
||||||
// 使用更轻量级的更新方式
|
|
||||||
if let Err(e) = feat::update_profile(uid.clone(), None).await {
|
match tokio::time::timeout(std::time::Duration::from_secs(40), async {
|
||||||
log::error!(target: "app", "timer task update error: {}", e);
|
Self::emit_update_event(&uid, true);
|
||||||
return;
|
|
||||||
|
let is_current = Config::profiles().latest().current.as_ref() == Some(&uid);
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Timer,
|
||||||
|
"配置 {} 是否为当前激活配置: {}",
|
||||||
|
uid,
|
||||||
|
is_current
|
||||||
|
);
|
||||||
|
|
||||||
|
feat::update_profile(uid.clone(), None, Some(is_current)).await
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(result) => match result {
|
||||||
|
Ok(_) => {
|
||||||
|
let duration = task_start.elapsed().as_millis();
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Timer,
|
||||||
|
"Timer task completed successfully for uid: {} (took {}ms)",
|
||||||
|
uid,
|
||||||
|
duration
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
logging_error!(Type::Timer, "Failed to update profile uid {}: {}", uid, e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(_) => {
|
||||||
|
logging_error!(Type::Timer, false, "Timer task timed out for uid: {}", uid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 只有更新成功后才刷新配置
|
// Emit completed event
|
||||||
if let Err(e) = CoreManager::global().update_config().await {
|
Self::emit_update_event(&uid, false);
|
||||||
log::error!(target: "app", "timer task refresh error: {}", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,22 +1,20 @@
|
|||||||
use crate::module::mihomo::Rate;
|
use crate::{
|
||||||
use crate::module::mihomo::MihomoManager;
|
module::mihomo::{MihomoManager, Rate},
|
||||||
use crate::utils::help::format_bytes_speed;
|
utils::help::format_bytes_speed,
|
||||||
|
};
|
||||||
use ab_glyph::FontArc;
|
use ab_glyph::FontArc;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use futures::Stream;
|
use futures::Stream;
|
||||||
use image::{GenericImageView, Rgba, RgbaImage};
|
use image::{GenericImageView, Rgba, RgbaImage};
|
||||||
use imageproc::drawing::draw_text_mut;
|
use imageproc::drawing::draw_text_mut;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use std::io::Cursor;
|
use std::{io::Cursor, sync::Arc};
|
||||||
use std::sync::Arc;
|
|
||||||
use tokio_tungstenite::tungstenite::http;
|
use tokio_tungstenite::tungstenite::http;
|
||||||
use tokio_tungstenite::tungstenite::Message;
|
|
||||||
use tungstenite::client::IntoClientRequest;
|
use tungstenite::client::IntoClientRequest;
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct SpeedRate {
|
pub struct SpeedRate {
|
||||||
rate: Arc<Mutex<(Rate, Rate)>>,
|
rate: Arc<Mutex<(Rate, Rate)>>,
|
||||||
last_update: Arc<Mutex<std::time::Instant>>,
|
last_update: Arc<Mutex<std::time::Instant>>,
|
||||||
// 移除 base_image,不再缓存原始图像
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SpeedRate {
|
impl SpeedRate {
|
||||||
@@ -39,12 +37,20 @@ impl SpeedRate {
|
|||||||
|
|
||||||
let (current, previous) = &mut *rates;
|
let (current, previous) = &mut *rates;
|
||||||
|
|
||||||
// 如果速率变化不大(小于10%),则不更新
|
// Avoid unnecessary float conversions for small value checks
|
||||||
let should_update = {
|
let should_update = if current.up < 1000 && down < 1000 {
|
||||||
let up_change = (current.up as f64 - up as f64).abs() / (current.up as f64 + 1.0);
|
// For small values, always update to ensure accuracy
|
||||||
let down_change =
|
current.up != up || current.down != down
|
||||||
(current.down as f64 - down as f64).abs() / (current.down as f64 + 1.0);
|
} else {
|
||||||
up_change > 0.1 || down_change > 0.1
|
// For larger values, use integer math to check for >5% change
|
||||||
|
// Multiply by 20 instead of dividing by 0.05 to avoid floating point
|
||||||
|
let up_threshold = current.up / 20;
|
||||||
|
let down_threshold = current.down / 20;
|
||||||
|
|
||||||
|
(up > current.up && up - current.up > up_threshold)
|
||||||
|
|| (up < current.up && current.up - up > up_threshold)
|
||||||
|
|| (down > current.down && down - current.down > down_threshold)
|
||||||
|
|| (down < current.down && current.down - down > down_threshold)
|
||||||
};
|
};
|
||||||
|
|
||||||
if !should_update {
|
if !should_update {
|
||||||
@@ -70,44 +76,71 @@ impl SpeedRate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 分离图标加载和速率渲染
|
// 分离图标加载和速率渲染
|
||||||
pub fn add_speed_text(icon_bytes: Vec<u8>, rate: Option<Rate>) -> Result<Vec<u8>> {
|
pub fn add_speed_text(
|
||||||
let rate = rate.unwrap_or(Rate { up: 0, down: 0 });
|
is_custom_icon: bool,
|
||||||
|
icon_bytes: Option<Vec<u8>>,
|
||||||
|
rate: Option<&Rate>,
|
||||||
|
) -> Result<Vec<u8>> {
|
||||||
|
let rate = rate.unwrap_or(&Rate { up: 0, down: 0 });
|
||||||
|
|
||||||
// 加载原始图标
|
let (mut icon_width, mut icon_height) = (0, 256);
|
||||||
let icon_image = image::load_from_memory(&icon_bytes)?;
|
let icon_image = if let Some(bytes) = icon_bytes.clone() {
|
||||||
let (icon_width, icon_height) = (icon_image.width(), icon_image.height());
|
let icon_image = image::load_from_memory(&bytes)?;
|
||||||
|
icon_width = icon_image.width();
|
||||||
|
icon_height = icon_image.height();
|
||||||
|
icon_image
|
||||||
|
} else {
|
||||||
|
// 返回一个空的 RGBA 图像
|
||||||
|
image::DynamicImage::new_rgba8(0, 0)
|
||||||
|
};
|
||||||
|
|
||||||
// 判断是否为彩色图标
|
let total_width = match (is_custom_icon, icon_bytes.is_some()) {
|
||||||
let is_colorful =
|
(true, true) => 510,
|
||||||
!crate::utils::help::is_monochrome_image_from_bytes(&icon_bytes).unwrap_or(false);
|
(true, false) => 740,
|
||||||
|
(false, false) => 740,
|
||||||
|
(false, true) => icon_width + 740,
|
||||||
|
};
|
||||||
|
|
||||||
// 增加文本宽度和间距
|
// println!(
|
||||||
let text_width = 580; // 文本区域宽度
|
// "icon_height: {}, icon_wight: {}, total_width: {}",
|
||||||
let total_width = icon_width + text_width;
|
// icon_height, icon_width, total_width
|
||||||
|
// );
|
||||||
|
|
||||||
// 创建新的透明画布
|
// 创建新的透明画布
|
||||||
let mut combined_image = RgbaImage::new(total_width, icon_height);
|
let mut combined_image = RgbaImage::new(total_width, icon_height);
|
||||||
|
|
||||||
// 将原始图标绘制到新画布的左侧
|
// 将原始图标绘制到新画布的左侧
|
||||||
for y in 0..icon_height {
|
if icon_bytes.is_some() {
|
||||||
for x in 0..icon_width {
|
for y in 0..icon_height {
|
||||||
let pixel = icon_image.get_pixel(x, y);
|
for x in 0..icon_width {
|
||||||
combined_image.put_pixel(x, y, pixel);
|
let pixel = icon_image.get_pixel(x, y);
|
||||||
|
combined_image.put_pixel(x, y, pixel);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let is_colorful = if let Some(bytes) = icon_bytes.clone() {
|
||||||
|
!crate::utils::help::is_monochrome_image_from_bytes(&bytes).unwrap_or(false)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
// 选择文本颜色
|
// 选择文本颜色
|
||||||
let (text_color, shadow_color) = if is_colorful {
|
let (text_color, shadow_color) = if is_colorful {
|
||||||
// 彩色图标使用黑色文本和轻微白色阴影
|
|
||||||
(
|
(
|
||||||
Rgba([255u8, 255u8, 255u8, 255u8]),
|
Rgba([144u8, 144u8, 144u8, 255u8]),
|
||||||
Rgba([0u8, 0u8, 0u8, 160u8]),
|
// Rgba([255u8, 255u8, 255u8, 128u8]),
|
||||||
|
Rgba([0u8, 0u8, 0u8, 128u8]),
|
||||||
)
|
)
|
||||||
|
// (
|
||||||
|
// Rgba([160u8, 160u8, 160u8, 255u8]),
|
||||||
|
// // Rgba([255u8, 255u8, 255u8, 128u8]),
|
||||||
|
// Rgba([0u8, 0u8, 0u8, 255u8]),
|
||||||
|
// )
|
||||||
} else {
|
} else {
|
||||||
// 单色图标使用白色文本和轻微黑色阴影
|
|
||||||
(
|
(
|
||||||
Rgba([255u8, 255u8, 255u8, 255u8]),
|
Rgba([255u8, 255u8, 255u8, 255u8]),
|
||||||
Rgba([0u8, 0u8, 0u8, 120u8]),
|
Rgba([0u8, 0u8, 0u8, 128u8]),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
// 减小字体大小以适应文本区域
|
// 减小字体大小以适应文本区域
|
||||||
@@ -117,17 +150,30 @@ impl SpeedRate {
|
|||||||
let scale = ab_glyph::PxScale::from(font_size);
|
let scale = ab_glyph::PxScale::from(font_size);
|
||||||
|
|
||||||
// 使用更简洁的速率格式
|
// 使用更简洁的速率格式
|
||||||
let up_text = format_bytes_speed(rate.up);
|
let up_text = format!("↑ {}", format_bytes_speed(rate.up));
|
||||||
let down_text = format_bytes_speed(rate.down);
|
let down_text = format!("↓ {}", format_bytes_speed(rate.down));
|
||||||
|
|
||||||
|
// For test rate display
|
||||||
|
// let down_text = format!("↓ {}", format_bytes_speed(102 * 1020 * 1024));
|
||||||
|
|
||||||
// 计算文本位置,确保垂直间距合适
|
// 计算文本位置,确保垂直间距合适
|
||||||
// 修改文本位置为居右显示
|
// 修改文本位置为居右显示
|
||||||
let up_text_width = imageproc::drawing::text_size(scale, &font, &up_text).0 as u32;
|
|
||||||
let down_text_width = imageproc::drawing::text_size(scale, &font, &down_text).0 as u32;
|
|
||||||
|
|
||||||
// 计算右对齐的文本位置
|
// 计算右对齐的文本位置
|
||||||
let up_text_x = total_width - up_text_width;
|
// let up_text_width = imageproc::drawing::text_size(scale, &font, &up_text).0 as u32;
|
||||||
let down_text_x = total_width - down_text_width;
|
// let down_text_width = imageproc::drawing::text_size(scale, &font, &down_text).0 as u32;
|
||||||
|
// let up_text_x = total_width - up_text_width;
|
||||||
|
// let down_text_x = total_width - down_text_width;
|
||||||
|
|
||||||
|
// 计算左对齐的文本位置
|
||||||
|
let (up_text_x, down_text_x) = {
|
||||||
|
if is_custom_icon || icon_bytes.is_some() {
|
||||||
|
let text_left_offset = 30;
|
||||||
|
let left_begin = icon_width + text_left_offset;
|
||||||
|
(left_begin, left_begin)
|
||||||
|
} else {
|
||||||
|
(icon_width, icon_width)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 优化垂直位置,使速率显示的高度和上下间距正好等于图标大小
|
// 优化垂直位置,使速率显示的高度和上下间距正好等于图标大小
|
||||||
let text_height = font_size as i32;
|
let text_height = font_size as i32;
|
||||||
@@ -194,41 +240,92 @@ pub struct Traffic {
|
|||||||
impl Traffic {
|
impl Traffic {
|
||||||
pub async fn get_traffic_stream() -> Result<impl Stream<Item = Result<Traffic, anyhow::Error>>>
|
pub async fn get_traffic_stream() -> Result<impl Stream<Item = Result<Traffic, anyhow::Error>>>
|
||||||
{
|
{
|
||||||
use futures::stream::{self, StreamExt};
|
use futures::{
|
||||||
|
future::FutureExt,
|
||||||
|
stream::{self, StreamExt},
|
||||||
|
};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
// 先处理错误和超时情况
|
||||||
let stream = Box::pin(
|
let stream = Box::pin(
|
||||||
stream::unfold((), |_| async {
|
stream::unfold((), move |_| async move {
|
||||||
loop {
|
'retry: loop {
|
||||||
|
log::info!(target: "app", "establishing traffic websocket connection");
|
||||||
let (url, token) = MihomoManager::get_traffic_ws_url();
|
let (url, token) = MihomoManager::get_traffic_ws_url();
|
||||||
let mut request = url.into_client_request().unwrap();
|
let mut request = match url.into_client_request() {
|
||||||
request
|
Ok(req) => req,
|
||||||
.headers_mut()
|
|
||||||
.insert(http::header::AUTHORIZATION, token);
|
|
||||||
|
|
||||||
match tokio_tungstenite::connect_async(request).await {
|
|
||||||
Ok((ws_stream, _)) => {
|
|
||||||
log::info!(target: "app", "traffic ws connection established");
|
|
||||||
return Some((
|
|
||||||
ws_stream.map(|msg| {
|
|
||||||
msg.map_err(anyhow::Error::from).and_then(|msg: Message| {
|
|
||||||
let data = msg.into_text()?;
|
|
||||||
let json: serde_json::Value = serde_json::from_str(&data)?;
|
|
||||||
Ok(Traffic {
|
|
||||||
up: json["up"].as_u64().unwrap_or(0),
|
|
||||||
down: json["down"].as_u64().unwrap_or(0),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!(target: "app", "traffic ws connection failed: {e}");
|
log::error!(target: "app", "failed to create websocket request: {}", e);
|
||||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||||
continue;
|
continue 'retry;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
request.headers_mut().insert(http::header::AUTHORIZATION, token);
|
||||||
|
|
||||||
|
match tokio::time::timeout(Duration::from_secs(3),
|
||||||
|
tokio_tungstenite::connect_async(request)
|
||||||
|
).await {
|
||||||
|
Ok(Ok((ws_stream, _))) => {
|
||||||
|
log::info!(target: "app", "traffic websocket connection established");
|
||||||
|
// 设置流超时控制
|
||||||
|
let traffic_stream = ws_stream
|
||||||
|
.take_while(|msg| {
|
||||||
|
let continue_stream = msg.is_ok();
|
||||||
|
async move { continue_stream }.boxed()
|
||||||
|
})
|
||||||
|
.filter_map(|msg| async move {
|
||||||
|
match msg {
|
||||||
|
Ok(msg) => {
|
||||||
|
if !msg.is_text() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
match tokio::time::timeout(
|
||||||
|
Duration::from_millis(200),
|
||||||
|
async { msg.into_text() }
|
||||||
|
).await {
|
||||||
|
Ok(Ok(text)) => {
|
||||||
|
match serde_json::from_str::<serde_json::Value>(&text) {
|
||||||
|
Ok(json) => {
|
||||||
|
let up = json["up"].as_u64().unwrap_or(0);
|
||||||
|
let down = json["down"].as_u64().unwrap_or(0);
|
||||||
|
Some(Ok(Traffic { up, down }))
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!(target: "app", "traffic json parse error: {} for {}", e, text);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Ok(Err(e)) => {
|
||||||
|
log::warn!(target: "app", "traffic text conversion error: {}", e);
|
||||||
|
None
|
||||||
|
},
|
||||||
|
Err(_) => {
|
||||||
|
log::warn!(target: "app", "traffic text processing timeout");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
log::error!(target: "app", "traffic websocket error: {}", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Some((traffic_stream, ()));
|
||||||
|
},
|
||||||
|
Ok(Err(e)) => {
|
||||||
|
log::error!(target: "app", "traffic websocket connection failed: {}", e);
|
||||||
|
},
|
||||||
|
Err(_) => {
|
||||||
|
log::error!(target: "app", "traffic websocket connection timed out");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.flatten(),
|
.flatten(),
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "mihomo_api"
|
|
||||||
edition = "2024"
|
|
||||||
|
|
||||||
[features]
|
|
||||||
debug = []
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
reqwest = { version = "0.12.12", features = ["json"] }
|
|
||||||
serde = { version = "1.0.218", features = ["derive"] }
|
|
||||||
serde_json = "1.0.140"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
tokio = { version = "1.43.0", features = ["rt", "macros"] }
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
use reqwest::header::HeaderMap;
|
|
||||||
use serde_json::json;
|
|
||||||
use std::{
|
|
||||||
sync::{Arc, Mutex},
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
pub mod model;
|
|
||||||
pub use model::{MihomoData, MihomoManager};
|
|
||||||
|
|
||||||
impl MihomoManager {
|
|
||||||
pub fn new(mihomo_server: String, headers: HeaderMap) -> Self {
|
|
||||||
Self {
|
|
||||||
mihomo_server,
|
|
||||||
data: Arc::new(Mutex::new(MihomoData {
|
|
||||||
proxies: serde_json::Value::Null,
|
|
||||||
providers_proxies: serde_json::Value::Null,
|
|
||||||
})),
|
|
||||||
headers: headers,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_proxies(&self, proxies: serde_json::Value) {
|
|
||||||
let mut data = self.data.lock().unwrap();
|
|
||||||
data.proxies = proxies;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_providers_proxies(&self, providers_proxies: serde_json::Value) {
|
|
||||||
let mut data = self.data.lock().unwrap();
|
|
||||||
data.providers_proxies = providers_proxies;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_mihomo_server(&self) -> String {
|
|
||||||
self.mihomo_server.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_proxies(&self) -> serde_json::Value {
|
|
||||||
let data = self.data.lock().unwrap();
|
|
||||||
data.proxies.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_providers_proxies(&self) -> serde_json::Value {
|
|
||||||
let data = self.data.lock().unwrap();
|
|
||||||
data.providers_proxies.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_request(
|
|
||||||
&self,
|
|
||||||
method: &str,
|
|
||||||
url: String,
|
|
||||||
data: Option<serde_json::Value>,
|
|
||||||
) -> Result<serde_json::Value, String> {
|
|
||||||
let client_response = reqwest::ClientBuilder::new()
|
|
||||||
.default_headers(self.headers.clone())
|
|
||||||
.no_proxy()
|
|
||||||
.timeout(Duration::from_secs(2))
|
|
||||||
.build()
|
|
||||||
.map_err(|e| e.to_string())?
|
|
||||||
.request(
|
|
||||||
match method {
|
|
||||||
"GET" => reqwest::Method::GET,
|
|
||||||
"PUT" => reqwest::Method::PUT,
|
|
||||||
"POST" => reqwest::Method::POST,
|
|
||||||
"PATCH" => reqwest::Method::PATCH,
|
|
||||||
_ => reqwest::Method::GET,
|
|
||||||
},
|
|
||||||
&url,
|
|
||||||
)
|
|
||||||
.json(&data.unwrap_or(json!({})))
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
let response = if method != "PUT" {
|
|
||||||
client_response.json::<serde_json::Value>().await
|
|
||||||
} else {
|
|
||||||
client_response.text().await.map(|text| json!(text))
|
|
||||||
}
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
return Ok(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn refresh_proxies(&self) -> Result<&Self, String> {
|
|
||||||
let url = format!("{}/proxies", self.mihomo_server);
|
|
||||||
let proxies = self.send_request("GET", url, None).await?;
|
|
||||||
self.update_proxies(proxies);
|
|
||||||
Ok(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn refresh_providers_proxies(&self) -> Result<&Self, String> {
|
|
||||||
let url = format!("{}/providers/proxies", self.mihomo_server);
|
|
||||||
let providers_proxies = self.send_request("GET", url, None).await?;
|
|
||||||
self.update_providers_proxies(providers_proxies);
|
|
||||||
Ok(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MihomoManager {
|
|
||||||
pub async fn put_configs_force(&self, clash_config_path: &str) -> Result<(), String> {
|
|
||||||
let url = format!("{}/configs?force=true", self.mihomo_server);
|
|
||||||
let payload = serde_json::json!({
|
|
||||||
"path": clash_config_path,
|
|
||||||
});
|
|
||||||
let _response = self.send_request("PUT", url, Some(payload)).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn patch_configs(&self, config: serde_json::Value) -> Result<(), String> {
|
|
||||||
let url = format!("{}/configs", self.mihomo_server);
|
|
||||||
let response = self.send_request("PATCH", url, Some(config)).await?;
|
|
||||||
if response["code"] == 204 {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(response["message"]
|
|
||||||
.as_str()
|
|
||||||
.unwrap_or("unknown error")
|
|
||||||
.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn test_proxy_delay(
|
|
||||||
&self,
|
|
||||||
name: &str,
|
|
||||||
test_url: Option<String>,
|
|
||||||
timeout: i32,
|
|
||||||
) -> Result<serde_json::Value, String> {
|
|
||||||
let test_url = test_url.unwrap_or("http://cp.cloudflare.com/generate_204".to_string());
|
|
||||||
let url = format!(
|
|
||||||
"{}/proxies/{}/delay?url={}&timeout={}",
|
|
||||||
self.mihomo_server, name, test_url, timeout
|
|
||||||
);
|
|
||||||
let response = self.send_request("GET", url, None).await?;
|
|
||||||
return Ok(response);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
use std::sync::{Arc, Mutex};
|
|
||||||
use reqwest::header::HeaderMap;
|
|
||||||
|
|
||||||
pub struct MihomoData {
|
|
||||||
pub(crate) proxies: serde_json::Value,
|
|
||||||
pub(crate) providers_proxies: serde_json::Value,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct MihomoManager {
|
|
||||||
pub(crate) mihomo_server: String,
|
|
||||||
pub(crate) data: Arc<Mutex<MihomoData>>,
|
|
||||||
pub(crate) headers: HeaderMap,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "debug")]
|
|
||||||
impl Drop for MihomoData {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
println!("Dropping MihomoData");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "debug")]
|
|
||||||
impl Drop for MihomoManager {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
println!("Dropping MihomoManager");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
use mihomo_api;
|
|
||||||
use reqwest::header::HeaderMap;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_mihomo_manager_init() {
|
|
||||||
let manager = mihomo_api::MihomoManager::new("url".into(), HeaderMap::new());
|
|
||||||
assert_eq!(manager.get_proxies(), serde_json::Value::Null);
|
|
||||||
assert_eq!(manager.get_providers_proxies(), serde_json::Value::Null);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_refresh_proxies() {
|
|
||||||
let manager = mihomo_api::MihomoManager::new("http://127.0.0.1:9097".into(), HeaderMap::new());
|
|
||||||
let manager = manager.refresh_proxies().await.unwrap();
|
|
||||||
let proxies = manager.get_proxies();
|
|
||||||
let providers = manager.get_providers_proxies();
|
|
||||||
assert_ne!(proxies, serde_json::Value::Null);
|
|
||||||
assert_eq!(providers, serde_json::Value::Null);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_refresh_providers_proxies() {
|
|
||||||
let manager = mihomo_api::MihomoManager::new("http://127.0.0.1:9097".into(), HeaderMap::new());
|
|
||||||
let manager = manager.refresh_providers_proxies().await.unwrap();
|
|
||||||
let proxies = manager.get_proxies();
|
|
||||||
let providers = manager.get_providers_proxies();
|
|
||||||
assert_eq!(proxies, serde_json::Value::Null);
|
|
||||||
assert_ne!(providers, serde_json::Value::Null);
|
|
||||||
}
|
|
||||||
@@ -5,17 +5,10 @@ mod script;
|
|||||||
pub mod seq;
|
pub mod seq;
|
||||||
mod tun;
|
mod tun;
|
||||||
|
|
||||||
use self::chain::*;
|
use self::{chain::*, field::*, merge::*, script::*, seq::*, tun::*};
|
||||||
use self::field::*;
|
use crate::{config::Config, utils::tmpl};
|
||||||
use self::merge::*;
|
|
||||||
use self::script::*;
|
|
||||||
use self::seq::*;
|
|
||||||
use self::tun::*;
|
|
||||||
use crate::config::Config;
|
|
||||||
use crate::utils::tmpl;
|
|
||||||
use serde_yaml::Mapping;
|
use serde_yaml::Mapping;
|
||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
type ResultLog = Vec<(String, String)>;
|
type ResultLog = Vec<(String, String)>;
|
||||||
|
|
||||||
@@ -29,7 +22,7 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
|||||||
let verge = Config::verge();
|
let verge = Config::verge();
|
||||||
let verge = verge.latest();
|
let verge = verge.latest();
|
||||||
(
|
(
|
||||||
verge.clash_core.clone(),
|
Some(verge.get_valid_clash_core()),
|
||||||
verge.enable_tun_mode.unwrap_or(false),
|
verge.enable_tun_mode.unwrap_or(false),
|
||||||
verge.enable_builtin_enhanced.unwrap_or(true),
|
verge.enable_builtin_enhanced.unwrap_or(true),
|
||||||
verge.verge_socks_enabled.unwrap_or(false),
|
verge.verge_socks_enabled.unwrap_or(false),
|
||||||
@@ -267,17 +260,30 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
|||||||
if enable_dns_settings {
|
if enable_dns_settings {
|
||||||
use crate::utils::dirs;
|
use crate::utils::dirs;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
// 尝试读取dns_config.yaml
|
|
||||||
if let Ok(app_dir) = dirs::app_home_dir() {
|
if let Ok(app_dir) = dirs::app_home_dir() {
|
||||||
let dns_path = app_dir.join("dns_config.yaml");
|
let dns_path = app_dir.join("dns_config.yaml");
|
||||||
|
|
||||||
if dns_path.exists() {
|
if dns_path.exists() {
|
||||||
if let Ok(dns_yaml) = fs::read_to_string(&dns_path) {
|
if let Ok(dns_yaml) = fs::read_to_string(&dns_path) {
|
||||||
if let Ok(dns_config) = serde_yaml::from_str::<serde_yaml::Mapping>(&dns_yaml) {
|
if let Ok(dns_config) = serde_yaml::from_str::<serde_yaml::Mapping>(&dns_yaml) {
|
||||||
// 将DNS配置合并到最终配置中
|
// 处理hosts配置
|
||||||
config.insert("dns".into(), dns_config.into());
|
if let Some(hosts_value) = dns_config.get("hosts") {
|
||||||
log::info!(target: "app", "apply dns_config.yaml");
|
if hosts_value.is_mapping() {
|
||||||
|
config.insert("hosts".into(), hosts_value.clone());
|
||||||
|
log::info!(target: "app", "apply hosts configuration");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(dns_value) = dns_config.get("dns") {
|
||||||
|
if let Some(dns_mapping) = dns_value.as_mapping() {
|
||||||
|
config.insert("dns".into(), dns_mapping.clone().into());
|
||||||
|
log::info!(target: "app", "apply dns_config.yaml (dns section)");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
config.insert("dns".into(), dns_config.into());
|
||||||
|
log::info!(target: "app", "apply dns_config.yaml");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,37 +33,40 @@ pub fn use_script(
|
|||||||
}
|
}
|
||||||
let _ = context.eval(Source::from_bytes(
|
let _ = context.eval(Source::from_bytes(
|
||||||
r#"var console = Object.freeze({
|
r#"var console = Object.freeze({
|
||||||
log(data){__verge_log__("log",JSON.stringify(data))},
|
log(data){__verge_log__("log",JSON.stringify(data, null, 2))},
|
||||||
info(data){__verge_log__("info",JSON.stringify(data))},
|
info(data){__verge_log__("info",JSON.stringify(data, null, 2))},
|
||||||
error(data){__verge_log__("error",JSON.stringify(data))},
|
error(data){__verge_log__("error",JSON.stringify(data, null, 2))},
|
||||||
debug(data){__verge_log__("debug",JSON.stringify(data))},
|
debug(data){__verge_log__("debug",JSON.stringify(data, null, 2))},
|
||||||
|
warn(data){__verge_log__("warn",JSON.stringify(data, null, 2))},
|
||||||
|
table(data){__verge_log__("table",JSON.stringify(data, null, 2))},
|
||||||
});"#,
|
});"#,
|
||||||
));
|
));
|
||||||
|
|
||||||
let config = use_lowercase(config.clone());
|
let config = use_lowercase(config.clone());
|
||||||
let config_str = serde_json::to_string(&config)?;
|
let config_str = serde_json::to_string(&config)?;
|
||||||
|
|
||||||
|
// 仅处理 name 参数中的特殊字符
|
||||||
|
let safe_name = escape_js_string_for_single_quote(&name);
|
||||||
|
|
||||||
let code = format!(
|
let code = format!(
|
||||||
r#"try{{
|
r#"try{{
|
||||||
{script};
|
{script};
|
||||||
JSON.stringify(main({config_str},'{name}')||'')
|
JSON.stringify(main({config_str},'{safe_name}')||'')
|
||||||
}} catch(err) {{
|
}} catch(err) {{
|
||||||
`__error_flag__ ${{err.toString()}}`
|
`__error_flag__ ${{err.toString()}}`
|
||||||
}}"#
|
}}"#
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Ok(result) = context.eval(Source::from_bytes(code.as_str())) {
|
if let Ok(result) = context.eval(Source::from_bytes(code.as_str())) {
|
||||||
if !result.is_string() {
|
if !result.is_string() {
|
||||||
anyhow::bail!("main function should return object");
|
anyhow::bail!("main function should return object");
|
||||||
}
|
}
|
||||||
let result = result.to_string(&mut context).unwrap();
|
let result = result.to_string(&mut context).unwrap();
|
||||||
let result = result.to_std_string().unwrap();
|
let result = result.to_std_string().unwrap();
|
||||||
if result.starts_with("__error_flag__") {
|
|
||||||
anyhow::bail!(result[15..].to_owned());
|
// 直接解析JSON结果,不做其他解析
|
||||||
}
|
let res: Result<Mapping, Error> = parse_json_safely(&result);
|
||||||
if result == "\"\"" {
|
|
||||||
anyhow::bail!("main function should return object");
|
|
||||||
}
|
|
||||||
let res: Result<Mapping, Error> = Ok(serde_json::from_str::<Mapping>(result.as_str())?);
|
|
||||||
let mut out = outputs.lock().unwrap();
|
let mut out = outputs.lock().unwrap();
|
||||||
match res {
|
match res {
|
||||||
Ok(config) => Ok((use_lowercase(config), out.to_vec())),
|
Ok(config) => Ok((use_lowercase(config), out.to_vec())),
|
||||||
@@ -77,6 +80,27 @@ pub fn use_script(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_json_safely(json_str: &str) -> Result<Mapping, Error> {
|
||||||
|
let json_str = strip_outer_quotes(json_str);
|
||||||
|
|
||||||
|
Ok(serde_json::from_str::<Mapping>(json_str)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除字符串外层的引号
|
||||||
|
fn strip_outer_quotes(s: &str) -> &str {
|
||||||
|
let s = s.trim();
|
||||||
|
if (s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')) {
|
||||||
|
&s[1..s.len() - 1]
|
||||||
|
} else {
|
||||||
|
s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转义单引号和反斜杠,用于单引号包裹的JavaScript字符串
|
||||||
|
fn escape_js_string_for_single_quote(s: &str) -> String {
|
||||||
|
s.replace('\\', "\\\\").replace('\'', "\\'")
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_script() {
|
fn test_script() {
|
||||||
let script = r#"
|
let script = r#"
|
||||||
@@ -104,6 +128,31 @@ fn test_script() {
|
|||||||
let (config, results) = use_script(script.into(), config, "".to_string()).unwrap();
|
let (config, results) = use_script(script.into(), config, "".to_string()).unwrap();
|
||||||
|
|
||||||
let _ = serde_yaml::to_string(&config).unwrap();
|
let _ = serde_yaml::to_string(&config).unwrap();
|
||||||
|
let yaml_config_size = std::mem::size_of_val(&config);
|
||||||
|
dbg!(yaml_config_size);
|
||||||
|
let box_yaml_config_size = std::mem::size_of_val(&Box::new(config));
|
||||||
|
dbg!(box_yaml_config_size);
|
||||||
dbg!(results);
|
dbg!(results);
|
||||||
|
assert!(box_yaml_config_size < yaml_config_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试特殊字符转义功能
|
||||||
|
#[test]
|
||||||
|
fn test_escape_unescape() {
|
||||||
|
let test_string = r#"Hello "World"!\nThis is a test with \u00A9 copyright symbol."#;
|
||||||
|
let escaped = escape_js_string_for_single_quote(test_string);
|
||||||
|
println!("Original: {}", test_string);
|
||||||
|
println!("Escaped: {}", escaped);
|
||||||
|
|
||||||
|
let json_str = r#"{"key":"value","nested":{"key":"value"}}"#;
|
||||||
|
let parsed = parse_json_safely(json_str).unwrap();
|
||||||
|
|
||||||
|
assert!(parsed.contains_key("key"));
|
||||||
|
assert!(parsed.contains_key("nested"));
|
||||||
|
|
||||||
|
let quoted_json_str = r#""{"key":"value","nested":{"key":"value"}}""#;
|
||||||
|
let parsed_quoted = parse_json_safely(quoted_json_str).unwrap();
|
||||||
|
|
||||||
|
assert!(parsed_quoted.contains_key("key"));
|
||||||
|
assert!(parsed_quoted.contains_key("nested"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ proxy-groups:
|
|||||||
- "proxy1"
|
- "proxy1"
|
||||||
"#;
|
"#;
|
||||||
let mut config: Mapping = serde_yaml::from_str(config_str).unwrap();
|
let mut config: Mapping = serde_yaml::from_str(config_str).unwrap();
|
||||||
|
|
||||||
let seq = SeqMap {
|
let seq = SeqMap {
|
||||||
prepend: Sequence::new(),
|
prepend: Sequence::new(),
|
||||||
append: Sequence::new(),
|
append: Sequence::new(),
|
||||||
@@ -121,16 +121,32 @@ proxy-groups:
|
|||||||
let proxies = config.get("proxies").unwrap().as_sequence().unwrap();
|
let proxies = config.get("proxies").unwrap().as_sequence().unwrap();
|
||||||
assert_eq!(proxies.len(), 1);
|
assert_eq!(proxies.len(), 1);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
proxies[0].as_mapping().unwrap().get("name").unwrap().as_str().unwrap(),
|
proxies[0]
|
||||||
|
.as_mapping()
|
||||||
|
.unwrap()
|
||||||
|
.get("name")
|
||||||
|
.unwrap()
|
||||||
|
.as_str()
|
||||||
|
.unwrap(),
|
||||||
"proxy2"
|
"proxy2"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check if proxy1 is removed from all groups
|
// Check if proxy1 is removed from all groups
|
||||||
let groups = config.get("proxy-groups").unwrap().as_sequence().unwrap();
|
let groups = config.get("proxy-groups").unwrap().as_sequence().unwrap();
|
||||||
let group1_proxies = groups[0].as_mapping().unwrap()
|
let group1_proxies = groups[0]
|
||||||
.get("proxies").unwrap().as_sequence().unwrap();
|
.as_mapping()
|
||||||
let group2_proxies = groups[1].as_mapping().unwrap()
|
.unwrap()
|
||||||
.get("proxies").unwrap().as_sequence().unwrap();
|
.get("proxies")
|
||||||
|
.unwrap()
|
||||||
|
.as_sequence()
|
||||||
|
.unwrap();
|
||||||
|
let group2_proxies = groups[1]
|
||||||
|
.as_mapping()
|
||||||
|
.unwrap()
|
||||||
|
.get("proxies")
|
||||||
|
.unwrap()
|
||||||
|
.as_sequence()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(group1_proxies.len(), 1);
|
assert_eq!(group1_proxies.len(), 1);
|
||||||
assert_eq!(group1_proxies[0].as_str().unwrap(), "proxy2");
|
assert_eq!(group1_proxies[0].as_str().unwrap(), "proxy2");
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ pub async fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
|
|||||||
let mut tun_val = tun_val.map_or(Mapping::new(), |val| {
|
let mut tun_val = tun_val.map_or(Mapping::new(), |val| {
|
||||||
val.as_mapping().cloned().unwrap_or(Mapping::new())
|
val.as_mapping().cloned().unwrap_or(Mapping::new())
|
||||||
});
|
});
|
||||||
|
|
||||||
if enable {
|
if enable {
|
||||||
// 读取DNS配置
|
// 读取DNS配置
|
||||||
let dns_key = Value::from("dns");
|
let dns_key = Value::from("dns");
|
||||||
@@ -40,20 +40,20 @@ pub async fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
|
|||||||
|
|
||||||
// 检查现有的 enhanced-mode 设置
|
// 检查现有的 enhanced-mode 设置
|
||||||
let current_mode = dns_val
|
let current_mode = dns_val
|
||||||
.get(&Value::from("enhanced-mode"))
|
.get(Value::from("enhanced-mode"))
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.unwrap_or("fake-ip");
|
.unwrap_or("fake-ip");
|
||||||
|
|
||||||
// 只有当 enhanced-mode 是 fake-ip 或未设置时才修改 DNS 配置
|
// 只有当 enhanced-mode 是 fake-ip 或未设置时才修改 DNS 配置
|
||||||
if current_mode == "fake-ip" || !dns_val.contains_key(&Value::from("enhanced-mode")) {
|
if current_mode == "fake-ip" || !dns_val.contains_key(Value::from("enhanced-mode")) {
|
||||||
revise!(dns_val, "enable", true);
|
revise!(dns_val, "enable", true);
|
||||||
revise!(dns_val, "ipv6", ipv6_val);
|
revise!(dns_val, "ipv6", ipv6_val);
|
||||||
|
|
||||||
if !dns_val.contains_key(&Value::from("enhanced-mode")) {
|
if !dns_val.contains_key(Value::from("enhanced-mode")) {
|
||||||
revise!(dns_val, "enhanced-mode", "fake-ip");
|
revise!(dns_val, "enhanced-mode", "fake-ip");
|
||||||
}
|
}
|
||||||
|
|
||||||
if !dns_val.contains_key(&Value::from("fake-ip-range")) {
|
if !dns_val.contains_key(Value::from("fake-ip-range")) {
|
||||||
revise!(dns_val, "fake-ip-range", "198.18.0.1/16");
|
revise!(dns_val, "fake-ip-range", "198.18.0.1/16");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ pub async fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
|
|||||||
crate::utils::resolve::set_public_dns("223.6.6.6".to_string()).await;
|
crate::utils::resolve::set_public_dns("223.6.6.6".to_string()).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 当TUN启用时,将修改后的DNS配置写回
|
// 当TUN启用时,将修改后的DNS配置写回
|
||||||
revise!(config, "dns", dns_val);
|
revise!(config, "dns", dns_val);
|
||||||
} else {
|
} else {
|
||||||
@@ -75,6 +75,6 @@ pub async fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
|
|||||||
// 更新TUN配置
|
// 更新TUN配置
|
||||||
revise!(tun_val, "enable", enable);
|
revise!(tun_val, "enable", enable);
|
||||||
revise!(config, "tun", tun_val);
|
revise!(config, "tun", tun_val);
|
||||||
|
|
||||||
config
|
config
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
use crate::config::{Config, IVerge};
|
use crate::{
|
||||||
use crate::core::backup;
|
config::{Config, IVerge},
|
||||||
use crate::log_err;
|
core::backup,
|
||||||
use crate::utils::dirs::app_home_dir;
|
logging_error,
|
||||||
|
utils::{dirs::app_home_dir, logging::Type},
|
||||||
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use reqwest_dav::list_cmd::ListFile;
|
use reqwest_dav::list_cmd::ListFile;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
@@ -67,8 +69,9 @@ pub async fn restore_webdav_backup(filename: String) -> Result<()> {
|
|||||||
// extract zip file
|
// extract zip file
|
||||||
let mut zip = zip::ZipArchive::new(fs::File::open(backup_storage_path.clone())?)?;
|
let mut zip = zip::ZipArchive::new(fs::File::open(backup_storage_path.clone())?)?;
|
||||||
zip.extract(app_home_dir()?)?;
|
zip.extract(app_home_dir()?)?;
|
||||||
|
logging_error!(
|
||||||
log_err!(
|
Type::Backup,
|
||||||
|
true,
|
||||||
super::patch_verge(
|
super::patch_verge(
|
||||||
IVerge {
|
IVerge {
|
||||||
webdav_url,
|
webdav_url,
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
use crate::config::Config;
|
use crate::{
|
||||||
use crate::core::{handle, tray, CoreManager};
|
config::Config,
|
||||||
use crate::log_err;
|
core::{handle, tray, CoreManager},
|
||||||
use crate::module::mihomo::MihomoManager;
|
logging_error,
|
||||||
use crate::utils::resolve;
|
module::mihomo::MihomoManager,
|
||||||
|
process::AsyncHandler,
|
||||||
|
utils::{logging::Type, resolve},
|
||||||
|
};
|
||||||
use serde_yaml::{Mapping, Value};
|
use serde_yaml::{Mapping, Value};
|
||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
|
|
||||||
/// Restart the Clash core
|
/// Restart the Clash core
|
||||||
pub fn restart_clash_core() {
|
pub fn restart_clash_core() {
|
||||||
tauri::async_runtime::spawn(async {
|
AsyncHandler::spawn(move || async move {
|
||||||
match CoreManager::global().restart_core().await {
|
match CoreManager::global().restart_core().await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
handle::Handle::refresh_clash();
|
handle::Handle::refresh_clash();
|
||||||
@@ -24,17 +27,34 @@ pub fn restart_clash_core() {
|
|||||||
|
|
||||||
/// Restart the application
|
/// Restart the application
|
||||||
pub fn restart_app() {
|
pub fn restart_app() {
|
||||||
tauri::async_runtime::spawn_blocking(|| {
|
AsyncHandler::spawn(move || async move {
|
||||||
tauri::async_runtime::block_on(async {
|
logging_error!(Type::Core, true, CoreManager::global().stop_core().await);
|
||||||
log_err!(CoreManager::global().stop_core().await);
|
resolve::resolve_reset_async().await;
|
||||||
});
|
|
||||||
resolve::resolve_reset();
|
|
||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||||
tauri::process::restart(&app_handle.env());
|
tauri::process::restart(&app_handle.env());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn after_change_clash_mode() {
|
||||||
|
AsyncHandler::spawn(move || async {
|
||||||
|
match MihomoManager::global().get_connections().await {
|
||||||
|
Ok(connections) => {
|
||||||
|
if let Some(connections_array) = connections["connections"].as_array() {
|
||||||
|
for connection in connections_array {
|
||||||
|
if let Some(id) = connection["id"].as_str() {
|
||||||
|
let _ = MihomoManager::global().delete_connection(id).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log::error!(target: "app", "Failed to get connections: {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Change Clash mode (rule/global/direct/script)
|
/// Change Clash mode (rule/global/direct/script)
|
||||||
pub fn change_clash_mode(mode: String) {
|
pub fn change_clash_mode(mode: String) {
|
||||||
let mut mapping = Mapping::new();
|
let mut mapping = Mapping::new();
|
||||||
@@ -43,9 +63,8 @@ pub fn change_clash_mode(mode: String) {
|
|||||||
let json_value = serde_json::json!({
|
let json_value = serde_json::json!({
|
||||||
"mode": mode
|
"mode": mode
|
||||||
});
|
});
|
||||||
tauri::async_runtime::spawn(async move {
|
AsyncHandler::spawn(move || async move {
|
||||||
log::debug!(target: "app", "change clash mode to {mode}");
|
log::debug!(target: "app", "change clash mode to {mode}");
|
||||||
|
|
||||||
match MihomoManager::global().patch_configs(json_value).await {
|
match MihomoManager::global().patch_configs(json_value).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// 更新订阅
|
// 更新订阅
|
||||||
@@ -53,8 +72,16 @@ pub fn change_clash_mode(mode: String) {
|
|||||||
|
|
||||||
if Config::clash().data().save_config().is_ok() {
|
if Config::clash().data().save_config().is_ok() {
|
||||||
handle::Handle::refresh_clash();
|
handle::Handle::refresh_clash();
|
||||||
log_err!(tray::Tray::global().update_menu());
|
logging_error!(Type::Tray, true, tray::Tray::global().update_menu());
|
||||||
log_err!(tray::Tray::global().update_icon(None));
|
logging_error!(Type::Tray, true, tray::Tray::global().update_icon(None));
|
||||||
|
}
|
||||||
|
|
||||||
|
let is_auto_close_connection = Config::verge()
|
||||||
|
.data()
|
||||||
|
.auto_close_connection
|
||||||
|
.unwrap_or(false);
|
||||||
|
if is_auto_close_connection {
|
||||||
|
after_change_clash_mode();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => log::error!(target: "app", "{err}"),
|
Err(err) => log::error!(target: "app", "{err}"),
|
||||||
@@ -64,36 +91,26 @@ pub fn change_clash_mode(mode: String) {
|
|||||||
|
|
||||||
/// Test connection delay to a URL
|
/// Test connection delay to a URL
|
||||||
pub async fn test_delay(url: String) -> anyhow::Result<u32> {
|
pub async fn test_delay(url: String) -> anyhow::Result<u32> {
|
||||||
use tokio::time::{Duration, Instant};
|
use crate::utils::network::{NetworkManager, ProxyType};
|
||||||
let mut builder = reqwest::ClientBuilder::new().use_rustls_tls().no_proxy();
|
use tokio::time::Instant;
|
||||||
|
|
||||||
let port = Config::verge()
|
|
||||||
.latest()
|
|
||||||
.verge_mixed_port
|
|
||||||
.unwrap_or(Config::clash().data().get_mixed_port());
|
|
||||||
let tun_mode = Config::verge().latest().enable_tun_mode.unwrap_or(false);
|
let tun_mode = Config::verge().latest().enable_tun_mode.unwrap_or(false);
|
||||||
|
|
||||||
let proxy_scheme = format!("http://127.0.0.1:{port}");
|
// 如果是TUN模式,不使用代理,否则使用自身代理
|
||||||
|
let proxy_type = if !tun_mode {
|
||||||
|
ProxyType::Localhost
|
||||||
|
} else {
|
||||||
|
ProxyType::None
|
||||||
|
};
|
||||||
|
|
||||||
if !tun_mode {
|
let user_agent = Some("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0".to_string());
|
||||||
if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) {
|
|
||||||
builder = builder.proxy(proxy);
|
|
||||||
}
|
|
||||||
if let Ok(proxy) = reqwest::Proxy::https(&proxy_scheme) {
|
|
||||||
builder = builder.proxy(proxy);
|
|
||||||
}
|
|
||||||
if let Ok(proxy) = reqwest::Proxy::all(&proxy_scheme) {
|
|
||||||
builder = builder.proxy(proxy);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let request = builder
|
|
||||||
.timeout(Duration::from_millis(10000))
|
|
||||||
.build()?
|
|
||||||
.get(url).header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0");
|
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
|
|
||||||
let response = request.send().await;
|
let response = NetworkManager::global()
|
||||||
|
.get_with_interrupt(&url, proxy_type, Some(10), user_agent, false)
|
||||||
|
.await;
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
log::trace!(target: "app", "test_delay response: {:#?}", response);
|
log::trace!(target: "app", "test_delay response: {:#?}", response);
|
||||||
@@ -105,7 +122,7 @@ pub async fn test_delay(url: String) -> anyhow::Result<u32> {
|
|||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::trace!(target: "app", "test_delay error: {:#?}", err);
|
log::trace!(target: "app", "test_delay error: {:#?}", err);
|
||||||
Err(err.into())
|
Err(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
use crate::config::{Config, IVerge};
|
use crate::{
|
||||||
use crate::core::{handle, hotkey, sysopt, tray, CoreManager};
|
config::{Config, IVerge},
|
||||||
use crate::log_err;
|
core::{handle, hotkey, sysopt, tray, CoreManager},
|
||||||
use crate::utils::resolve;
|
logging_error,
|
||||||
|
module::lightweight,
|
||||||
|
utils::logging::Type,
|
||||||
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use serde_yaml::Mapping;
|
use serde_yaml::Mapping;
|
||||||
use tauri::Manager;
|
|
||||||
|
|
||||||
/// Patch Clash configuration
|
/// Patch Clash configuration
|
||||||
pub async fn patch_clash(patch: Mapping) -> Result<()> {
|
pub async fn patch_clash(patch: Mapping) -> Result<()> {
|
||||||
@@ -17,8 +19,8 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
|
|||||||
CoreManager::global().restart_core().await?;
|
CoreManager::global().restart_core().await?;
|
||||||
} else {
|
} else {
|
||||||
if patch.get("mode").is_some() {
|
if patch.get("mode").is_some() {
|
||||||
log_err!(tray::Tray::global().update_menu());
|
logging_error!(Type::Tray, true, tray::Tray::global().update_menu());
|
||||||
log_err!(tray::Tray::global().update_icon(None));
|
logging_error!(Type::Tray, true, tray::Tray::global().update_icon(None));
|
||||||
}
|
}
|
||||||
Config::runtime().latest().patch_config(patch);
|
Config::runtime().latest().patch_config(patch);
|
||||||
CoreManager::global().update_config().await?;
|
CoreManager::global().update_config().await?;
|
||||||
@@ -39,6 +41,23 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Define update flags as bitflags for better performance
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
enum UpdateFlags {
|
||||||
|
None = 0,
|
||||||
|
RestartCore = 1 << 0,
|
||||||
|
ClashConfig = 1 << 1,
|
||||||
|
VergeConfig = 1 << 2,
|
||||||
|
Launch = 1 << 3,
|
||||||
|
SysProxy = 1 << 4,
|
||||||
|
SystrayIcon = 1 << 5,
|
||||||
|
Hotkey = 1 << 6,
|
||||||
|
SystrayMenu = 1 << 7,
|
||||||
|
SystrayTooltip = 1 << 8,
|
||||||
|
SystrayClickBehavior = 1 << 9,
|
||||||
|
LighteWeight = 1 << 10,
|
||||||
|
}
|
||||||
|
|
||||||
/// Patch Verge configuration
|
/// Patch Verge configuration
|
||||||
pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
|
pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
|
||||||
Config::verge().draft().patch_config(patch.clone());
|
Config::verge().draft().patch_config(patch.clone());
|
||||||
@@ -51,7 +70,6 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
|
|||||||
let proxy_bypass = patch.system_proxy_bypass;
|
let proxy_bypass = patch.system_proxy_bypass;
|
||||||
let language = patch.language;
|
let language = patch.language;
|
||||||
let mixed_port = patch.verge_mixed_port;
|
let mixed_port = patch.verge_mixed_port;
|
||||||
let lite_mode = patch.enable_lite_mode;
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
let tray_icon = patch.tray_icon;
|
let tray_icon = patch.tray_icon;
|
||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
@@ -72,35 +90,31 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
|
|||||||
let http_enabled = patch.verge_http_enabled;
|
let http_enabled = patch.verge_http_enabled;
|
||||||
let http_port = patch.verge_port;
|
let http_port = patch.verge_port;
|
||||||
let enable_tray_speed = patch.enable_tray_speed;
|
let enable_tray_speed = patch.enable_tray_speed;
|
||||||
|
let enable_tray_icon = patch.enable_tray_icon;
|
||||||
let enable_global_hotkey = patch.enable_global_hotkey;
|
let enable_global_hotkey = patch.enable_global_hotkey;
|
||||||
|
let tray_event = patch.tray_event;
|
||||||
|
let home_cards = patch.home_cards.clone();
|
||||||
|
let enable_auto_light_weight = patch.enable_auto_light_weight_mode;
|
||||||
let res: std::result::Result<(), anyhow::Error> = {
|
let res: std::result::Result<(), anyhow::Error> = {
|
||||||
let mut should_restart_core = false;
|
// Initialize with no flags set
|
||||||
let mut should_update_clash_config = false;
|
let mut update_flags: i32 = UpdateFlags::None as i32;
|
||||||
let mut should_update_verge_config = false;
|
|
||||||
let mut should_update_launch = false;
|
|
||||||
let mut should_update_sysproxy = false;
|
|
||||||
let mut should_update_systray_icon = false;
|
|
||||||
let mut should_update_hotkey = false;
|
|
||||||
let mut should_update_systray_menu = false;
|
|
||||||
let mut should_update_systray_tooltip = false;
|
|
||||||
|
|
||||||
if tun_mode.is_some() {
|
if tun_mode.is_some() {
|
||||||
should_update_clash_config = true;
|
update_flags |= UpdateFlags::ClashConfig as i32;
|
||||||
should_update_systray_menu = true;
|
update_flags |= UpdateFlags::SystrayMenu as i32;
|
||||||
should_update_systray_tooltip = true;
|
update_flags |= UpdateFlags::SystrayTooltip as i32;
|
||||||
should_update_systray_icon = true;
|
update_flags |= UpdateFlags::SystrayIcon as i32;
|
||||||
}
|
}
|
||||||
if enable_global_hotkey.is_some() {
|
if enable_global_hotkey.is_some() || home_cards.is_some() {
|
||||||
should_update_verge_config = true;
|
update_flags |= UpdateFlags::VergeConfig as i32;
|
||||||
}
|
}
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
if redir_enabled.is_some() || redir_port.is_some() {
|
if redir_enabled.is_some() || redir_port.is_some() {
|
||||||
should_restart_core = true;
|
update_flags |= UpdateFlags::RestartCore as i32;
|
||||||
}
|
}
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
if tproxy_enabled.is_some() || tproxy_port.is_some() {
|
if tproxy_enabled.is_some() || tproxy_port.is_some() {
|
||||||
should_restart_core = true;
|
update_flags |= UpdateFlags::RestartCore as i32;
|
||||||
}
|
}
|
||||||
if socks_enabled.is_some()
|
if socks_enabled.is_some()
|
||||||
|| http_enabled.is_some()
|
|| http_enabled.is_some()
|
||||||
@@ -108,91 +122,88 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
|
|||||||
|| http_port.is_some()
|
|| http_port.is_some()
|
||||||
|| mixed_port.is_some()
|
|| mixed_port.is_some()
|
||||||
{
|
{
|
||||||
should_restart_core = true;
|
update_flags |= UpdateFlags::RestartCore as i32;
|
||||||
}
|
}
|
||||||
if auto_launch.is_some() {
|
if auto_launch.is_some() {
|
||||||
should_update_launch = true;
|
update_flags |= UpdateFlags::Launch as i32;
|
||||||
}
|
}
|
||||||
|
|
||||||
if system_proxy.is_some() {
|
if system_proxy.is_some() {
|
||||||
should_update_sysproxy = true;
|
update_flags |= UpdateFlags::SysProxy as i32;
|
||||||
should_update_systray_menu = true;
|
update_flags |= UpdateFlags::SystrayMenu as i32;
|
||||||
should_update_systray_tooltip = true;
|
update_flags |= UpdateFlags::SystrayTooltip as i32;
|
||||||
should_update_systray_icon = true;
|
update_flags |= UpdateFlags::SystrayIcon as i32;
|
||||||
}
|
}
|
||||||
|
|
||||||
if proxy_bypass.is_some() || pac_content.is_some() || pac.is_some() {
|
if proxy_bypass.is_some() || pac_content.is_some() || pac.is_some() {
|
||||||
should_update_sysproxy = true;
|
update_flags |= UpdateFlags::SysProxy as i32;
|
||||||
}
|
}
|
||||||
|
|
||||||
if language.is_some() {
|
if language.is_some() {
|
||||||
should_update_systray_menu = true;
|
update_flags |= UpdateFlags::SystrayMenu as i32;
|
||||||
}
|
}
|
||||||
if common_tray_icon.is_some()
|
if common_tray_icon.is_some()
|
||||||
|| sysproxy_tray_icon.is_some()
|
|| sysproxy_tray_icon.is_some()
|
||||||
|| tun_tray_icon.is_some()
|
|| tun_tray_icon.is_some()
|
||||||
|| tray_icon.is_some()
|
|| tray_icon.is_some()
|
||||||
|
|| enable_tray_speed.is_some()
|
||||||
|
|| enable_tray_icon.is_some()
|
||||||
{
|
{
|
||||||
should_update_systray_icon = true;
|
update_flags |= UpdateFlags::SystrayIcon as i32;
|
||||||
}
|
}
|
||||||
|
|
||||||
if patch.hotkeys.is_some() {
|
if patch.hotkeys.is_some() {
|
||||||
should_update_hotkey = true;
|
update_flags |= UpdateFlags::Hotkey as i32;
|
||||||
should_update_systray_menu = true;
|
update_flags |= UpdateFlags::SystrayMenu as i32;
|
||||||
}
|
}
|
||||||
|
|
||||||
if enable_tray_speed.is_some() {
|
if tray_event.is_some() {
|
||||||
should_update_systray_icon = true;
|
update_flags |= UpdateFlags::SystrayClickBehavior as i32;
|
||||||
}
|
}
|
||||||
|
|
||||||
if should_restart_core {
|
if enable_auto_light_weight.is_some() {
|
||||||
|
update_flags |= UpdateFlags::LighteWeight as i32;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process updates based on flags
|
||||||
|
if (update_flags & (UpdateFlags::RestartCore as i32)) != 0 {
|
||||||
|
Config::generate().await?;
|
||||||
CoreManager::global().restart_core().await?;
|
CoreManager::global().restart_core().await?;
|
||||||
}
|
}
|
||||||
if should_update_clash_config {
|
if (update_flags & (UpdateFlags::ClashConfig as i32)) != 0 {
|
||||||
CoreManager::global().update_config().await?;
|
CoreManager::global().update_config().await?;
|
||||||
handle::Handle::refresh_clash();
|
handle::Handle::refresh_clash();
|
||||||
}
|
}
|
||||||
if should_update_verge_config {
|
if (update_flags & (UpdateFlags::VergeConfig as i32)) != 0 {
|
||||||
Config::verge().draft().enable_global_hotkey = enable_global_hotkey;
|
Config::verge().draft().enable_global_hotkey = enable_global_hotkey;
|
||||||
handle::Handle::refresh_verge();
|
handle::Handle::refresh_verge();
|
||||||
}
|
}
|
||||||
if should_update_launch {
|
if (update_flags & (UpdateFlags::Launch as i32)) != 0 {
|
||||||
sysopt::Sysopt::global().update_launch()?;
|
sysopt::Sysopt::global().update_launch()?;
|
||||||
}
|
}
|
||||||
|
if (update_flags & (UpdateFlags::SysProxy as i32)) != 0 {
|
||||||
if should_update_sysproxy {
|
|
||||||
sysopt::Sysopt::global().update_sysproxy().await?;
|
sysopt::Sysopt::global().update_sysproxy().await?;
|
||||||
}
|
}
|
||||||
|
if (update_flags & (UpdateFlags::Hotkey as i32)) != 0 {
|
||||||
if should_update_hotkey {
|
|
||||||
hotkey::Hotkey::global().update(patch.hotkeys.unwrap())?;
|
hotkey::Hotkey::global().update(patch.hotkeys.unwrap())?;
|
||||||
}
|
}
|
||||||
|
if (update_flags & (UpdateFlags::SystrayMenu as i32)) != 0 {
|
||||||
if should_update_systray_menu {
|
|
||||||
tray::Tray::global().update_menu()?;
|
tray::Tray::global().update_menu()?;
|
||||||
}
|
}
|
||||||
|
if (update_flags & (UpdateFlags::SystrayIcon as i32)) != 0 {
|
||||||
if should_update_systray_icon {
|
|
||||||
tray::Tray::global().update_icon(None)?;
|
tray::Tray::global().update_icon(None)?;
|
||||||
}
|
}
|
||||||
|
if (update_flags & (UpdateFlags::SystrayTooltip as i32)) != 0 {
|
||||||
if should_update_systray_tooltip {
|
|
||||||
tray::Tray::global().update_tooltip()?;
|
tray::Tray::global().update_tooltip()?;
|
||||||
}
|
}
|
||||||
|
if (update_flags & (UpdateFlags::SystrayClickBehavior as i32)) != 0 {
|
||||||
// 处理轻量模式切换
|
tray::Tray::global().update_click_behavior()?;
|
||||||
if lite_mode.is_some() {
|
}
|
||||||
if let Some(window) = handle::Handle::global().get_window() {
|
if (update_flags & (UpdateFlags::LighteWeight as i32)) != 0 {
|
||||||
if lite_mode.unwrap() {
|
if enable_auto_light_weight.unwrap() {
|
||||||
// 完全退出 webview 进程
|
lightweight::enable_auto_light_weight_mode();
|
||||||
window.close()?; // 先关闭窗口
|
} else {
|
||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
lightweight::disable_auto_light_weight_mode();
|
||||||
if let Some(webview) = app_handle.get_webview_window("main") {
|
|
||||||
webview.destroy()?; // 销毁 webview 进程
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
resolve::create_window(); // 重新创建窗口
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
use crate::cmd;
|
use crate::{
|
||||||
use crate::config::{Config, PrfItem, PrfOption};
|
cmd,
|
||||||
use crate::core::handle;
|
config::{Config, PrfItem, PrfOption},
|
||||||
use crate::core::CoreManager;
|
core::{handle, CoreManager, *},
|
||||||
use crate::core::*;
|
logging,
|
||||||
|
process::AsyncHandler,
|
||||||
|
utils::logging::Type,
|
||||||
|
};
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
|
|
||||||
/// Toggle proxy profile
|
/// Toggle proxy profile
|
||||||
pub fn toggle_proxy_profile(profile_index: String) {
|
pub fn toggle_proxy_profile(profile_index: String) {
|
||||||
tauri::async_runtime::spawn(async move {
|
AsyncHandler::spawn(|| async move {
|
||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||||
match cmd::patch_profiles_config_by_profile_index(app_handle, profile_index).await {
|
match cmd::patch_profiles_config_by_profile_index(app_handle, profile_index).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
@@ -22,23 +25,29 @@ pub fn toggle_proxy_profile(profile_index: String) {
|
|||||||
|
|
||||||
/// Update a profile
|
/// Update a profile
|
||||||
/// If updating current profile, activate it
|
/// If updating current profile, activate it
|
||||||
pub async fn update_profile(uid: String, option: Option<PrfOption>) -> Result<()> {
|
/// auto_refresh: 是否自动更新配置和刷新前端
|
||||||
println!("[订阅更新] 开始更新订阅 {}", uid);
|
pub async fn update_profile(
|
||||||
|
uid: String,
|
||||||
|
option: Option<PrfOption>,
|
||||||
|
auto_refresh: Option<bool>,
|
||||||
|
) -> Result<()> {
|
||||||
|
logging!(info, Type::Config, true, "[订阅更新] 开始更新订阅 {}", uid);
|
||||||
|
let auto_refresh = auto_refresh.unwrap_or(true); // 默认为true,保持兼容性
|
||||||
|
|
||||||
let url_opt = {
|
let url_opt = {
|
||||||
let profiles = Config::profiles();
|
let profiles = Config::profiles();
|
||||||
let profiles = profiles.latest();
|
let profiles = profiles.latest();
|
||||||
let item = profiles.get_item(&uid)?;
|
let item = profiles.get_item(&uid)?;
|
||||||
let is_remote = item.itype.as_ref().map_or(false, |s| s == "remote");
|
let is_remote = item.itype.as_ref().is_some_and(|s| s == "remote");
|
||||||
|
|
||||||
if !is_remote {
|
if !is_remote {
|
||||||
println!("[订阅更新] {} 不是远程订阅,跳过更新", uid);
|
log::info!(target: "app", "[订阅更新] {} 不是远程订阅,跳过更新", uid);
|
||||||
None // 非远程订阅直接更新
|
None // 非远程订阅直接更新
|
||||||
} else if item.url.is_none() {
|
} else if item.url.is_none() {
|
||||||
println!("[订阅更新] {} 缺少URL,无法更新", uid);
|
log::warn!(target: "app", "[订阅更新] {} 缺少URL,无法更新", uid);
|
||||||
bail!("failed to get the profile item url");
|
bail!("failed to get the profile item url");
|
||||||
} else {
|
} else {
|
||||||
println!(
|
log::info!(target: "app",
|
||||||
"[订阅更新] {} 是远程订阅,URL: {}",
|
"[订阅更新] {} 是远程订阅,URL: {}",
|
||||||
uid,
|
uid,
|
||||||
item.url.clone().unwrap()
|
item.url.clone().unwrap()
|
||||||
@@ -49,32 +58,88 @@ pub async fn update_profile(uid: String, option: Option<PrfOption>) -> Result<()
|
|||||||
|
|
||||||
let should_update = match url_opt {
|
let should_update = match url_opt {
|
||||||
Some((url, opt)) => {
|
Some((url, opt)) => {
|
||||||
println!("[订阅更新] 开始下载新的订阅内容");
|
log::info!(target: "app", "[订阅更新] 开始下载新的订阅内容");
|
||||||
let merged_opt = PrfOption::merge(opt, option);
|
let merged_opt = PrfOption::merge(opt.clone(), option.clone());
|
||||||
let item = PrfItem::from_url(&url, None, None, merged_opt).await?;
|
|
||||||
|
|
||||||
println!("[订阅更新] 更新订阅配置");
|
// 尝试使用正常设置更新
|
||||||
let profiles = Config::profiles();
|
match PrfItem::from_url(&url, None, None, merged_opt.clone()).await {
|
||||||
let mut profiles = profiles.latest();
|
Ok(item) => {
|
||||||
profiles.update_item(uid.clone(), item)?;
|
log::info!(target: "app", "[订阅更新] 更新订阅配置成功");
|
||||||
|
let profiles = Config::profiles();
|
||||||
|
let mut profiles = profiles.latest();
|
||||||
|
profiles.update_item(uid.clone(), item)?;
|
||||||
|
|
||||||
let is_current = Some(uid.clone()) == profiles.get_current();
|
let is_current = Some(uid.clone()) == profiles.get_current();
|
||||||
println!("[订阅更新] 是否为当前使用的订阅: {}", is_current);
|
log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {}", is_current);
|
||||||
is_current
|
is_current && auto_refresh
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
// 首次更新失败,尝试使用Clash代理
|
||||||
|
log::warn!(target: "app", "[订阅更新] 正常更新失败: {},尝试使用Clash代理更新", err);
|
||||||
|
|
||||||
|
// 发送通知
|
||||||
|
handle::Handle::notice_message("update_retry_with_clash", uid.clone());
|
||||||
|
|
||||||
|
// 保存原始代理设置
|
||||||
|
let original_with_proxy = merged_opt.as_ref().and_then(|o| o.with_proxy);
|
||||||
|
let original_self_proxy = merged_opt.as_ref().and_then(|o| o.self_proxy);
|
||||||
|
|
||||||
|
// 创建使用Clash代理的选项
|
||||||
|
let mut fallback_opt = merged_opt.unwrap_or_default();
|
||||||
|
fallback_opt.with_proxy = Some(false);
|
||||||
|
fallback_opt.self_proxy = Some(true);
|
||||||
|
|
||||||
|
// 使用Clash代理重试
|
||||||
|
match PrfItem::from_url(&url, None, None, Some(fallback_opt)).await {
|
||||||
|
Ok(mut item) => {
|
||||||
|
log::info!(target: "app", "[订阅更新] 使用Clash代理更新成功");
|
||||||
|
|
||||||
|
// 恢复原始代理设置到item
|
||||||
|
if let Some(option) = item.option.as_mut() {
|
||||||
|
option.with_proxy = original_with_proxy;
|
||||||
|
option.self_proxy = original_self_proxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新到配置
|
||||||
|
let profiles = Config::profiles();
|
||||||
|
let mut profiles = profiles.latest();
|
||||||
|
profiles.update_item(uid.clone(), item.clone())?;
|
||||||
|
|
||||||
|
// 获取配置名称用于通知
|
||||||
|
let profile_name = item.name.clone().unwrap_or_else(|| uid.clone());
|
||||||
|
|
||||||
|
// 发送通知告知用户自动更新使用了回退机制
|
||||||
|
handle::Handle::notice_message("update_with_clash_proxy", profile_name);
|
||||||
|
|
||||||
|
let is_current = Some(uid.clone()) == profiles.get_current();
|
||||||
|
log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {}", is_current);
|
||||||
|
is_current && auto_refresh
|
||||||
|
}
|
||||||
|
Err(retry_err) => {
|
||||||
|
log::error!(target: "app", "[订阅更新] 使用Clash代理更新仍然失败: {}", retry_err);
|
||||||
|
handle::Handle::notice_message(
|
||||||
|
"update_failed_even_with_clash",
|
||||||
|
format!("{}", retry_err),
|
||||||
|
);
|
||||||
|
return Err(retry_err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
None => true,
|
None => auto_refresh,
|
||||||
};
|
};
|
||||||
|
|
||||||
if should_update {
|
if should_update {
|
||||||
println!("[订阅更新] 更新内核配置");
|
logging!(info, Type::Config, true, "[订阅更新] 更新内核配置");
|
||||||
match CoreManager::global().update_config().await {
|
match CoreManager::global().update_config().await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
println!("[订阅更新] 更新成功");
|
logging!(info, Type::Config, true, "[订阅更新] 更新成功");
|
||||||
handle::Handle::refresh_clash();
|
handle::Handle::refresh_clash();
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
println!("[订阅更新] 更新失败: {}", err);
|
logging!(error, Type::Config, true, "[订阅更新] 更新失败: {}", err);
|
||||||
handle::Handle::notice_message("set_config::error", format!("{err}"));
|
handle::Handle::notice_message("update_failed", format!("{err}"));
|
||||||
log::error!(target: "app", "{err}");
|
log::error!(target: "app", "{err}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,3 +147,11 @@ pub async fn update_profile(uid: String, option: Option<PrfOption>) -> Result<()
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 增强配置
|
||||||
|
pub async fn enhance_profiles() -> Result<()> {
|
||||||
|
crate::core::CoreManager::global()
|
||||||
|
.update_config()
|
||||||
|
.await
|
||||||
|
.map(|_| ())
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
use crate::config::Config;
|
use crate::{
|
||||||
use crate::config::IVerge;
|
config::{Config, IVerge},
|
||||||
use crate::core::handle;
|
core::handle,
|
||||||
|
process::AsyncHandler,
|
||||||
|
};
|
||||||
use std::env;
|
use std::env;
|
||||||
use tauri_plugin_clipboard_manager::ClipboardExt;
|
use tauri_plugin_clipboard_manager::ClipboardExt;
|
||||||
|
|
||||||
@@ -8,8 +10,22 @@ use tauri_plugin_clipboard_manager::ClipboardExt;
|
|||||||
pub fn toggle_system_proxy() {
|
pub fn toggle_system_proxy() {
|
||||||
let enable = Config::verge().draft().enable_system_proxy;
|
let enable = Config::verge().draft().enable_system_proxy;
|
||||||
let enable = enable.unwrap_or(false);
|
let enable = enable.unwrap_or(false);
|
||||||
|
let auto_close_connection = Config::verge()
|
||||||
|
.data()
|
||||||
|
.auto_close_connection
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
AsyncHandler::spawn(move || async move {
|
||||||
|
// 如果当前系统代理即将关闭,且自动关闭连接设置为true,则关闭所有连接
|
||||||
|
if enable && auto_close_connection {
|
||||||
|
if let Err(err) = crate::module::mihomo::MihomoManager::global()
|
||||||
|
.close_all_connections()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
log::error!(target: "app", "Failed to close all connections: {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tauri::async_runtime::spawn(async move {
|
|
||||||
match super::patch_verge(
|
match super::patch_verge(
|
||||||
IVerge {
|
IVerge {
|
||||||
enable_system_proxy: Some(!enable),
|
enable_system_proxy: Some(!enable),
|
||||||
@@ -30,7 +46,7 @@ pub fn toggle_tun_mode(not_save_file: Option<bool>) {
|
|||||||
let enable = Config::verge().data().enable_tun_mode;
|
let enable = Config::verge().data().enable_tun_mode;
|
||||||
let enable = enable.unwrap_or(false);
|
let enable = enable.unwrap_or(false);
|
||||||
|
|
||||||
tauri::async_runtime::spawn(async move {
|
AsyncHandler::spawn(async move || {
|
||||||
match super::patch_verge(
|
match super::patch_verge(
|
||||||
IVerge {
|
IVerge {
|
||||||
enable_tun_mode: Some(!enable),
|
enable_tun_mode: Some(!enable),
|
||||||
@@ -48,9 +64,14 @@ pub fn toggle_tun_mode(not_save_file: Option<bool>) {
|
|||||||
|
|
||||||
/// Copy proxy environment variables to clipboard
|
/// Copy proxy environment variables to clipboard
|
||||||
pub fn copy_clash_env() {
|
pub fn copy_clash_env() {
|
||||||
// 从环境变量获取IP地址,默认127.0.0.1
|
// 从环境变量获取IP地址,如果没有则从配置中获取 proxy_host,默认为 127.0.0.1
|
||||||
let clash_verge_rev_ip =
|
let clash_verge_rev_ip = env::var("CLASH_VERGE_REV_IP").unwrap_or_else(|_| {
|
||||||
env::var("CLASH_VERGE_REV_IP").unwrap_or_else(|_| "127.0.0.1".to_string());
|
Config::verge()
|
||||||
|
.latest()
|
||||||
|
.proxy_host
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| "127.0.0.1".to_string())
|
||||||
|
});
|
||||||
|
|
||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||||
let port = { Config::verge().latest().verge_mixed_port.unwrap_or(7897) };
|
let port = { Config::verge().latest().verge_mixed_port.unwrap_or(7897) };
|
||||||
@@ -72,10 +93,16 @@ pub fn copy_clash_env() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let export_text = match env_type.as_str() {
|
let export_text = match env_type.as_str() {
|
||||||
"bash" => format!("export https_proxy={http_proxy} http_proxy={http_proxy} all_proxy={socks5_proxy}"),
|
"bash" => format!(
|
||||||
|
"export https_proxy={http_proxy} http_proxy={http_proxy} all_proxy={socks5_proxy}"
|
||||||
|
),
|
||||||
"cmd" => format!("set http_proxy={http_proxy}\r\nset https_proxy={http_proxy}"),
|
"cmd" => format!("set http_proxy={http_proxy}\r\nset https_proxy={http_proxy}"),
|
||||||
"powershell" => format!("$env:HTTP_PROXY=\"{http_proxy}\"; $env:HTTPS_PROXY=\"{http_proxy}\""),
|
"powershell" => {
|
||||||
"nushell" => format!("load-env {{ http_proxy: \"{http_proxy}\", https_proxy: \"{http_proxy}\" }}"),
|
format!("$env:HTTP_PROXY=\"{http_proxy}\"; $env:HTTPS_PROXY=\"{http_proxy}\"")
|
||||||
|
}
|
||||||
|
"nushell" => {
|
||||||
|
format!("load-env {{ http_proxy: \"{http_proxy}\", https_proxy: \"{http_proxy}\" }}")
|
||||||
|
}
|
||||||
"fish" => format!("set -x http_proxy {http_proxy}; set -x https_proxy {http_proxy}"),
|
"fish" => format!("set -x http_proxy {http_proxy}; set -x https_proxy {http_proxy}"),
|
||||||
_ => {
|
_ => {
|
||||||
log::error!(target: "app", "copy_clash_env: Invalid env type! {env_type}");
|
log::error!(target: "app", "copy_clash_env: Invalid env type! {env_type}");
|
||||||
@@ -83,7 +110,7 @@ pub fn copy_clash_env() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(_) = cliboard.write_text(export_text) {
|
if cliboard.write_text(export_text).is_err() {
|
||||||
log::error!(target: "app", "Failed to write to clipboard");
|
log::error!(target: "app", "Failed to write to clipboard");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,87 +1,39 @@
|
|||||||
use crate::config::Config;
|
#[cfg(target_os = "macos")]
|
||||||
use crate::core::handle;
|
use crate::AppHandleManager;
|
||||||
use crate::core::{sysopt, CoreManager};
|
use crate::{
|
||||||
use crate::module::mihomo::MihomoManager;
|
config::Config,
|
||||||
use crate::utils::resolve;
|
core::{handle, sysopt, CoreManager},
|
||||||
use futures;
|
logging,
|
||||||
use tauri::Manager;
|
module::mihomo::MihomoManager,
|
||||||
use tauri_plugin_window_state::{AppHandleExt, StateFlags};
|
utils::logging::Type,
|
||||||
|
};
|
||||||
|
|
||||||
/// Open or close the dashboard window
|
/// Open or close the dashboard window
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn open_or_close_dashboard() {
|
pub fn open_or_close_dashboard() {
|
||||||
println!("Attempting to open/close dashboard");
|
use crate::utils::window_manager::WindowManager;
|
||||||
|
|
||||||
log::info!(target: "app", "Attempting to open/close dashboard");
|
log::info!(target: "app", "Attempting to open/close dashboard");
|
||||||
|
|
||||||
if let Some(window) = handle::Handle::global().get_window() {
|
// 检查是否在轻量模式下
|
||||||
println!("Found existing window");
|
if crate::module::lightweight::is_in_lightweight_mode() {
|
||||||
log::info!(target: "app", "Found existing window");
|
log::info!(target: "app", "Currently in lightweight mode, exiting lightweight mode");
|
||||||
|
crate::module::lightweight::exit_lightweight_mode();
|
||||||
// 如果窗口存在,则切换其显示状态
|
log::info!(target: "app", "Creating new window after exiting lightweight mode");
|
||||||
match window.is_visible() {
|
let result = WindowManager::show_main_window();
|
||||||
Ok(visible) => {
|
log::info!(target: "app", "Window operation result: {:?}", result);
|
||||||
println!("Window visibility status: {}", visible);
|
return;
|
||||||
log::info!(target: "app", "Window visibility status: {}", visible);
|
|
||||||
|
|
||||||
if visible {
|
|
||||||
println!("Attempting to hide window");
|
|
||||||
log::info!(target: "app", "Attempting to hide window");
|
|
||||||
let _ = window.hide();
|
|
||||||
} else {
|
|
||||||
println!("Attempting to show and focus window");
|
|
||||||
log::info!(target: "app", "Attempting to show and focus window");
|
|
||||||
if window.is_minimized().unwrap_or(false) {
|
|
||||||
let _ = window.unminimize();
|
|
||||||
}
|
|
||||||
let _ = window.show();
|
|
||||||
let _ = window.set_focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
println!("Failed to get window visibility: {:?}", e);
|
|
||||||
log::error!(target: "app", "Failed to get window visibility: {:?}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
println!("No existing window found, creating new window");
|
|
||||||
log::info!(target: "app", "No existing window found, creating new window");
|
|
||||||
resolve::create_window();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 使用统一的窗口管理器切换窗口状态
|
||||||
|
let result = WindowManager::toggle_main_window();
|
||||||
|
log::info!(target: "app", "Window toggle result: {:?}", result);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Setup window state monitor to save window position and size in real-time
|
/// 异步优化的应用退出函数
|
||||||
pub fn setup_window_state_monitor(app_handle: &tauri::AppHandle) {
|
pub fn quit() {
|
||||||
let window = app_handle.get_webview_window("main").unwrap();
|
use crate::process::AsyncHandler;
|
||||||
let app_handle_clone = app_handle.clone();
|
logging!(debug, Type::System, true, "启动退出流程");
|
||||||
|
|
||||||
// 监听窗口移动事件
|
|
||||||
let app_handle_move = app_handle_clone.clone();
|
|
||||||
window.on_window_event(move |event| {
|
|
||||||
match event {
|
|
||||||
// 窗口移动时保存状态
|
|
||||||
tauri::WindowEvent::Moved(_) => {
|
|
||||||
let _ = app_handle_move.save_window_state(StateFlags::all());
|
|
||||||
}
|
|
||||||
// 窗口调整大小时保存状态
|
|
||||||
tauri::WindowEvent::Resized(_) => {
|
|
||||||
let _ = app_handle_move.save_window_state(StateFlags::all());
|
|
||||||
}
|
|
||||||
// 其他可能改变窗口状态的事件
|
|
||||||
tauri::WindowEvent::ScaleFactorChanged { .. } => {
|
|
||||||
let _ = app_handle_move.save_window_state(StateFlags::all());
|
|
||||||
}
|
|
||||||
// 窗口关闭时保存
|
|
||||||
tauri::WindowEvent::CloseRequested { .. } => {
|
|
||||||
let _ = app_handle_move.save_window_state(StateFlags::all());
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 优化的应用退出函数
|
|
||||||
pub fn quit(code: Option<i32>) {
|
|
||||||
log::debug!(target: "app", "启动退出流程");
|
|
||||||
|
|
||||||
// 获取应用句柄并设置退出标志
|
// 获取应用句柄并设置退出标志
|
||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||||
@@ -90,50 +42,185 @@ pub fn quit(code: Option<i32>) {
|
|||||||
// 优先关闭窗口,提供立即反馈
|
// 优先关闭窗口,提供立即反馈
|
||||||
if let Some(window) = handle::Handle::global().get_window() {
|
if let Some(window) = handle::Handle::global().get_window() {
|
||||||
let _ = window.hide();
|
let _ = window.hide();
|
||||||
|
log::info!(target: "app", "窗口已隐藏");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 在单独线程中处理资源清理,避免阻塞主线程
|
// 使用异步任务处理资源清理,避免阻塞
|
||||||
std::thread::spawn(move || {
|
AsyncHandler::spawn(move || async move {
|
||||||
// 使用tokio运行时执行异步清理任务
|
logging!(info, Type::System, true, "开始异步清理资源");
|
||||||
tauri::async_runtime::block_on(async {
|
let cleanup_result = clean_async().await;
|
||||||
// 使用超时机制处理清理操作
|
|
||||||
use tokio::time::{timeout, Duration};
|
|
||||||
|
|
||||||
// 1. 直接关闭TUN模式 (优先处理,通常最容易卡住)
|
logging!(
|
||||||
if Config::verge().data().enable_tun_mode.unwrap_or(false) {
|
info,
|
||||||
let disable = serde_json::json!({
|
Type::System,
|
||||||
"tun": {
|
true,
|
||||||
"enable": false
|
"资源清理完成,退出代码: {}",
|
||||||
}
|
if cleanup_result { 0 } else { 1 }
|
||||||
});
|
);
|
||||||
|
app_handle.exit(if cleanup_result { 0 } else { 1 });
|
||||||
// 设置1秒超时
|
|
||||||
let _ = timeout(
|
|
||||||
Duration::from_secs(1),
|
|
||||||
MihomoManager::global().patch_configs(disable),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 并行处理系统代理和核心进程清理
|
|
||||||
let proxy_future = timeout(
|
|
||||||
Duration::from_secs(1),
|
|
||||||
sysopt::Sysopt::global().reset_sysproxy(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let core_future = timeout(Duration::from_secs(1), CoreManager::global().stop_core());
|
|
||||||
|
|
||||||
// 同时等待两个任务完成
|
|
||||||
let _ = futures::join!(proxy_future, core_future);
|
|
||||||
|
|
||||||
// 3. 处理macOS特定清理
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
{
|
|
||||||
let _ = timeout(Duration::from_millis(500), resolve::restore_public_dns()).await;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 无论清理结果如何,确保应用退出
|
|
||||||
app_handle.exit(code.unwrap_or(0));
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn clean_async() -> bool {
|
||||||
|
use tokio::time::{timeout, Duration};
|
||||||
|
|
||||||
|
logging!(info, Type::System, true, "开始执行异步清理操作...");
|
||||||
|
|
||||||
|
// 1. 处理TUN模式
|
||||||
|
let tun_task = async {
|
||||||
|
if Config::verge().data().enable_tun_mode.unwrap_or(false) {
|
||||||
|
let disable_tun = serde_json::json!({
|
||||||
|
"tun": {
|
||||||
|
"enable": false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
match timeout(
|
||||||
|
Duration::from_secs(2),
|
||||||
|
MihomoManager::global().patch_configs(disable_tun),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => {
|
||||||
|
log::info!(target: "app", "TUN模式已禁用");
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
log::warn!(target: "app", "禁用TUN模式超时");
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. 系统代理重置
|
||||||
|
let proxy_task = async {
|
||||||
|
match timeout(
|
||||||
|
Duration::from_secs(3),
|
||||||
|
sysopt::Sysopt::global().reset_sysproxy(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => {
|
||||||
|
log::info!(target: "app", "系统代理已重置");
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
log::warn!(target: "app", "重置系统代理超时");
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. 核心服务停止
|
||||||
|
let core_task = async {
|
||||||
|
match timeout(Duration::from_secs(3), CoreManager::global().stop_core()).await {
|
||||||
|
Ok(_) => {
|
||||||
|
log::info!(target: "app", "核心服务已停止");
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
log::warn!(target: "app", "停止核心服务超时");
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 4. DNS恢复(仅macOS)
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
let dns_task = async {
|
||||||
|
match timeout(
|
||||||
|
Duration::from_millis(1000),
|
||||||
|
crate::utils::resolve::restore_public_dns(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => {
|
||||||
|
log::info!(target: "app", "DNS设置已恢复");
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
log::warn!(target: "app", "恢复DNS设置超时");
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 并行执行所有清理任务
|
||||||
|
let (tun_success, proxy_success, core_success) = tokio::join!(tun_task, proxy_task, core_task);
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
let dns_success = dns_task.await;
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
let dns_success = true;
|
||||||
|
|
||||||
|
let all_success = tun_success && proxy_success && core_success && dns_success;
|
||||||
|
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::System,
|
||||||
|
true,
|
||||||
|
"异步清理操作完成 - TUN: {}, 代理: {}, 核心: {}, DNS: {}, 总体: {}",
|
||||||
|
tun_success,
|
||||||
|
proxy_success,
|
||||||
|
core_success,
|
||||||
|
dns_success,
|
||||||
|
all_success
|
||||||
|
);
|
||||||
|
|
||||||
|
all_success
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clean() -> bool {
|
||||||
|
use crate::process::AsyncHandler;
|
||||||
|
|
||||||
|
let (tx, rx) = std::sync::mpsc::channel();
|
||||||
|
|
||||||
|
AsyncHandler::spawn(move || async move {
|
||||||
|
logging!(info, Type::System, true, "开始执行清理操作...");
|
||||||
|
|
||||||
|
// 使用已有的异步清理函数
|
||||||
|
let cleanup_result = clean_async().await;
|
||||||
|
|
||||||
|
// 发送结果
|
||||||
|
let _ = tx.send(cleanup_result);
|
||||||
|
});
|
||||||
|
|
||||||
|
match rx.recv_timeout(std::time::Duration::from_secs(8)) {
|
||||||
|
Ok(result) => {
|
||||||
|
logging!(info, Type::System, true, "清理操作完成,结果: {}", result);
|
||||||
|
result
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
logging!(
|
||||||
|
warn,
|
||||||
|
Type::System,
|
||||||
|
true,
|
||||||
|
"清理操作超时,返回成功状态避免阻塞"
|
||||||
|
);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
pub fn hide() {
|
||||||
|
use crate::module::lightweight::add_light_weight_timer;
|
||||||
|
|
||||||
|
let enable_auto_light_weight_mode = Config::verge()
|
||||||
|
.data()
|
||||||
|
.enable_auto_light_weight_mode
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if enable_auto_light_weight_mode {
|
||||||
|
add_light_weight_timer();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(window) = handle::Handle::global().get_window() {
|
||||||
|
if window.is_visible().unwrap_or(false) {
|
||||||
|
let _ = window.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AppHandleManager::global().set_activation_policy_accessory();
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,17 +3,24 @@ mod config;
|
|||||||
mod core;
|
mod core;
|
||||||
mod enhance;
|
mod enhance;
|
||||||
mod feat;
|
mod feat;
|
||||||
mod utils;
|
|
||||||
mod module;
|
mod module;
|
||||||
use crate::core::hotkey;
|
mod process;
|
||||||
use crate::utils::{resolve, resolve::resolve_scheme, server};
|
mod state;
|
||||||
|
mod utils;
|
||||||
|
use crate::{
|
||||||
|
core::hotkey,
|
||||||
|
process::AsyncHandler,
|
||||||
|
utils::{resolve, resolve::resolve_scheme, server},
|
||||||
|
};
|
||||||
use config::Config;
|
use config::Config;
|
||||||
use tauri_plugin_autostart::MacosLauncher;
|
|
||||||
use tauri_plugin_deep_link::DeepLinkExt;
|
|
||||||
use std::sync::{Mutex, Once};
|
use std::sync::{Mutex, Once};
|
||||||
use tauri::AppHandle;
|
use tauri::AppHandle;
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
use tauri_plugin_autostart::MacosLauncher;
|
||||||
|
use tauri_plugin_deep_link::DeepLinkExt;
|
||||||
|
use tokio::time::{timeout, Duration};
|
||||||
|
use utils::logging::Type;
|
||||||
|
|
||||||
/// A global singleton handle to the application.
|
/// A global singleton handle to the application.
|
||||||
pub struct AppHandleManager {
|
pub struct AppHandleManager {
|
||||||
@@ -57,7 +64,7 @@ impl AppHandleManager {
|
|||||||
let _ = app_handle.set_activation_policy(tauri::ActivationPolicy::Regular);
|
let _ = app_handle.set_activation_policy(tauri::ActivationPolicy::Regular);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_activation_policy_accessory(&self) {
|
pub fn set_activation_policy_accessory(&self) {
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
@@ -66,7 +73,7 @@ impl AppHandleManager {
|
|||||||
let _ = app_handle.set_activation_policy(tauri::ActivationPolicy::Accessory);
|
let _ = app_handle.set_activation_policy(tauri::ActivationPolicy::Accessory);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_activation_policy_prohibited(&self) {
|
pub fn set_activation_policy_prohibited(&self) {
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
@@ -77,19 +84,38 @@ impl AppHandleManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::panic)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
// 单例检测
|
utils::network::NetworkManager::global().init();
|
||||||
let app_exists: bool = tauri::async_runtime::block_on(async move {
|
|
||||||
if server::check_singleton().await.is_err() {
|
let _ = utils::dirs::init_portable_flag();
|
||||||
println!("app exists");
|
|
||||||
true
|
// 异步单例检测
|
||||||
} else {
|
AsyncHandler::spawn(move || async move {
|
||||||
false
|
logging!(info, Type::Setup, true, "开始检查单例实例...");
|
||||||
|
match timeout(Duration::from_secs(3), server::check_singleton()).await {
|
||||||
|
Ok(result) => {
|
||||||
|
if result.is_err() {
|
||||||
|
logging!(info, Type::Setup, true, "检测到已有应用实例运行");
|
||||||
|
if let Some(app_handle) = AppHandleManager::global().get() {
|
||||||
|
app_handle.exit(0);
|
||||||
|
} else {
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logging!(info, Type::Setup, true, "未检测到其他应用实例");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
logging!(
|
||||||
|
warn,
|
||||||
|
Type::Setup,
|
||||||
|
true,
|
||||||
|
"单例检查超时,假定没有其他实例运行"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if app_exists {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
|
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
|
||||||
@@ -99,40 +125,97 @@ pub fn run() {
|
|||||||
|
|
||||||
#[allow(unused_mut)]
|
#[allow(unused_mut)]
|
||||||
let mut builder = tauri::Builder::default()
|
let mut builder = tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_autostart::init(
|
|
||||||
MacosLauncher::LaunchAgent,
|
|
||||||
None,
|
|
||||||
))
|
|
||||||
.plugin(tauri_plugin_window_state::Builder::new().build())
|
|
||||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||||
.plugin(tauri_plugin_clipboard_manager::init())
|
.plugin(tauri_plugin_clipboard_manager::init())
|
||||||
.plugin(tauri_plugin_process::init())
|
.plugin(tauri_plugin_process::init())
|
||||||
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
|
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
|
||||||
.plugin(tauri_plugin_notification::init())
|
|
||||||
.plugin(tauri_plugin_fs::init())
|
.plugin(tauri_plugin_fs::init())
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
.plugin(tauri_plugin_deep_link::init())
|
.plugin(tauri_plugin_deep_link::init())
|
||||||
.plugin(tauri_plugin_window_state::Builder::default().build())
|
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
|
logging!(info, Type::Setup, true, "开始应用初始化...");
|
||||||
|
let mut auto_start_plugin_builder = tauri_plugin_autostart::Builder::new();
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
auto_start_plugin_builder = auto_start_plugin_builder
|
||||||
|
.macos_launcher(MacosLauncher::LaunchAgent)
|
||||||
|
.app_name(app.config().identifier.clone());
|
||||||
|
}
|
||||||
|
let _ = app.handle().plugin(auto_start_plugin_builder.build());
|
||||||
|
|
||||||
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
|
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
|
||||||
{
|
{
|
||||||
use tauri_plugin_deep_link::DeepLinkExt;
|
use tauri_plugin_deep_link::DeepLinkExt;
|
||||||
log_err!(app.deep_link().register_all());
|
logging!(info, Type::Setup, true, "注册深层链接...");
|
||||||
|
logging_error!(Type::System, true, app.deep_link().register_all());
|
||||||
}
|
}
|
||||||
|
|
||||||
app.deep_link().on_open_url(|event| {
|
app.deep_link().on_open_url(|event| {
|
||||||
tauri::async_runtime::spawn(async move {
|
AsyncHandler::spawn(move || {
|
||||||
if let Some(url) = event.urls().first() {
|
let url = event.urls().first().map(|u| u.to_string());
|
||||||
log_err!(resolve_scheme(url.to_string()).await);
|
async move {
|
||||||
|
if let Some(url) = url {
|
||||||
|
logging_error!(Type::Setup, true, resolve_scheme(url).await);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
tauri::async_runtime::block_on(async move {
|
// 窗口管理
|
||||||
resolve::resolve_setup(app).await;
|
logging!(info, Type::Setup, true, "初始化窗口状态管理...");
|
||||||
|
let window_state_plugin = tauri_plugin_window_state::Builder::new()
|
||||||
|
.with_filename("window_state.json")
|
||||||
|
.with_state_flags(tauri_plugin_window_state::StateFlags::default())
|
||||||
|
.build();
|
||||||
|
let _ = app.handle().plugin(window_state_plugin);
|
||||||
|
|
||||||
|
// 异步处理
|
||||||
|
let app_handle = app.handle().clone();
|
||||||
|
AsyncHandler::spawn(move || async move {
|
||||||
|
logging!(info, Type::Setup, true, "异步执行应用设置...");
|
||||||
|
match timeout(
|
||||||
|
Duration::from_secs(30),
|
||||||
|
resolve::resolve_setup_async(&app_handle),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => {
|
||||||
|
logging!(info, Type::Setup, true, "应用设置成功完成");
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Setup,
|
||||||
|
true,
|
||||||
|
"应用设置超时(30秒),继续执行后续流程"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
logging!(info, Type::Setup, true, "执行主要设置操作...");
|
||||||
|
|
||||||
|
logging!(info, Type::Setup, true, "初始化AppHandleManager...");
|
||||||
|
AppHandleManager::global().init(app.handle().clone());
|
||||||
|
|
||||||
|
logging!(info, Type::Setup, true, "初始化核心句柄...");
|
||||||
|
core::handle::Handle::global().init(app.handle());
|
||||||
|
|
||||||
|
logging!(info, Type::Setup, true, "初始化配置...");
|
||||||
|
if let Err(e) = utils::init::init_config() {
|
||||||
|
logging!(error, Type::Setup, true, "初始化配置失败: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
logging!(info, Type::Setup, true, "初始化资源...");
|
||||||
|
if let Err(e) = utils::init::init_resources() {
|
||||||
|
logging!(error, Type::Setup, true, "初始化资源失败: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
app.manage(Mutex::new(state::proxy::CmdProxyState::default()));
|
||||||
|
app.manage(Mutex::new(state::lightweight::LightWeightState::default()));
|
||||||
|
|
||||||
|
logging!(info, Type::Setup, true, "初始化完成,继续执行");
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
@@ -145,11 +228,29 @@ pub fn run() {
|
|||||||
cmd::open_core_dir,
|
cmd::open_core_dir,
|
||||||
cmd::get_portable_flag,
|
cmd::get_portable_flag,
|
||||||
cmd::get_network_interfaces,
|
cmd::get_network_interfaces,
|
||||||
cmd::restart_core,
|
cmd::get_system_hostname,
|
||||||
cmd::restart_app,
|
cmd::restart_app,
|
||||||
// 添加新的命令
|
// 内核管理
|
||||||
|
cmd::start_core,
|
||||||
|
cmd::stop_core,
|
||||||
|
cmd::restart_core,
|
||||||
|
// 启动命令
|
||||||
|
cmd::notify_ui_ready,
|
||||||
|
cmd::update_ui_stage,
|
||||||
|
cmd::reset_ui_ready_state,
|
||||||
cmd::get_running_mode,
|
cmd::get_running_mode,
|
||||||
|
cmd::get_app_uptime,
|
||||||
|
cmd::get_auto_launch_status,
|
||||||
|
cmd::is_admin,
|
||||||
|
// 添加轻量模式相关命令
|
||||||
|
cmd::entry_lightweight_mode,
|
||||||
|
cmd::exit_lightweight_mode,
|
||||||
|
// service 管理
|
||||||
cmd::install_service,
|
cmd::install_service,
|
||||||
|
cmd::uninstall_service,
|
||||||
|
cmd::reinstall_service,
|
||||||
|
cmd::repair_service,
|
||||||
|
cmd::is_service_available,
|
||||||
// clash
|
// clash
|
||||||
cmd::get_clash_info,
|
cmd::get_clash_info,
|
||||||
cmd::patch_clash_config,
|
cmd::patch_clash_config,
|
||||||
@@ -162,11 +263,13 @@ pub fn run() {
|
|||||||
cmd::invoke_uwp_tool,
|
cmd::invoke_uwp_tool,
|
||||||
cmd::copy_clash_env,
|
cmd::copy_clash_env,
|
||||||
cmd::get_proxies,
|
cmd::get_proxies,
|
||||||
|
cmd::force_refresh_proxies,
|
||||||
cmd::get_providers_proxies,
|
cmd::get_providers_proxies,
|
||||||
cmd::save_dns_config,
|
cmd::save_dns_config,
|
||||||
cmd::apply_dns_config,
|
cmd::apply_dns_config,
|
||||||
cmd::check_dns_config_exists,
|
cmd::check_dns_config_exists,
|
||||||
cmd::get_dns_config_content,
|
cmd::get_dns_config_content,
|
||||||
|
cmd::validate_dns_config,
|
||||||
// verge
|
// verge
|
||||||
cmd::get_verge_config,
|
cmd::get_verge_config,
|
||||||
cmd::patch_verge_config,
|
cmd::patch_verge_config,
|
||||||
@@ -190,6 +293,7 @@ pub fn run() {
|
|||||||
cmd::delete_profile,
|
cmd::delete_profile,
|
||||||
cmd::read_profile_file,
|
cmd::read_profile_file,
|
||||||
cmd::save_profile_file,
|
cmd::save_profile_file,
|
||||||
|
cmd::get_next_update_time,
|
||||||
// script validation
|
// script validation
|
||||||
cmd::script_validate_notice,
|
cmd::script_validate_notice,
|
||||||
cmd::validate_script_file,
|
cmd::validate_script_file,
|
||||||
@@ -203,6 +307,13 @@ pub fn run() {
|
|||||||
cmd::restore_webdav_backup,
|
cmd::restore_webdav_backup,
|
||||||
// export diagnostic info for issue reporting
|
// export diagnostic info for issue reporting
|
||||||
cmd::export_diagnostic_info,
|
cmd::export_diagnostic_info,
|
||||||
|
// get system info for display
|
||||||
|
cmd::get_system_info,
|
||||||
|
// media unlock checker
|
||||||
|
cmd::get_unlock_items,
|
||||||
|
cmd::check_media_unlock,
|
||||||
|
// light-weight model
|
||||||
|
cmd::entry_lightweight_mode,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
@@ -210,21 +321,36 @@ pub fn run() {
|
|||||||
builder = builder.plugin(devtools);
|
builder = builder.plugin(devtools);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Macos Application Menu
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
// Temporary Achived due to cannot CMD+C/V/A
|
||||||
|
}
|
||||||
|
|
||||||
let app = builder
|
let app = builder
|
||||||
.build(tauri::generate_context!())
|
.build(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|
||||||
app.run(|app_handle, e| match e {
|
app.run(|app_handle, e| match e {
|
||||||
tauri::RunEvent::Ready | tauri::RunEvent::Resumed => {
|
tauri::RunEvent::Ready | tauri::RunEvent::Resumed => {
|
||||||
|
logging!(info, Type::System, true, "应用就绪或恢复");
|
||||||
AppHandleManager::global().init(app_handle.clone());
|
AppHandleManager::global().init(app_handle.clone());
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
let main_window = AppHandleManager::global().get_handle().get_webview_window("main").unwrap();
|
if let Some(window) = AppHandleManager::global()
|
||||||
let _ = main_window.set_title("Clash Verge");
|
.get_handle()
|
||||||
|
.get_webview_window("main")
|
||||||
|
{
|
||||||
|
logging!(info, Type::Window, true, "设置macOS窗口标题");
|
||||||
|
let _ = window.set_title("Clash Verge");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
tauri::RunEvent::Reopen { has_visible_windows, .. } => {
|
tauri::RunEvent::Reopen {
|
||||||
|
has_visible_windows,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
if !has_visible_windows {
|
if !has_visible_windows {
|
||||||
AppHandleManager::global().set_activation_policy_regular();
|
AppHandleManager::global().set_activation_policy_regular();
|
||||||
}
|
}
|
||||||
@@ -235,6 +361,13 @@ pub fn run() {
|
|||||||
api.prevent_exit();
|
api.prevent_exit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
tauri::RunEvent::Exit => {
|
||||||
|
// avoid duplicate cleanup
|
||||||
|
if core::handle::Handle::global().is_exiting() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
feat::clean();
|
||||||
|
}
|
||||||
tauri::RunEvent::WindowEvent { label, event, .. } => {
|
tauri::RunEvent::WindowEvent { label, event, .. } => {
|
||||||
if label == "main" {
|
if label == "main" {
|
||||||
match event {
|
match event {
|
||||||
@@ -244,54 +377,76 @@ pub fn run() {
|
|||||||
if core::handle::Handle::global().is_exiting() {
|
if core::handle::Handle::global().is_exiting() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
println!("closing window...");
|
log::info!(target: "app", "closing window...");
|
||||||
api.prevent_close();
|
api.prevent_close();
|
||||||
let window = core::handle::Handle::global().get_window().unwrap();
|
if let Some(window) = core::handle::Handle::global().get_window() {
|
||||||
let _ = window.hide();
|
let _ = window.hide();
|
||||||
|
} else {
|
||||||
|
logging!(warn, Type::Window, true, "尝试隐藏窗口但窗口不存在");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
tauri::WindowEvent::Focused(true) => {
|
tauri::WindowEvent::Focused(true) => {
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
log_err!(hotkey::Hotkey::global().register("CMD+Q", "quit"));
|
logging_error!(
|
||||||
|
Type::Hotkey,
|
||||||
|
true,
|
||||||
|
hotkey::Hotkey::global().register("CMD+Q", "quit")
|
||||||
|
);
|
||||||
|
logging_error!(
|
||||||
|
Type::Hotkey,
|
||||||
|
true,
|
||||||
|
hotkey::Hotkey::global().register("CMD+W", "hide")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
|
||||||
{
|
{
|
||||||
log_err!(hotkey::Hotkey::global().register("Control+Q", "quit"));
|
let is_enable_global_hotkey = Config::verge()
|
||||||
};
|
.latest()
|
||||||
{
|
.enable_global_hotkey
|
||||||
let is_enable_global_hotkey = Config::verge().latest().enable_global_hotkey.unwrap_or(true);
|
.unwrap_or(true);
|
||||||
if !is_enable_global_hotkey {
|
if !is_enable_global_hotkey {
|
||||||
log_err!(hotkey::Hotkey::global().init())
|
logging_error!(Type::Hotkey, true, hotkey::Hotkey::global().init())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tauri::WindowEvent::Focused(false) => {
|
tauri::WindowEvent::Focused(false) => {
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
log_err!(hotkey::Hotkey::global().unregister("CMD+Q"));
|
logging_error!(
|
||||||
|
Type::Hotkey,
|
||||||
|
true,
|
||||||
|
hotkey::Hotkey::global().unregister("CMD+Q")
|
||||||
|
);
|
||||||
|
logging_error!(
|
||||||
|
Type::Hotkey,
|
||||||
|
true,
|
||||||
|
hotkey::Hotkey::global().unregister("CMD+W")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
#[cfg(not(target_os = "macos"))]
|
|
||||||
{
|
{
|
||||||
log_err!(hotkey::Hotkey::global().unregister("Control+Q"));
|
let is_enable_global_hotkey = Config::verge()
|
||||||
};
|
.latest()
|
||||||
{
|
.enable_global_hotkey
|
||||||
let is_enable_global_hotkey = Config::verge().latest().enable_global_hotkey.unwrap_or(true);
|
.unwrap_or(true);
|
||||||
if !is_enable_global_hotkey {
|
if !is_enable_global_hotkey {
|
||||||
log_err!(hotkey::Hotkey::global().reset())
|
logging_error!(Type::Hotkey, true, hotkey::Hotkey::global().reset())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tauri::WindowEvent::Destroyed => {
|
tauri::WindowEvent::Destroyed => {
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
log_err!(hotkey::Hotkey::global().unregister("CMD+Q"));
|
logging_error!(
|
||||||
|
Type::Hotkey,
|
||||||
|
true,
|
||||||
|
hotkey::Hotkey::global().unregister("CMD+Q")
|
||||||
|
);
|
||||||
|
logging_error!(
|
||||||
|
Type::Hotkey,
|
||||||
|
true,
|
||||||
|
hotkey::Hotkey::global().unregister("CMD+W")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
|
||||||
{
|
|
||||||
log_err!(hotkey::Hotkey::global().unregister("Control+Q"));
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|||||||
265
src-tauri/src/module/lightweight.rs
Normal file
265
src-tauri/src/module/lightweight.rs
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
use crate::{
|
||||||
|
config::Config,
|
||||||
|
core::{handle, timer::Timer, tray::Tray},
|
||||||
|
log_err, logging,
|
||||||
|
state::lightweight::LightWeightState,
|
||||||
|
utils::logging::Type,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
use crate::logging_error;
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
use crate::AppHandleManager;
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use delay_timer::prelude::TaskBuilder;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use tauri::{Listener, Manager};
|
||||||
|
|
||||||
|
const LIGHT_WEIGHT_TASK_UID: &str = "light_weight_task";
|
||||||
|
|
||||||
|
fn with_lightweight_status<F, R>(f: F) -> R
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut LightWeightState) -> R,
|
||||||
|
{
|
||||||
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||||
|
let state = app_handle.state::<Mutex<LightWeightState>>();
|
||||||
|
let mut guard = state.lock().unwrap();
|
||||||
|
f(&mut guard)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_once_auto_lightweight() {
|
||||||
|
LightWeightState::default().run_once_time(|| {
|
||||||
|
let is_silent_start = Config::verge().data().enable_silent_start.unwrap_or(true);
|
||||||
|
let enable_auto = Config::verge()
|
||||||
|
.data()
|
||||||
|
.enable_auto_light_weight_mode
|
||||||
|
.unwrap_or(true);
|
||||||
|
if enable_auto && is_silent_start {
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Lightweight,
|
||||||
|
true,
|
||||||
|
"正常创建窗口和添加定时器监听器"
|
||||||
|
);
|
||||||
|
set_lightweight_mode(false);
|
||||||
|
disable_auto_light_weight_mode();
|
||||||
|
|
||||||
|
// 触发托盘更新
|
||||||
|
if let Err(e) = Tray::global().update_part() {
|
||||||
|
log::warn!("Failed to update tray: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn auto_lightweight_mode_init() {
|
||||||
|
if let Some(app_handle) = handle::Handle::global().app_handle() {
|
||||||
|
let _ = app_handle.state::<Mutex<LightWeightState>>();
|
||||||
|
let is_silent_start = { Config::verge().data().enable_silent_start }.unwrap_or(false);
|
||||||
|
let enable_auto = { Config::verge().data().enable_auto_light_weight_mode }.unwrap_or(false);
|
||||||
|
|
||||||
|
if enable_auto && is_silent_start {
|
||||||
|
logging!(info, Type::Lightweight, true, "自动轻量模式静默启动");
|
||||||
|
set_lightweight_mode(true);
|
||||||
|
enable_auto_light_weight_mode();
|
||||||
|
|
||||||
|
// 确保托盘状态更新
|
||||||
|
if let Err(e) = Tray::global().update_part() {
|
||||||
|
log::warn!("Failed to update tray: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否处于轻量模式
|
||||||
|
pub fn is_in_lightweight_mode() -> bool {
|
||||||
|
with_lightweight_status(|state| state.is_lightweight)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置轻量模式状态
|
||||||
|
fn set_lightweight_mode(value: bool) {
|
||||||
|
with_lightweight_status(|state| {
|
||||||
|
state.set_lightweight_mode(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 触发托盘更新
|
||||||
|
if let Err(e) = Tray::global().update_part() {
|
||||||
|
log::warn!("Failed to update tray: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enable_auto_light_weight_mode() {
|
||||||
|
Timer::global().init().unwrap();
|
||||||
|
logging!(info, Type::Lightweight, true, "开启自动轻量模式");
|
||||||
|
setup_window_close_listener();
|
||||||
|
setup_webview_focus_listener();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn disable_auto_light_weight_mode() {
|
||||||
|
logging!(info, Type::Lightweight, true, "关闭自动轻量模式");
|
||||||
|
let _ = cancel_light_weight_timer();
|
||||||
|
cancel_window_close_listener();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn entry_lightweight_mode() {
|
||||||
|
use crate::utils::window_manager::WindowManager;
|
||||||
|
|
||||||
|
let result = WindowManager::hide_main_window();
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Lightweight,
|
||||||
|
true,
|
||||||
|
"轻量模式隐藏窗口结果: {:?}",
|
||||||
|
result
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(window) = handle::Handle::global().get_window() {
|
||||||
|
if let Some(webview) = window.get_webview_window("main") {
|
||||||
|
let _ = webview.destroy();
|
||||||
|
}
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
AppHandleManager::global().set_activation_policy_accessory();
|
||||||
|
logging!(info, Type::Lightweight, true, "轻量模式已开启");
|
||||||
|
}
|
||||||
|
set_lightweight_mode(true);
|
||||||
|
let _ = cancel_light_weight_timer();
|
||||||
|
|
||||||
|
// 更新托盘显示
|
||||||
|
let _tray = crate::core::tray::Tray::global();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加从轻量模式恢复的函数
|
||||||
|
pub fn exit_lightweight_mode() {
|
||||||
|
// 确保当前确实处于轻量模式才执行退出操作
|
||||||
|
if !is_in_lightweight_mode() {
|
||||||
|
logging!(info, Type::Lightweight, true, "当前不在轻量模式,无需退出");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
set_lightweight_mode(false);
|
||||||
|
logging!(info, Type::Lightweight, true, "正在退出轻量模式");
|
||||||
|
|
||||||
|
// macOS激活策略
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
AppHandleManager::global().set_activation_policy_regular();
|
||||||
|
|
||||||
|
// 重置UI就绪状态
|
||||||
|
crate::utils::resolve::reset_ui_ready();
|
||||||
|
|
||||||
|
// 更新托盘显示
|
||||||
|
let _tray = crate::core::tray::Tray::global();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
pub fn add_light_weight_timer() {
|
||||||
|
logging_error!(Type::Lightweight, setup_light_weight_timer());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_window_close_listener() -> u32 {
|
||||||
|
if let Some(window) = handle::Handle::global().get_window() {
|
||||||
|
let handler = window.listen("tauri://close-requested", move |_event| {
|
||||||
|
let _ = setup_light_weight_timer();
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Lightweight,
|
||||||
|
true,
|
||||||
|
"监听到关闭请求,开始轻量模式计时"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return handler;
|
||||||
|
}
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_webview_focus_listener() -> u32 {
|
||||||
|
if let Some(window) = handle::Handle::global().get_window() {
|
||||||
|
let handler = window.listen("tauri://focus", move |_event| {
|
||||||
|
log_err!(cancel_light_weight_timer());
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Lightweight,
|
||||||
|
"监听到窗口获得焦点,取消轻量模式计时"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return handler;
|
||||||
|
}
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cancel_window_close_listener() {
|
||||||
|
if let Some(window) = handle::Handle::global().get_window() {
|
||||||
|
window.unlisten(setup_window_close_listener());
|
||||||
|
logging!(info, Type::Lightweight, true, "取消了窗口关闭监听");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_light_weight_timer() -> Result<()> {
|
||||||
|
Timer::global().init()?;
|
||||||
|
let once_by_minutes = Config::verge()
|
||||||
|
.latest()
|
||||||
|
.auto_light_weight_minutes
|
||||||
|
.unwrap_or(10);
|
||||||
|
|
||||||
|
// 获取task_id
|
||||||
|
let task_id = {
|
||||||
|
let mut timer_count = Timer::global().timer_count.lock();
|
||||||
|
let id = *timer_count;
|
||||||
|
*timer_count += 1;
|
||||||
|
id
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建任务
|
||||||
|
let task = TaskBuilder::default()
|
||||||
|
.set_task_id(task_id)
|
||||||
|
.set_maximum_parallel_runnable_num(1)
|
||||||
|
.set_frequency_once_by_minutes(once_by_minutes)
|
||||||
|
.spawn_async_routine(move || async move {
|
||||||
|
logging!(info, Type::Timer, true, "计时器到期,开始进入轻量模式");
|
||||||
|
entry_lightweight_mode();
|
||||||
|
})
|
||||||
|
.context("failed to create timer task")?;
|
||||||
|
|
||||||
|
// 添加任务到定时器
|
||||||
|
{
|
||||||
|
let delay_timer = Timer::global().delay_timer.write();
|
||||||
|
delay_timer
|
||||||
|
.add_task(task)
|
||||||
|
.context("failed to add timer task")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新任务映射
|
||||||
|
{
|
||||||
|
let mut timer_map = Timer::global().timer_map.write();
|
||||||
|
let timer_task = crate::core::timer::TimerTask {
|
||||||
|
task_id,
|
||||||
|
interval_minutes: once_by_minutes,
|
||||||
|
last_run: chrono::Local::now().timestamp(),
|
||||||
|
};
|
||||||
|
timer_map.insert(LIGHT_WEIGHT_TASK_UID.to_string(), timer_task);
|
||||||
|
}
|
||||||
|
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Timer,
|
||||||
|
true,
|
||||||
|
"计时器已设置,{} 分钟后将自动进入轻量模式",
|
||||||
|
once_by_minutes
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cancel_light_weight_timer() -> Result<()> {
|
||||||
|
let mut timer_map = Timer::global().timer_map.write();
|
||||||
|
let delay_timer = Timer::global().delay_timer.write();
|
||||||
|
|
||||||
|
if let Some(task) = timer_map.remove(LIGHT_WEIGHT_TASK_UID) {
|
||||||
|
delay_timer
|
||||||
|
.remove_task(task.task_id)
|
||||||
|
.context("failed to remove timer task")?;
|
||||||
|
logging!(info, Type::Timer, true, "计时器已取消");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -1,43 +1,94 @@
|
|||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use mihomo_api;
|
use mihomo_api;
|
||||||
use once_cell::sync::{Lazy, OnceCell};
|
use once_cell::sync::Lazy;
|
||||||
use std::sync::Mutex;
|
use parking_lot::{Mutex, RwLock};
|
||||||
use tauri::http::{HeaderMap, HeaderValue};
|
use std::time::{Duration, Instant};
|
||||||
|
use tauri::http::HeaderMap;
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
use tokio_tungstenite::tungstenite::http;
|
use tauri::http::HeaderValue;
|
||||||
|
|
||||||
|
// 缓存的最大有效期(5秒)
|
||||||
|
const CACHE_TTL: Duration = Duration::from_secs(5);
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, PartialEq)]
|
#[derive(Debug, Clone, Default, PartialEq)]
|
||||||
pub struct Rate {
|
pub struct Rate {
|
||||||
pub up: u64,
|
pub up: u64,
|
||||||
pub down: u64,
|
pub down: u64,
|
||||||
}
|
}
|
||||||
|
// 缓存MihomoManager实例
|
||||||
|
struct MihomoCache {
|
||||||
|
manager: mihomo_api::MihomoManager,
|
||||||
|
created_at: Instant,
|
||||||
|
server: String,
|
||||||
|
}
|
||||||
|
// 使用RwLock替代Mutex,允许多个读取操作并发进行
|
||||||
pub struct MihomoManager {
|
pub struct MihomoManager {
|
||||||
mihomo: Mutex<OnceCell<mihomo_api::MihomoManager>>,
|
mihomo_cache: RwLock<Option<MihomoCache>>,
|
||||||
|
create_lock: Mutex<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MihomoManager {
|
impl MihomoManager {
|
||||||
fn __global() -> &'static MihomoManager {
|
fn __global() -> &'static MihomoManager {
|
||||||
static INSTANCE: Lazy<MihomoManager> = Lazy::new(|| MihomoManager {
|
static INSTANCE: Lazy<MihomoManager> = Lazy::new(|| MihomoManager {
|
||||||
mihomo: Mutex::new(OnceCell::new()),
|
mihomo_cache: RwLock::new(None),
|
||||||
|
create_lock: Mutex::new(()),
|
||||||
});
|
});
|
||||||
&INSTANCE
|
&INSTANCE
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn global() -> mihomo_api::MihomoManager {
|
pub fn global() -> mihomo_api::MihomoManager {
|
||||||
let instance = MihomoManager::__global();
|
let instance = MihomoManager::__global();
|
||||||
let (current_server, headers) = MihomoManager::get_clash_client_info().unwrap();
|
|
||||||
|
|
||||||
let lock = instance.mihomo.lock().unwrap();
|
// 尝试从缓存读取(只需读锁)
|
||||||
if let Some(mihomo) = lock.get() {
|
{
|
||||||
if mihomo.get_mihomo_server() == current_server {
|
let cache = instance.mihomo_cache.read();
|
||||||
return mihomo.clone();
|
if let Some(cache_entry) = &*cache {
|
||||||
|
let (current_server, _) = MihomoManager::get_clash_client_info()
|
||||||
|
.unwrap_or_else(|| (String::new(), HeaderMap::new()));
|
||||||
|
|
||||||
|
// 检查缓存是否有效
|
||||||
|
if cache_entry.server == current_server
|
||||||
|
&& cache_entry.created_at.elapsed() < CACHE_TTL
|
||||||
|
{
|
||||||
|
return cache_entry.manager.clone();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lock.set(mihomo_api::MihomoManager::new(current_server, headers))
|
// 缓存无效,获取创建锁
|
||||||
.ok();
|
let _create_guard = instance.create_lock.lock();
|
||||||
lock.get().unwrap().clone()
|
|
||||||
|
// 再次检查缓存(双重检查锁定模式)
|
||||||
|
{
|
||||||
|
let cache = instance.mihomo_cache.read();
|
||||||
|
if let Some(cache_entry) = &*cache {
|
||||||
|
let (current_server, _) = MihomoManager::get_clash_client_info()
|
||||||
|
.unwrap_or_else(|| (String::new(), HeaderMap::new()));
|
||||||
|
|
||||||
|
if cache_entry.server == current_server
|
||||||
|
&& cache_entry.created_at.elapsed() < CACHE_TTL
|
||||||
|
{
|
||||||
|
return cache_entry.manager.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新实例
|
||||||
|
let (current_server, headers) = MihomoManager::get_clash_client_info()
|
||||||
|
.unwrap_or_else(|| (String::new(), HeaderMap::new()));
|
||||||
|
let manager = mihomo_api::MihomoManager::new(current_server.clone(), headers);
|
||||||
|
|
||||||
|
// 更新缓存
|
||||||
|
{
|
||||||
|
let mut cache = instance.mihomo_cache.write();
|
||||||
|
*cache = Some(MihomoCache {
|
||||||
|
manager: manager.clone(),
|
||||||
|
created_at: Instant::now(),
|
||||||
|
server: current_server,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
manager
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,17 +105,32 @@ impl MihomoManager {
|
|||||||
|
|
||||||
Some((server, headers))
|
Some((server, headers))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 提供默认值的版本,避免在connection_info为None时panic
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
fn get_clash_client_info_or_default() -> (String, HeaderMap) {
|
||||||
|
Self::get_clash_client_info().unwrap_or_else(|| {
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
headers.insert("Content-Type", "application/json".parse().unwrap());
|
||||||
|
("http://127.0.0.1:9090".to_string(), headers)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
pub fn get_traffic_ws_url() -> (String, HeaderValue) {
|
pub fn get_traffic_ws_url() -> (String, HeaderValue) {
|
||||||
let (url, headers) = MihomoManager::get_clash_client_info().unwrap();
|
let (url, headers) = MihomoManager::get_clash_client_info_or_default();
|
||||||
let ws_url = url.replace("http://", "ws://") + "/traffic";
|
let ws_url = url.replace("http://", "ws://") + "/traffic";
|
||||||
let auth = headers
|
let auth = headers
|
||||||
.get("Authorization")
|
.get("Authorization")
|
||||||
.unwrap()
|
.map(|val| val.to_str().unwrap_or("").to_string())
|
||||||
.to_str()
|
.unwrap_or_default();
|
||||||
.unwrap()
|
|
||||||
.to_string();
|
// 创建默认的空HeaderValue而不是使用unwrap_or_default
|
||||||
let token = http::header::HeaderValue::from_str(&auth).unwrap();
|
let token = match HeaderValue::from_str(&auth) {
|
||||||
return (ws_url, token);
|
Ok(v) => v,
|
||||||
|
Err(_) => HeaderValue::from_static(""),
|
||||||
|
};
|
||||||
|
|
||||||
|
(ws_url, token)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
|
pub mod lightweight;
|
||||||
|
pub mod mihomo;
|
||||||
pub mod sysinfo;
|
pub mod sysinfo;
|
||||||
pub mod mihomo;
|
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
use crate::core::{handle, CoreManager};
|
use crate::{
|
||||||
|
cmd::system,
|
||||||
|
core::{handle, CoreManager},
|
||||||
|
};
|
||||||
use std::fmt::{self, Debug, Formatter};
|
use std::fmt::{self, Debug, Formatter};
|
||||||
use sysinfo::System;
|
use sysinfo::System;
|
||||||
|
|
||||||
@@ -9,14 +12,15 @@ pub struct PlatformSpecification {
|
|||||||
system_arch: String,
|
system_arch: String,
|
||||||
verge_version: String,
|
verge_version: String,
|
||||||
running_mode: String,
|
running_mode: String,
|
||||||
|
is_admin: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for PlatformSpecification {
|
impl Debug for PlatformSpecification {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"System Name: {}\nSystem Version: {}\nSystem kernel Version: {}\nSystem Arch: {}\nVerge Version: {}\nRunning Mode: {}",
|
"System Name: {}\nSystem Version: {}\nSystem kernel Version: {}\nSystem Arch: {}\nVerge Version: {}\nRunning Mode: {}\nIs Admin: {}",
|
||||||
self.system_name, self.system_version, self.system_kernel_version, self.system_arch, self.verge_version, self.running_mode
|
self.system_name, self.system_version, self.system_kernel_version, self.system_arch, self.verge_version, self.running_mode, self.is_admin
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,23 +30,15 @@ impl PlatformSpecification {
|
|||||||
let system_name = System::name().unwrap_or("Null".into());
|
let system_name = System::name().unwrap_or("Null".into());
|
||||||
let system_version = System::long_os_version().unwrap_or("Null".into());
|
let system_version = System::long_os_version().unwrap_or("Null".into());
|
||||||
let system_kernel_version = System::kernel_version().unwrap_or("Null".into());
|
let system_kernel_version = System::kernel_version().unwrap_or("Null".into());
|
||||||
let system_arch = std::env::consts::ARCH.to_string();
|
let system_arch = System::cpu_arch();
|
||||||
|
|
||||||
let handler = handle::Handle::global().app_handle().unwrap();
|
let handler = handle::Handle::global().app_handle().unwrap();
|
||||||
let config = handler.config();
|
let verge_version = handler.package_info().version.to_string();
|
||||||
let verge_version = config.version.clone().unwrap_or("Null".into());
|
|
||||||
|
|
||||||
// Get running mode asynchronously
|
// 使用默认值避免在同步上下文中执行异步操作
|
||||||
let running_mode = tokio::task::block_in_place(|| {
|
let running_mode = "NotRunning".to_string();
|
||||||
tokio::runtime::Handle::current().block_on(async {
|
|
||||||
match CoreManager::global().get_running_mode().await {
|
|
||||||
crate::core::RunningMode::Service => "Service".to_string(),
|
|
||||||
crate::core::RunningMode::Sidecar => "Sidecar".to_string(),
|
|
||||||
crate::core::RunningMode::NotRunning => "Not Running".to_string(),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
|
let is_admin = system::is_admin().unwrap_or_default();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
system_name,
|
system_name,
|
||||||
@@ -51,6 +47,17 @@ impl PlatformSpecification {
|
|||||||
system_arch,
|
system_arch,
|
||||||
verge_version,
|
verge_version,
|
||||||
running_mode,
|
running_mode,
|
||||||
|
is_admin,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 异步方法来获取完整的系统信息
|
||||||
|
pub async fn new_async() -> Self {
|
||||||
|
let mut info = Self::new();
|
||||||
|
|
||||||
|
let running_mode = CoreManager::global().get_running_mode().await;
|
||||||
|
info.running_mode = running_mode.to_string();
|
||||||
|
|
||||||
|
info
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14
src-tauri/src/process/async_handler.rs
Normal file
14
src-tauri/src/process/async_handler.rs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
use std::future::Future;
|
||||||
|
use tauri::{async_runtime, async_runtime::JoinHandle};
|
||||||
|
|
||||||
|
pub struct AsyncHandler;
|
||||||
|
|
||||||
|
impl AsyncHandler {
|
||||||
|
pub fn spawn<F, Fut>(f: F) -> JoinHandle<()>
|
||||||
|
where
|
||||||
|
F: FnOnce() -> Fut + Send + 'static,
|
||||||
|
Fut: Future<Output = ()> + Send + 'static,
|
||||||
|
{
|
||||||
|
async_runtime::spawn(f())
|
||||||
|
}
|
||||||
|
}
|
||||||
2
src-tauri/src/process/mod.rs
Normal file
2
src-tauri/src/process/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
mod async_handler;
|
||||||
|
pub use async_handler::AsyncHandler;
|
||||||
44
src-tauri/src/state/lightweight.rs
Normal file
44
src-tauri/src/state/lightweight.rs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
use std::sync::{Arc, Once, OnceLock};
|
||||||
|
|
||||||
|
use crate::{logging, utils::logging::Type};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct LightWeightState {
|
||||||
|
#[allow(unused)]
|
||||||
|
once: Arc<Once>,
|
||||||
|
pub is_lightweight: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LightWeightState {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
once: Arc::new(Once::new()),
|
||||||
|
is_lightweight: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
pub fn run_once_time<F>(&self, f: F)
|
||||||
|
where
|
||||||
|
F: FnOnce() + Send + 'static,
|
||||||
|
{
|
||||||
|
self.once.call_once(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_lightweight_mode(&mut self, value: bool) -> &Self {
|
||||||
|
self.is_lightweight = value;
|
||||||
|
if value {
|
||||||
|
logging!(info, Type::Lightweight, true, "轻量模式已开启");
|
||||||
|
} else {
|
||||||
|
logging!(info, Type::Lightweight, true, "轻量模式已关闭");
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LightWeightState {
|
||||||
|
fn default() -> Self {
|
||||||
|
static INSTANCE: OnceLock<LightWeightState> = OnceLock::new();
|
||||||
|
INSTANCE.get_or_init(LightWeightState::new).clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src-tauri/src/state/mod.rs
Normal file
5
src-tauri/src/state/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// Tauri Manager 会进行 Arc 管理,无需额外 Arc
|
||||||
|
// https://tauri.app/develop/state-management/#do-you-need-arc
|
||||||
|
|
||||||
|
pub mod lightweight;
|
||||||
|
pub mod proxy;
|
||||||
19
src-tauri/src/state/proxy.rs
Normal file
19
src-tauri/src/state/proxy.rs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
pub struct CmdProxyState {
|
||||||
|
pub last_refresh_time: std::time::Instant,
|
||||||
|
pub need_refresh: bool,
|
||||||
|
pub proxies: Box<Value>,
|
||||||
|
pub providers_proxies: Box<Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CmdProxyState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
last_refresh_time: std::time::Instant::now(),
|
||||||
|
need_refresh: true,
|
||||||
|
proxies: Box::new(Value::Null),
|
||||||
|
providers_proxies: Box::new(Value::Null),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
116
src-tauri/src/utils/autostart.rs
Normal file
116
src-tauri/src/utils/autostart.rs
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
use log::info;
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
use std::{fs, path::Path, path::PathBuf};
|
||||||
|
|
||||||
|
/// Windows 下的开机启动文件夹路径
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub fn get_startup_dir() -> Result<PathBuf> {
|
||||||
|
let appdata = std::env::var("APPDATA").map_err(|_| anyhow!("无法获取 APPDATA 环境变量"))?;
|
||||||
|
|
||||||
|
let startup_dir = Path::new(&appdata)
|
||||||
|
.join("Microsoft")
|
||||||
|
.join("Windows")
|
||||||
|
.join("Start Menu")
|
||||||
|
.join("Programs")
|
||||||
|
.join("Startup");
|
||||||
|
|
||||||
|
if !startup_dir.exists() {
|
||||||
|
return Err(anyhow!("Startup 目录不存在: {:?}", startup_dir));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(startup_dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取当前可执行文件路径
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub fn get_exe_path() -> Result<PathBuf> {
|
||||||
|
let exe_path =
|
||||||
|
std::env::current_exe().map_err(|e| anyhow!("无法获取当前可执行文件路径: {}", e))?;
|
||||||
|
|
||||||
|
Ok(exe_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 创建快捷方式
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub fn create_shortcut() -> Result<()> {
|
||||||
|
let exe_path = get_exe_path()?;
|
||||||
|
let startup_dir = get_startup_dir()?;
|
||||||
|
let shortcut_path = startup_dir.join("Clash-Verge.lnk");
|
||||||
|
|
||||||
|
// 如果快捷方式已存在,直接返回成功
|
||||||
|
if shortcut_path.exists() {
|
||||||
|
info!(target: "app", "启动快捷方式已存在");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 PowerShell 创建快捷方式
|
||||||
|
let powershell_command = format!(
|
||||||
|
"$WshShell = New-Object -ComObject WScript.Shell; \
|
||||||
|
$Shortcut = $WshShell.CreateShortcut('{}'); \
|
||||||
|
$Shortcut.TargetPath = '{}'; \
|
||||||
|
$Shortcut.Save()",
|
||||||
|
shortcut_path.to_string_lossy().replace("\\", "\\\\"),
|
||||||
|
exe_path.to_string_lossy().replace("\\", "\\\\")
|
||||||
|
);
|
||||||
|
|
||||||
|
let output = std::process::Command::new("powershell")
|
||||||
|
.args(["-Command", &powershell_command])
|
||||||
|
.output()
|
||||||
|
.map_err(|e| anyhow!("执行 PowerShell 命令失败: {}", e))?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
let error_msg = String::from_utf8_lossy(&output.stderr);
|
||||||
|
return Err(anyhow!("创建快捷方式失败: {}", error_msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
info!(target: "app", "成功创建启动快捷方式");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 删除快捷方式
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub fn remove_shortcut() -> Result<()> {
|
||||||
|
let startup_dir = get_startup_dir()?;
|
||||||
|
let shortcut_path = startup_dir.join("Clash-Verge.lnk");
|
||||||
|
|
||||||
|
// 如果快捷方式不存在,直接返回成功
|
||||||
|
if !shortcut_path.exists() {
|
||||||
|
info!(target: "app", "启动快捷方式不存在,无需删除");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除快捷方式
|
||||||
|
fs::remove_file(&shortcut_path).map_err(|e| anyhow!("删除快捷方式失败: {}", e))?;
|
||||||
|
|
||||||
|
info!(target: "app", "成功删除启动快捷方式");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 检查快捷方式是否存在
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub fn is_shortcut_enabled() -> Result<bool> {
|
||||||
|
let startup_dir = get_startup_dir()?;
|
||||||
|
let shortcut_path = startup_dir.join("Clash-Verge.lnk");
|
||||||
|
|
||||||
|
Ok(shortcut_path.exists())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 非 Windows 平台使用的空方法
|
||||||
|
// #[cfg(not(target_os = "windows"))]
|
||||||
|
// pub fn create_shortcut() -> Result<()> {
|
||||||
|
// Ok(())
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[cfg(not(target_os = "windows"))]
|
||||||
|
// pub fn remove_shortcut() -> Result<()> {
|
||||||
|
// Ok(())
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[cfg(not(target_os = "windows"))]
|
||||||
|
// pub fn is_shortcut_enabled() -> Result<bool> {
|
||||||
|
// Ok(false)
|
||||||
|
// }
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
use crate::core::handle;
|
use crate::core::handle;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use std::fs;
|
use std::{fs, path::PathBuf};
|
||||||
use std::path::PathBuf;
|
|
||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
|
|
||||||
#[cfg(not(feature = "verge-dev"))]
|
#[cfg(not(feature = "verge-dev"))]
|
||||||
@@ -50,12 +49,60 @@ pub fn app_home_dir() -> Result<PathBuf> {
|
|||||||
.ok_or(anyhow::anyhow!("failed to get the portable app dir"))?;
|
.ok_or(anyhow::anyhow!("failed to get the portable app dir"))?;
|
||||||
return Ok(PathBuf::from(app_dir).join(".config").join(APP_ID));
|
return Ok(PathBuf::from(app_dir).join(".config").join(APP_ID));
|
||||||
}
|
}
|
||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
|
||||||
|
// 避免在Handle未初始化时崩溃
|
||||||
|
let app_handle = match handle::Handle::global().app_handle() {
|
||||||
|
Some(handle) => handle,
|
||||||
|
None => {
|
||||||
|
log::warn!(target: "app", "app_handle not initialized, using default path");
|
||||||
|
// 使用可执行文件目录作为备用
|
||||||
|
let exe_path = tauri::utils::platform::current_exe()?;
|
||||||
|
let exe_dir = exe_path
|
||||||
|
.parent()
|
||||||
|
.ok_or(anyhow::anyhow!("failed to get executable directory"))?;
|
||||||
|
|
||||||
|
// 使用系统临时目录 + 应用ID
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
if let Some(local_app_data) = std::env::var_os("LOCALAPPDATA") {
|
||||||
|
let path = PathBuf::from(local_app_data).join(APP_ID);
|
||||||
|
return Ok(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
if let Some(home) = std::env::var_os("HOME") {
|
||||||
|
let path = PathBuf::from(home)
|
||||||
|
.join("Library")
|
||||||
|
.join("Application Support")
|
||||||
|
.join(APP_ID);
|
||||||
|
return Ok(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
if let Some(home) = std::env::var_os("HOME") {
|
||||||
|
let path = PathBuf::from(home)
|
||||||
|
.join(".local")
|
||||||
|
.join("share")
|
||||||
|
.join(APP_ID);
|
||||||
|
return Ok(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果无法获取系统目录,则回退到可执行文件目录
|
||||||
|
let fallback_dir = PathBuf::from(exe_dir).join(".config").join(APP_ID);
|
||||||
|
log::warn!(target: "app", "Using fallback data directory: {:?}", fallback_dir);
|
||||||
|
return Ok(fallback_dir);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
match app_handle.path().data_dir() {
|
match app_handle.path().data_dir() {
|
||||||
Ok(dir) => Ok(dir.join(APP_ID)),
|
Ok(dir) => Ok(dir.join(APP_ID)),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!(target:"app", "Failed to get the app home directory: {}", e);
|
log::error!(target: "app", "Failed to get the app home directory: {}", e);
|
||||||
Err(anyhow::anyhow!("Failed to get the app homedirectory"))
|
Err(anyhow::anyhow!("Failed to get the app homedirectory"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,11 +110,24 @@ pub fn app_home_dir() -> Result<PathBuf> {
|
|||||||
|
|
||||||
/// get the resources dir
|
/// get the resources dir
|
||||||
pub fn app_resources_dir() -> Result<PathBuf> {
|
pub fn app_resources_dir() -> Result<PathBuf> {
|
||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
// 避免在Handle未初始化时崩溃
|
||||||
|
let app_handle = match handle::Handle::global().app_handle() {
|
||||||
|
Some(handle) => handle,
|
||||||
|
None => {
|
||||||
|
log::warn!(target: "app", "app_handle not initialized in app_resources_dir, using fallback");
|
||||||
|
// 使用可执行文件目录作为备用
|
||||||
|
let exe_dir = tauri::utils::platform::current_exe()?
|
||||||
|
.parent()
|
||||||
|
.ok_or(anyhow::anyhow!("failed to get executable directory"))?
|
||||||
|
.to_path_buf();
|
||||||
|
return Ok(exe_dir.join("resources"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
match app_handle.path().resource_dir() {
|
match app_handle.path().resource_dir() {
|
||||||
Ok(dir) => Ok(dir.join("resources")),
|
Ok(dir) => Ok(dir.join("resources")),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!(target:"app", "Failed to get the resource directory: {}", e);
|
log::error!(target: "app", "Failed to get the resource directory: {}", e);
|
||||||
Err(anyhow::anyhow!("Failed to get the resource directory"))
|
Err(anyhow::anyhow!("Failed to get the resource directory"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,6 +138,36 @@ pub fn app_profiles_dir() -> Result<PathBuf> {
|
|||||||
Ok(app_home_dir()?.join("profiles"))
|
Ok(app_home_dir()?.join("profiles"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// icons dir
|
||||||
|
pub fn app_icons_dir() -> Result<PathBuf> {
|
||||||
|
Ok(app_home_dir()?.join("icons"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_target_icons(target: &str) -> Result<Option<String>> {
|
||||||
|
let icons_dir = app_icons_dir()?;
|
||||||
|
let mut matching_files = Vec::new();
|
||||||
|
|
||||||
|
for entry in fs::read_dir(icons_dir)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let path = entry.path();
|
||||||
|
|
||||||
|
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
|
||||||
|
if file_name.starts_with(target)
|
||||||
|
&& (file_name.ends_with(".ico") || file_name.ends_with(".png"))
|
||||||
|
{
|
||||||
|
matching_files.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if matching_files.is_empty() {
|
||||||
|
Ok(None)
|
||||||
|
} else {
|
||||||
|
let first = path_to_str(matching_files.first().unwrap())?;
|
||||||
|
Ok(Some(first.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// logs dir
|
/// logs dir
|
||||||
pub fn app_logs_dir() -> Result<PathBuf> {
|
pub fn app_logs_dir() -> Result<PathBuf> {
|
||||||
Ok(app_home_dir()?.join("logs"))
|
Ok(app_home_dir()?.join("logs"))
|
||||||
@@ -97,12 +187,14 @@ pub fn profiles_path() -> Result<PathBuf> {
|
|||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
pub fn service_path() -> Result<PathBuf> {
|
pub fn service_path() -> Result<PathBuf> {
|
||||||
Ok(app_resources_dir()?.join("clash-verge-service"))
|
let res_dir = app_resources_dir()?;
|
||||||
|
Ok(res_dir.join("clash-verge-service"))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
pub fn service_path() -> Result<PathBuf> {
|
pub fn service_path() -> Result<PathBuf> {
|
||||||
Ok(app_resources_dir()?.join("clash-verge-service.exe"))
|
let res_dir = app_resources_dir()?;
|
||||||
|
Ok(res_dir.join("clash-verge-service.exe"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn service_log_file() -> Result<PathBuf> {
|
pub fn service_log_file() -> Result<PathBuf> {
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
use crate::log_err;
|
|
||||||
use anyhow;
|
|
||||||
use std::{
|
|
||||||
backtrace::{Backtrace, BacktraceStatus},
|
|
||||||
thread,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn redirect_panic_to_log() {
|
|
||||||
std::panic::set_hook(Box::new(move |panic_info| {
|
|
||||||
let thread = thread::current();
|
|
||||||
let thread_name = thread.name().unwrap_or("<unnamed>");
|
|
||||||
let payload = panic_info.payload();
|
|
||||||
|
|
||||||
let payload = if let Some(s) = payload.downcast_ref::<&str>() {
|
|
||||||
&**s
|
|
||||||
} else if let Some(s) = payload.downcast_ref::<String>() {
|
|
||||||
s
|
|
||||||
} else {
|
|
||||||
&format!("{:?}", payload)
|
|
||||||
};
|
|
||||||
|
|
||||||
let location = panic_info
|
|
||||||
.location()
|
|
||||||
.map(|l| l.to_string())
|
|
||||||
.unwrap_or("unknown location".to_string());
|
|
||||||
|
|
||||||
let backtrace = Backtrace::capture();
|
|
||||||
let backtrace = if backtrace.status() == BacktraceStatus::Captured {
|
|
||||||
&format!("stack backtrace:\n{}", backtrace)
|
|
||||||
} else {
|
|
||||||
"note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace"
|
|
||||||
};
|
|
||||||
|
|
||||||
let err: Result<(), anyhow::Error> = Err(anyhow::anyhow!(format!(
|
|
||||||
"thread '{}' panicked at {}:\n{}\n{}",
|
|
||||||
thread_name, location, payload, backtrace
|
|
||||||
)));
|
|
||||||
log_err!(err);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
use crate::enhance::seq::SeqMap;
|
use crate::{enhance::seq::SeqMap, logging, utils::logging::Type};
|
||||||
use anyhow::{anyhow, bail, Context, Result};
|
use anyhow::{anyhow, bail, Context, Result};
|
||||||
use nanoid::nanoid;
|
use nanoid::nanoid;
|
||||||
use serde::{de::DeserializeOwned, Serialize};
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
use serde_yaml::{Mapping, Value};
|
use serde_yaml::Mapping;
|
||||||
use std::{fs, path::PathBuf, str::FromStr};
|
use std::{fs, path::PathBuf, str::FromStr};
|
||||||
|
|
||||||
/// read data from yaml as struct T
|
/// read data from yaml as struct T
|
||||||
@@ -22,19 +22,41 @@ pub fn read_yaml<T: DeserializeOwned>(path: &PathBuf) -> Result<T> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// read mapping from yaml fix #165
|
/// read mapping from yaml
|
||||||
pub fn read_mapping(path: &PathBuf) -> Result<Mapping> {
|
pub fn read_mapping(path: &PathBuf) -> Result<Mapping> {
|
||||||
let mut val: Value = read_yaml(path)?;
|
if !path.exists() {
|
||||||
val.apply_merge()
|
bail!("file not found \"{}\"", path.display());
|
||||||
.with_context(|| format!("failed to apply merge \"{}\"", path.display()))?;
|
}
|
||||||
|
|
||||||
Ok(val
|
let yaml_str = fs::read_to_string(path)
|
||||||
.as_mapping()
|
.with_context(|| format!("failed to read the file \"{}\"", path.display()))?;
|
||||||
.ok_or(anyhow!(
|
|
||||||
"failed to transform to yaml mapping \"{}\"",
|
// YAML语法检查
|
||||||
path.display()
|
match serde_yaml::from_str::<serde_yaml::Value>(&yaml_str) {
|
||||||
))?
|
Ok(mut val) => {
|
||||||
.to_owned())
|
val.apply_merge()
|
||||||
|
.with_context(|| format!("failed to apply merge \"{}\"", path.display()))?;
|
||||||
|
|
||||||
|
Ok(val
|
||||||
|
.as_mapping()
|
||||||
|
.ok_or(anyhow!(
|
||||||
|
"failed to transform to yaml mapping \"{}\"",
|
||||||
|
path.display()
|
||||||
|
))?
|
||||||
|
.to_owned())
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
let error_msg = format!("YAML syntax error in {}: {}", path.display(), err);
|
||||||
|
logging!(error, Type::Config, true, "{}", error_msg);
|
||||||
|
|
||||||
|
crate::core::handle::Handle::notice_message(
|
||||||
|
"config_validate::yaml_syntax_error",
|
||||||
|
&error_msg,
|
||||||
|
);
|
||||||
|
|
||||||
|
bail!("YAML syntax error: {}", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// read mapping from yaml fix #165
|
/// read mapping from yaml fix #165
|
||||||
@@ -136,52 +158,6 @@ pub fn linux_elevator() -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! error {
|
|
||||||
($result: expr) => {
|
|
||||||
log::error!(target: "app", "{}", $result);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! log_err {
|
|
||||||
($result: expr) => {
|
|
||||||
if let Err(err) = $result {
|
|
||||||
log::error!(target: "app", "{err}");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
($result: expr, $err_str: expr) => {
|
|
||||||
if let Err(_) = $result {
|
|
||||||
log::error!(target: "app", "{}", $err_str);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! trace_err {
|
|
||||||
($result: expr, $err_str: expr) => {
|
|
||||||
if let Err(err) = $result {
|
|
||||||
log::trace!(target: "app", "{}, err {}", $err_str, err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// wrap the anyhow error
|
|
||||||
/// transform the error to String
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! wrap_err {
|
|
||||||
($stat: expr) => {
|
|
||||||
match $stat {
|
|
||||||
Ok(a) => Ok(a),
|
|
||||||
Err(err) => {
|
|
||||||
log::error!(target: "app", "{}", err.to_string());
|
|
||||||
Err(format!("{}", err.to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// return the string literal error
|
/// return the string literal error
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! ret_err {
|
macro_rules! ret_err {
|
||||||
@@ -207,28 +183,29 @@ macro_rules! t {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```not_run
|
/// ```not_run
|
||||||
/// format_bytes_speed(1000) // returns "1000B/s"
|
/// format_bytes_speed(1000) // returns "1000B/s"
|
||||||
/// format_bytes_speed(1024) // returns "1.0KB/s"
|
/// format_bytes_speed(1024) // returns "1.0KB/s"
|
||||||
/// format_bytes_speed(1024 * 1024) // returns "1.0MB/s"
|
/// format_bytes_speed(1024 * 1024) // returns "1.0MB/s"
|
||||||
/// ```
|
/// ```
|
||||||
/// ```
|
/// ```
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
pub fn format_bytes_speed(speed: u64) -> String {
|
pub fn format_bytes_speed(speed: u64) -> String {
|
||||||
if speed < 1024 {
|
const UNITS: [&str; 4] = ["B", "KB", "MB", "GB"];
|
||||||
format!("{}B/s", speed)
|
let mut size = speed as f64;
|
||||||
} else if speed < 1024 * 1024 {
|
let mut unit_index = 0;
|
||||||
format!("{:.1}KB/s", speed as f64 / 1024.0)
|
|
||||||
} else if speed < 1024 * 1024 * 1024 {
|
while size >= 1000.0 && unit_index < UNITS.len() - 1 {
|
||||||
format!("{:.1}MB/s", speed as f64 / 1024.0 / 1024.0)
|
size /= 1024.0;
|
||||||
} else {
|
unit_index += 1;
|
||||||
format!("{:.1}GB/s", speed as f64 / 1024.0 / 1024.0 / 1024.0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
format!("{:.1}{}/s", size, UNITS[unit_index])
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
#[test]
|
#[test]
|
||||||
fn test_format_bytes_speed() {
|
fn test_format_bytes_speed() {
|
||||||
assert_eq!(format_bytes_speed(0), "0B/s");
|
assert_eq!(format_bytes_speed(0), "0.0B/s");
|
||||||
assert_eq!(format_bytes_speed(1023), "1023B/s");
|
assert_eq!(format_bytes_speed(1023), "1.0KB/s");
|
||||||
assert_eq!(format_bytes_speed(1024), "1.0KB/s");
|
assert_eq!(format_bytes_speed(1024), "1.0KB/s");
|
||||||
assert_eq!(format_bytes_speed(1024 * 1024), "1.0MB/s");
|
assert_eq!(format_bytes_speed(1024 * 1024), "1.0MB/s");
|
||||||
assert_eq!(format_bytes_speed(1024 * 1024 * 1024), "1.0GB/s");
|
assert_eq!(format_bytes_speed(1024 * 1024 * 1024), "1.0GB/s");
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user