mirror of
https://github.com/clash-verge-rev/clash-verge-rev
synced 2025-05-05 04:33:45 +08:00
Compare commits
2447 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
84fbccbfd9 | ||
|
49c81f6201 | ||
|
c894a15d13 | ||
|
196b887381 | ||
|
2ad20ed239 | ||
|
98de91771e | ||
|
9dfd9bad20 | ||
|
0b8d08d13b | ||
|
0de304d4e3 | ||
|
55f1766ebc | ||
|
6d1a8fb264 | ||
|
d3958594d9 | ||
|
b5952f320b | ||
|
98be9621a6 | ||
|
e4eb13ce22 | ||
|
ecf2da7c0a | ||
|
7d7c8988d7 | ||
|
5e11d36972 | ||
|
3039f39d40 | ||
|
7b5fd104de | ||
|
30ea408019 | ||
|
3fa130695c | ||
|
fece31438a | ||
|
ad1f2bea3b | ||
|
cd4bfdd743 | ||
|
cde8c4004f | ||
|
c53514e060 | ||
|
8e99672265 | ||
|
5b9b5cb6a8 | ||
|
62141380d8 | ||
|
52a15bb281 | ||
|
b092f74c88 | ||
|
937f43c270 | ||
|
9b02088918 | ||
|
1bd503a654 | ||
|
c6477dfda4 | ||
|
4831d88467 | ||
|
9ebde802d4 | ||
|
a9cccc7b97 | ||
|
d54a765bd6 | ||
|
5a2751162f | ||
|
492a5a6de7 | ||
|
f6c0f144a6 | ||
|
1c046f3ca3 | ||
|
4a47c5bb6f | ||
|
b7e01aefb4 | ||
|
e8e16f7d57 | ||
|
59caa22431 | ||
|
e2046f3e48 | ||
|
8fdcffc731 | ||
|
3ec77c6256 | ||
|
b09313756e | ||
|
f800e2e3b6 | ||
|
daad623855 | ||
|
7716e2bc87 | ||
|
70bd5ec03c | ||
|
ce5c86c3b0 | ||
|
a6a6d9d036 | ||
|
971dd6a2cf | ||
|
42d0ea7e36 | ||
|
006bcffe8c | ||
|
ff4101fa47 | ||
|
7280635741 | ||
|
7ede91599c | ||
|
6e40dd9862 | ||
|
42db9ea0bb | ||
|
ca0cf4552c | ||
|
d91653b218 | ||
|
1ace560531 | ||
|
81968a579d | ||
|
5a0eb56f70 | ||
|
804fad6083 | ||
|
98d3a48710 | ||
|
0ec4f46052 | ||
|
a891341e35 | ||
|
10426af3ad | ||
|
5be1d604ee | ||
|
1baa840160 | ||
|
14347f60d5 | ||
|
df5424d55e | ||
|
12065330e1 | ||
|
e054ac67fb | ||
|
31a7750482 | ||
|
2e38307f65 | ||
|
47accdd2b1 | ||
|
cb0146573f | ||
|
cf78bb3686 | ||
|
b5b5ae4e7b | ||
|
b99bc7fcd1 | ||
|
f50fe9159d | ||
|
09f6917638 | ||
|
e330d75a89 | ||
|
4f0ce7458e | ||
|
a2811c4803 | ||
|
1c233783a7 | ||
|
3e45cc4650 | ||
|
1a7c076e07 | ||
|
da4fddf150 | ||
|
970eb62aa6 | ||
|
d669650758 | ||
|
69347160e9 | ||
|
a345b54a77 | ||
|
8aabcd77a5 | ||
|
c30f54609d | ||
|
0830236a73 | ||
|
44f21444bb | ||
|
1d88d98ea1 | ||
|
86f69fd574 | ||
|
e21846a2ce | ||
|
d5981ca94f | ||
|
8c5eb3b550 | ||
|
55dc416109 | ||
|
ec30b888d1 | ||
|
2a92755e65 | ||
|
6976ea3c09 | ||
|
b07ed2dbf5 | ||
|
2ab923da87 | ||
|
9799d4f747 | ||
|
f739836891 | ||
|
a28887be8e | ||
|
0f13691ae0 | ||
|
ae72b83dbe | ||
|
2e38404434 | ||
|
11b8c8be45 | ||
|
a06597a3a6 | ||
|
108840c4be | ||
|
16c8672aeb | ||
|
167edcf8ef | ||
|
d6dd89b674 | ||
|
fac2ee6374 | ||
|
dd7876845a | ||
|
56e6139c2b | ||
|
04bdd48a2a | ||
|
5b47fe5b88 | ||
|
84a5cf6b89 | ||
|
618ba52bca | ||
|
5c0cde517f | ||
|
1b249564a3 | ||
|
81b5501b0e | ||
|
91ccb3045c | ||
|
e31f176c25 | ||
|
ad45485009 | ||
|
25e5cf2ac2 | ||
|
bd58d935c6 | ||
|
da2705ff7d | ||
|
61f019f194 | ||
|
74e441df5b | ||
|
772ecdd3b0 | ||
|
baa535b609 | ||
|
a2ff0a7e20 | ||
|
84732f9835 | ||
|
dd17bcb0d6 | ||
|
cab8e613a6 | ||
|
fe1227618a | ||
|
596c52de87 | ||
|
ba5d5e9f86 | ||
|
530669d288 | ||
|
70b0f9a03a | ||
|
105de99d06 | ||
|
6239f81f36 | ||
|
697d200ffe | ||
|
16d5077f55 | ||
|
e0e1a05448 | ||
|
bcaafa67a3 | ||
|
36142656a4 | ||
|
d6a48deb5a | ||
|
e98ce0c2ae | ||
|
8118fc754c | ||
|
1ec7a0f23c | ||
|
488e8ef1d5 | ||
|
1f99cee78b | ||
|
c25015ed54 | ||
|
aaefc5b479 | ||
|
1c58816c73 | ||
|
0fd99358aa | ||
|
d4012bace9 | ||
|
af7660686d | ||
|
b57c6e408a | ||
|
124934b012 | ||
|
1bef6d085d | ||
|
c73927c5ba | ||
|
692deb6012 | ||
|
8ec499f631 | ||
|
2bcd653a56 | ||
|
0f10952979 | ||
|
58fa67100f | ||
|
8e294916c4 | ||
|
6877e0c95d | ||
|
37a333a023 | ||
|
48f1da963a | ||
|
e1905aced4 | ||
|
f18202a3a4 | ||
|
c1a9de4d66 | ||
|
18f86874ee | ||
|
e6686e0b82 | ||
|
4bf166986d | ||
|
0f60d84f6c | ||
|
15e54df67c | ||
|
4cb6ad7736 | ||
|
e27a32395a | ||
|
eddcf209c1 | ||
|
10a151d411 | ||
|
54d5586a60 | ||
|
30ca547e50 | ||
|
a1944d1a90 | ||
|
c2b35fdaa5 | ||
|
805b54d81e | ||
|
e3579dac65 | ||
|
f80591242e | ||
|
69cb9769c1 | ||
|
efd42d9da0 | ||
|
21a6340095 | ||
|
6c96724dce | ||
|
ebb194d2a2 | ||
|
1a51a92b70 | ||
|
5760f16272 | ||
|
4ed36f6223 | ||
|
7ea7ca1415 | ||
|
1ee8786ab7 | ||
|
44ca513241 | ||
|
73310b466b | ||
|
1ba688727e | ||
|
3b69465016 | ||
|
3e53ea7209 | ||
|
07bdc108ed | ||
|
a18efb0e71 | ||
|
de1c825ad3 | ||
|
de2cff824e | ||
|
aff504bddc | ||
|
277390e597 | ||
|
fdcefe458e | ||
|
9bb2160abe | ||
|
97d683541d | ||
|
9f7ffb80e1 | ||
|
c957ea7b24 | ||
|
181fce16b1 | ||
|
825f023505 | ||
|
347ea53b32 | ||
|
d525e0dd70 | ||
|
365e844b83 | ||
|
44bdeb555a | ||
|
520c33557e | ||
|
bfad5ac091 | ||
|
3ecf4bc238 | ||
|
028e4012aa | ||
|
dc6d429b9c | ||
|
625cf1a803 | ||
|
9ee011514a | ||
|
184fd4a1ba | ||
|
23dcfd9401 | ||
|
1cdba297fb | ||
|
1ed6743bbb | ||
|
41c42bba32 | ||
|
8fb66ea32c | ||
|
51d4c1c4a5 | ||
|
86e069994e | ||
|
1cb923b6d8 | ||
|
b1d003b073 | ||
|
0fb4254481 | ||
|
ae7f456011 | ||
|
cd4bec6bfd | ||
|
5906e0126d | ||
|
dce1395af1 | ||
|
e7db13af37 | ||
|
2eee8cd7d3 | ||
|
836a2abae1 | ||
|
a68a86d6db | ||
|
ee9f0990fd | ||
|
59d0629e3f | ||
|
17af292761 | ||
|
a4dd4bcc8a | ||
|
1a9b0a476b | ||
|
bb015506e7 | ||
|
76be5d8469 | ||
|
1258e187f5 | ||
|
4056a4c35f | ||
|
b6677f0f72 | ||
|
e8c1e6f241 | ||
|
618595ac4c | ||
|
d54ba48c11 | ||
|
a489012a0c | ||
|
a5acdc04e3 | ||
|
709a20ed7b | ||
|
c88f2099c1 | ||
|
f72a2a943b | ||
|
c51199719d | ||
|
bf374f2e85 | ||
|
34f450fcdb | ||
|
23f75598e5 | ||
|
afc238d60e | ||
|
1291c38d58 | ||
|
16caccde51 | ||
|
33f199fcd2 | ||
|
2b534e0d51 | ||
|
23d1d210c7 | ||
|
39a1d6202a | ||
|
f00a5af6c9 | ||
|
48d68f5766 | ||
|
f948da748e | ||
|
8b25d45109 | ||
|
c4ddc35746 | ||
|
0122e2bdcf | ||
|
94b75f463b | ||
|
3c2e04290c | ||
|
f0331ec2d9 | ||
|
3b4013a1b0 | ||
|
31ddccd3e1 | ||
|
d29fe4cb6c | ||
|
6763537f22 | ||
|
8ab4bd6293 | ||
|
31bc644763 | ||
|
fcd672abeb | ||
|
5f550da0bb | ||
|
e865a86eef | ||
|
80ee2e4289 | ||
|
4d327594d3 | ||
|
3363c37457 | ||
|
cee9be81bf | ||
|
932d36462f | ||
|
75c930f7ef | ||
|
bdb178d893 | ||
|
3b0635e8a1 | ||
|
f5760784bf | ||
|
67f3554095 | ||
|
3bb3872e38 | ||
|
c98330ea1f | ||
|
d895b68f04 | ||
|
e230981ac4 | ||
|
20763a741a | ||
|
4babcd9442 | ||
|
d75f36066a | ||
|
26ca4670ad | ||
|
c4d6c167a2 | ||
|
e5af9541da | ||
|
89d9f47191 | ||
|
ff2cf30238 | ||
|
ebe0899eb1 | ||
|
a3d0a38b1e | ||
|
63bd0c87b2 | ||
|
db593fb188 | ||
|
67ae10b593 | ||
|
c8d91c9e14 | ||
|
6c54f5e9b4 | ||
|
8749648d97 | ||
|
0b75b5ef26 | ||
|
fbcadd0493 | ||
|
75bb7a4dd7 | ||
|
4604fe4841 | ||
|
e8badb0c0f | ||
|
8906a8f3c6 | ||
|
4e32990a5d | ||
|
9f2583d1f2 | ||
|
a9b3d8885d | ||
|
29ec4dc546 | ||
|
d0d5204cbc | ||
|
3d84acd7ac | ||
|
5057221f59 | ||
|
3ddfbc5d2f | ||
|
362270e3ea | ||
|
db91177e90 | ||
|
d2f51ce509 | ||
|
146a66fb09 | ||
|
03305f03c1 | ||
|
82e76bc58e | ||
|
e8ff6c785a | ||
|
a8fafb469a | ||
|
b9a220cb63 | ||
|
0b44d40b39 | ||
|
6b349eda45 | ||
|
ad335ba005 | ||
|
04d766884a | ||
|
44d1ec433d | ||
|
97864e8df3 | ||
|
3916293e8f | ||
|
a527177b67 | ||
|
9c027b10b2 | ||
|
5da7086475 | ||
|
0006012ae7 | ||
|
a3f46ec037 | ||
|
bfea52f9dd | ||
|
53334f05b8 | ||
|
ba195c41b6 | ||
|
cca2f1ce61 | ||
|
a51191c661 | ||
|
4cdb5f93b9 | ||
|
fae658c9c2 | ||
|
886a469634 | ||
|
c3c1394e86 | ||
|
6a00255fff | ||
|
4f0aae0879 | ||
|
4f4fe4c41c | ||
|
2a4a3c8250 | ||
|
7864acbadb | ||
|
ba18e64be0 | ||
|
ba8c1e5eb2 | ||
|
2737fb2d87 | ||
|
899285735f | ||
|
f561d12d35 | ||
|
be258b13e0 | ||
|
dbce6b5f1a | ||
|
30f0c99a58 | ||
|
49880c05d9 | ||
|
dc2fc84f58 | ||
|
78c2a1694f | ||
|
baf34dd0d3 | ||
|
48f9dede7b | ||
|
a1f2a621ef | ||
|
a1e8ddb461 | ||
|
d33d90a36e | ||
|
c3114b876f | ||
|
50285aebde | ||
|
0eb5ee6ea8 | ||
|
16c9c95e19 | ||
|
3bc4da3e85 | ||
|
a028a2e1cc | ||
|
9675a35dff | ||
|
c1546fdd64 | ||
|
a109efc1d6 | ||
|
0782b25830 | ||
|
0041ff13b8 | ||
|
4693a25aa0 | ||
|
609df5b4a6 | ||
|
6df8140cb1 | ||
|
65b4cb3191 | ||
|
e1de481349 | ||
|
6e1cc80b91 | ||
|
b658ce7e75 | ||
|
f91f374dfa | ||
|
56f6de5410 | ||
|
94d22ecfc3 | ||
|
e25d71c6c8 | ||
|
bb1b156d2f | ||
|
1b80ddf1e9 | ||
|
66d2fe9074 | ||
|
6e36910734 | ||
|
a553a33c46 | ||
|
2a6f8b401b | ||
|
fa30567140 | ||
|
243f685b83 | ||
|
6cf2373b34 | ||
|
e842ea745a | ||
|
9696c7cec0 | ||
|
c4986eec50 | ||
|
61079e769e | ||
|
00d2c915d1 | ||
|
6ad975c420 | ||
|
1cd1a2d907 | ||
|
39a3c3d3a7 | ||
|
dfefcf03ad | ||
|
825b00e618 | ||
|
ae562e1e92 | ||
|
21c7888595 | ||
|
3b87a4f9d0 | ||
|
8564a58eab | ||
|
c5d009c2cd | ||
|
c2e165d825 | ||
|
922020c57a | ||
|
8cdc33beab | ||
|
23eafdfe00 | ||
|
e72e8ea631 | ||
|
a610a43db0 | ||
|
4a90ffe619 | ||
|
18e8357b6a | ||
|
df39347b19 | ||
|
a36261d705 | ||
|
f133d22124 | ||
|
6ba276b43f | ||
|
44db98f260 | ||
|
8873526619 | ||
|
37c2599754 | ||
|
a079b470b8 | ||
|
0f9ed02bf0 | ||
|
9aeb68205c | ||
|
82b4cf259c | ||
|
566fd3e88b | ||
|
fbecf4f47b | ||
|
52899d4def | ||
|
a89a828b35 | ||
|
4d0dbdaced | ||
|
8003f9902e | ||
|
15bd7324fe | ||
|
bb44fc51bd | ||
|
67a32e60c7 | ||
|
960725777c | ||
|
98c6e0311b | ||
|
95b7641f9c | ||
|
18b0c3f7aa | ||
|
49d3644d6a | ||
|
ee9d12d933 | ||
|
5d37015f4d | ||
|
dca25637c9 | ||
|
a7020fd46c | ||
|
1ef2b1aaf1 | ||
|
a7a661e60f | ||
|
2a9e2d47f5 | ||
|
e33b3043df | ||
|
3f41618aa1 | ||
|
a507d7567f | ||
|
62a6f58705 | ||
|
77dd074fc3 | ||
|
e8c0051be3 | ||
|
b3923eafc7 | ||
|
9ebd96611a | ||
|
824325a2eb | ||
|
e8b3bd5bdc | ||
|
a59fda512c | ||
|
ae181f6835 | ||
|
67bb242778 | ||
|
2028c189aa | ||
|
ba0dc4fb81 | ||
|
c40db417d2 | ||
|
0eb776cdd3 | ||
|
c79a7a7f6f | ||
|
1e3c995e6a | ||
|
3f79e42628 | ||
|
c16ae89a3d | ||
|
7132eaeb11 | ||
|
aef96f0d27 | ||
|
b20a56f1de | ||
|
3073b4e48e | ||
|
7b53752ccd | ||
|
03eedf6175 | ||
|
2330a4bc93 | ||
|
36afae50b1 | ||
|
272ee7577c | ||
|
7f34073da6 | ||
|
f46ee2a0a3 | ||
|
4a79f0c75d | ||
|
27a78af269 | ||
|
586af67829 | ||
|
575d8c4240 | ||
|
d32734214b | ||
|
22ce5aab25 | ||
|
9f90a1c58e | ||
|
b5e0374946 | ||
|
44cb1c7f3e | ||
|
80aba859e7 | ||
|
08360edd26 | ||
|
6e69b3f032 | ||
|
19bb9c7f50 | ||
|
f5dee51e9c | ||
|
bd37fef720 | ||
|
c22e4e5e2c | ||
|
2887a2b6d3 | ||
|
01bde19701 | ||
|
792f1826ee | ||
|
c16795dce9 | ||
|
c1597a0968 | ||
|
590aa950df | ||
|
402018b95c | ||
|
d5101ac2f3 | ||
|
251942c91d | ||
|
fe86b812cd | ||
|
cb3bff589f | ||
|
ec7d7ec559 | ||
|
24c7a5b805 | ||
|
0a4ecb1507 | ||
|
d736dace50 | ||
|
70bbab909f | ||
|
6625f78e4f | ||
|
da2b4c8858 | ||
|
12df415dfd | ||
|
2493f463f3 | ||
|
f4238b1fb9 | ||
|
794783ab4e | ||
|
02634622a5 | ||
|
ac24501e76 | ||
|
e40ea38112 | ||
|
b809b9bb80 | ||
|
73bad8f355 | ||
|
ac884da56b | ||
|
c35ab2e1cd | ||
|
ed3907c273 | ||
|
5e00287045 | ||
|
95c6578911 | ||
|
d22097ee33 | ||
|
74251af163 | ||
|
7c1b11851f | ||
|
f8724c4cb9 | ||
|
3baac034e5 | ||
|
114f1426f3 | ||
|
7de63cea5c | ||
|
34e3af2b38 | ||
|
da907d0eea | ||
|
9cbc2d9206 | ||
|
db2e466d60 | ||
|
e5cdbf7361 | ||
|
a919f493d6 | ||
|
3660298683 | ||
|
c845efe475 | ||
|
38eb47132d | ||
|
b1f097f32b | ||
|
d3123253b3 | ||
|
7b1ec1ec22 | ||
|
4cefacfe73 | ||
|
978acfa471 | ||
|
fb2d138cbf | ||
|
0edd63edb5 | ||
|
97b730668c | ||
|
26b8cf6d52 | ||
|
a979638368 | ||
|
97f434ad4a | ||
|
34af040c48 | ||
|
cc81b443be | ||
|
d44f3c22c7 | ||
|
3795b537f6 | ||
|
ecb5f0885c | ||
|
86d2234713 | ||
|
62ddf26150 | ||
|
ec14b7c52f | ||
|
5f9cc38e82 | ||
|
f48c58f299 | ||
|
c2843f3c4b | ||
|
5d33df4e12 | ||
|
c030fb47ca | ||
|
964daadb18 | ||
|
71a5698ac7 | ||
|
d41d74d0f8 | ||
|
f6c7a611a3 | ||
|
06f4e79e5c | ||
|
154cf44f0a | ||
|
f6e2ff0e44 | ||
|
2aba616f7f | ||
|
95e21386b8 | ||
|
250e908d9a | ||
|
9742fb296c | ||
|
9ff3c2c0d4 | ||
|
9dd7bd9530 | ||
|
6f477b7147 | ||
|
8389826e30 | ||
|
aa31fb7470 | ||
|
a013fe663c | ||
|
89ce497431 | ||
|
60c0b649e8 | ||
|
2bbb5ea23b | ||
|
4f9c1533c1 | ||
|
a6a3847e30 | ||
|
118f38dba3 | ||
|
879f946b28 | ||
|
6acb8a5a91 | ||
|
a38f1e92e3 | ||
|
4ca977466e | ||
|
ec45dc56fb | ||
|
5686302653 | ||
|
6322773513 | ||
|
8e845fc919 | ||
|
f90c8f2ae5 | ||
|
b5af06529f | ||
|
fac3669f8e | ||
|
28ff8d6dcc | ||
|
d0e7f6673c | ||
|
800dc21202 | ||
|
f52089a674 | ||
|
82543de95e | ||
|
12db69407e | ||
|
9b2b447b8b | ||
|
35f5e4ca41 | ||
|
8a69713f6c | ||
|
efd8ef0380 | ||
|
c5eacd1627 | ||
|
5fdb52d8d0 | ||
|
7869ce060f | ||
|
e0d96c0ce1 | ||
|
30678904ee | ||
|
953be61d89 | ||
|
d8c85007d4 | ||
|
591c1cb454 | ||
|
0ca90ed082 | ||
|
4c963b3978 | ||
|
071665f0c3 | ||
|
9a7826752f | ||
|
44b4187365 | ||
|
148807543f | ||
|
2b9fa09293 | ||
|
10211d1d03 | ||
|
46811f33ad | ||
|
32b16790d3 | ||
|
10592ca5a8 | ||
|
dc5cb2e1b8 | ||
|
c10d782524 | ||
|
d73366984f | ||
|
60b1e47ae6 | ||
|
9591fb2c21 | ||
|
c3cba03ac6 | ||
|
ce7818c436 | ||
|
f367a81e44 | ||
|
de507f7ec9 | ||
|
0a8be603c8 | ||
|
d7f033bd46 | ||
|
1fb3b87697 | ||
|
b9c8fa61b2 | ||
|
99ea6d5080 | ||
|
0bacfa9286 | ||
|
b350b605a8 | ||
|
d1eeeab7b1 | ||
|
54296ba84a | ||
|
f82b0f259c | ||
|
57f1c005e6 | ||
|
d9e5387bff | ||
|
3154b8ce55 | ||
|
45b48ede44 | ||
|
961b86dcd2 | ||
|
4d57c64b0d | ||
|
1c894f3cfa | ||
|
3d6faecaed | ||
|
2263ade187 | ||
|
2cdf33d8a1 | ||
|
f6fce6bd31 | ||
|
f9f1721d66 | ||
|
0792ac7de8 | ||
|
98edb048b7 | ||
|
52fcdf28fa | ||
|
d9671faca7 | ||
|
f456004543 | ||
|
a38040d0ea | ||
|
f18cd92318 | ||
|
06e1d0f8da | ||
|
dffd663d7a | ||
|
414f9e9e96 | ||
|
c17ea74856 | ||
|
9a08740e5b | ||
|
8bea0db843 | ||
|
2c612e371f | ||
|
2f61dc9bc6 | ||
|
d18b78c11c | ||
|
95fb4f2e50 | ||
|
f31fe1440d | ||
|
10cd0a1f30 | ||
|
659f854f62 | ||
|
4a7f8dbe09 | ||
|
3e19e574e6 | ||
|
2af2d3664f | ||
|
7dad46adb4 | ||
|
b6fc6a751a | ||
|
934674a8d7 | ||
|
c66986f065 | ||
|
fc49e4a0da | ||
|
b6e1d71b81 | ||
|
b3626a786d | ||
|
a398e28ac0 | ||
|
892d4e597e | ||
|
e8311dd306 | ||
|
a0b266fef8 | ||
|
983d1ea361 | ||
|
db615b932c | ||
|
07415e512f | ||
|
0c6d417d8c | ||
|
772e01ad40 | ||
|
1b2509d5bc | ||
|
fd963a8e66 | ||
|
3c17fca369 | ||
|
4a76997044 | ||
|
894960ef5a | ||
|
3c24d4bc4e | ||
|
ed7e6a3495 | ||
|
07de032e62 | ||
|
c07165531a | ||
|
b1a22d4412 | ||
|
6734e5ef57 | ||
|
6cc81fe6b8 | ||
|
ad80d21e89 | ||
|
97689c6cbb | ||
|
41c83dabde | ||
|
f909f0dcf9 | ||
|
5c9bf30c79 | ||
|
6efc518eed | ||
|
198bd3a3dc | ||
|
7b1055702b | ||
|
d21bcce3c4 | ||
|
b5a26941ef | ||
|
6ab7131378 | ||
|
22bcc2e438 | ||
|
32edc0f1fe | ||
|
8faa0ce2c2 | ||
|
bf05e5999b | ||
|
3d7bdded31 | ||
|
7507182097 | ||
|
6853b3c531 | ||
|
f2372a13e8 | ||
|
3f321c8801 | ||
|
8b47107df8 | ||
|
97e7136293 | ||
|
c8db58150e | ||
|
5e1067df59 | ||
|
175444c59f | ||
|
b09d5ff3c9 | ||
|
43fc97137e | ||
|
c67eee57d6 | ||
|
607aa78059 | ||
|
7edbae7b4c | ||
|
232ff38084 | ||
|
d9d9ca67cd | ||
|
57fa48aef4 | ||
|
5a8e0749c2 | ||
|
f834f069cd | ||
|
9b2dc10da2 | ||
|
c3730b7efd | ||
|
23499497a3 | ||
|
3b78d609b7 | ||
|
65529c3356 | ||
|
899849d4dc | ||
|
52393206e6 | ||
|
2cd1fa6601 | ||
|
e371bbedc0 | ||
|
9dde385073 | ||
|
afa3d39cb3 | ||
|
fe41817f25 | ||
|
a865465514 | ||
|
9278e74e9e | ||
|
689a1f739f | ||
|
19dee57b7e | ||
|
8690b91632 | ||
|
fa31cab11b | ||
|
46ee783f99 | ||
|
199bba5da4 | ||
|
16e8791472 | ||
|
1728442d62 | ||
|
22f7f059ce | ||
|
97629c1fc3 | ||
|
4727d613c0 | ||
|
9ed138ea2b | ||
|
2cbd998941 | ||
|
806d70c243 | ||
|
74a1c7d489 | ||
|
f6ed5dc126 | ||
|
e25185b9b8 | ||
|
5e6d8873b9 | ||
|
951b48c337 | ||
|
7f209b76bf | ||
|
890bfbe02d | ||
|
70f8c28ca6 | ||
|
47bacdaed0 | ||
|
bcd8eb2a09 | ||
|
e4855d0143 | ||
|
94f0ff1ed1 | ||
|
b5f0243a89 | ||
|
17e59b8783 | ||
|
ffdf308b40 | ||
|
cdadc80945 | ||
|
294e1f5b10 | ||
|
1c5eab6055 | ||
|
f74f06e403 | ||
|
f04ee0baf2 | ||
|
689273fc24 | ||
|
6f4c59a15c | ||
|
dc87097dfe | ||
|
24f4ab7597 | ||
|
ad94f0a292 | ||
|
695613a063 | ||
|
6f1828eabc | ||
|
bf158b3bf0 | ||
|
13618e6a0a | ||
|
c424e9dec8 | ||
|
a2e9523707 | ||
|
606817ae06 | ||
|
7124d326fc | ||
|
f9f4653e33 | ||
|
bf8eebe537 | ||
|
bd9eef6502 | ||
|
e343b1790e | ||
|
d81ef1d67c | ||
|
6e374bcd4e | ||
|
fb4648d2af | ||
|
a63fc25f14 | ||
|
5e20e9ae1c | ||
|
7d5d604ea6 | ||
|
a722581868 | ||
|
28f3044bdd | ||
|
497804434d | ||
|
d2d6ee806d | ||
|
2e106265f9 | ||
|
28fb0b433b | ||
|
171bd6b327 | ||
|
198e215d54 | ||
|
4d424e70bc | ||
|
3efef52398 | ||
|
b85929772e | ||
|
041522f94e | ||
|
80d3c9e96f | ||
|
6ee5e560cc | ||
|
2be9eb4bae | ||
|
0a8935686a | ||
|
0109d9148b | ||
|
212518c682 | ||
|
59b4f1ebab | ||
|
ee9462c221 | ||
|
c648dc6c99 | ||
|
4f1b8094a3 | ||
|
c89ccf7185 | ||
|
753395965a | ||
|
04be747d52 | ||
|
f828ed3edf | ||
|
b98d9c2932 | ||
|
aba2ce8390 | ||
|
8bd8e149cf | ||
|
e7c359a2e7 | ||
|
d64d25380a | ||
|
6cba6166fb | ||
|
e66f5fe253 | ||
|
1d4388d444 | ||
|
28ab08a7ca | ||
|
6fa0f92ceb | ||
|
3083ab74a6 | ||
|
b6ea73af83 | ||
|
1fa3ffb1ff | ||
|
af89630095 | ||
|
18f0177fce | ||
|
d89eecacba | ||
|
a9149fb92e | ||
|
9a04208a11 | ||
|
6cdf199531 | ||
|
44dc7fe24a | ||
|
455892b414 | ||
|
4f5227782a | ||
|
481e473b60 | ||
|
2c2a1f638b | ||
|
973e269f46 | ||
|
9f76e0e056 | ||
|
3a5f1b41a4 | ||
|
e2d8369daf | ||
|
a20d4959bf | ||
|
7b887e4cdd | ||
|
bc9cbd2993 | ||
|
9baa0e247f | ||
|
2df8d2bc69 | ||
|
b8165fb06e | ||
|
c698b24e01 | ||
|
e70249cb2e | ||
|
8a9bfe8281 | ||
|
8c2a4e627e | ||
|
bf35c92c14 | ||
|
019293a034 | ||
|
46dc40149e | ||
|
444643eb6f | ||
|
2913b911e3 | ||
|
ca323371a7 | ||
|
a06cb39777 | ||
|
b0ec8767a2 | ||
|
353fb49a87 | ||
|
e453b40e0b | ||
|
0c6f8ce77d | ||
|
c0219662bb | ||
|
aae71d375c | ||
|
b6228e4c59 | ||
|
3a9a1439d9 | ||
|
7cf256dc7c | ||
|
c3c26998bf | ||
|
02e860480b | ||
|
7737b8b596 | ||
|
2725322fd5 | ||
|
6c6ccda6b3 | ||
|
d71269e223 | ||
|
36266d2b10 | ||
|
acae62de87 | ||
|
9fe4197cae | ||
|
7fa1a8d54a | ||
|
2333271c20 | ||
|
5b83149567 | ||
|
250a35baab | ||
|
d60ba95532 | ||
|
c901472198 | ||
|
2a5b70fb13 | ||
|
dc6db6e4b3 | ||
|
8df6f32314 | ||
|
a2d8c894fe | ||
|
a1996768f1 | ||
|
205587cb9e | ||
|
224b2ef952 | ||
|
90a83dc753 | ||
|
a7cf968d04 | ||
|
80ff72bae1 | ||
|
5320fc8111 | ||
|
8753531e82 | ||
|
03a845f2b3 | ||
|
25b05f127d | ||
|
073beb0135 | ||
|
ef7659691b | ||
|
e69c0c079e | ||
|
dc31269a06 | ||
|
df9eccabea | ||
|
7788f5ae4c | ||
|
ff5456c178 | ||
|
40ed702437 | ||
|
65924e9a5d | ||
|
a88d149dad | ||
|
b9ec94d835 | ||
|
fc1675575a | ||
|
2f7229720f | ||
|
0187fc7b22 | ||
|
540e1a9650 | ||
|
a371cd1d79 | ||
|
16b11fee31 | ||
|
1bc46f22b4 | ||
|
554c8fe163 | ||
|
8f6bf6e002 | ||
|
d5dd8e9346 | ||
|
4a67e1021a | ||
|
c97061770a | ||
|
55b331511e | ||
|
4b9b5e861f | ||
|
b8599a0642 | ||
|
ae6530585a | ||
|
39aa1fa2a4 | ||
|
4f740acabd | ||
|
2cc9b91895 | ||
|
4eedc39e97 | ||
|
b99e8d7f46 | ||
|
a8a27aeadd | ||
|
21176d2fd3 | ||
|
224c65438f | ||
|
f1c21b642f | ||
|
030d1f374a | ||
|
0b29fa2288 | ||
|
b721f148f0 | ||
|
63434a2f87 | ||
|
0d6f0e66be | ||
|
fb3f1365c5 | ||
|
8de7d5d377 | ||
|
952d7494ac | ||
|
9aeba20086 | ||
|
9150c9c40e | ||
|
3e75897154 | ||
|
b0aa4402c2 | ||
|
41f80bcafd | ||
|
c67359c49d | ||
|
2f7c3cf21e | ||
|
9731c8a750 | ||
|
8092e5c3a8 | ||
|
f7ab8cc471 | ||
|
b593f62c4f | ||
|
6905b7a410 | ||
|
402f27b2a3 | ||
|
c9c46d05d0 | ||
|
6591575d22 | ||
|
6064119779 | ||
|
00cd9b581d | ||
|
a3d7b72485 | ||
|
88c73be2f4 | ||
|
39a9181cdd | ||
|
0e5c6f56a0 | ||
|
5147a070a1 | ||
|
b11be1838a | ||
|
be99768a32 | ||
|
fe439a0cb6 | ||
|
87f49ec879 | ||
|
20f2730125 | ||
|
bc5b34db6b | ||
|
bcf9df3744 | ||
|
4d3674ee0a | ||
|
28567e4629 | ||
|
1180a4fb0b | ||
|
71928d2c9f | ||
|
3853072a2e | ||
|
0cf630ef23 | ||
|
202015fe34 | ||
|
ae43e5cae4 | ||
|
67b67bae6a | ||
|
dbb8fe15cf | ||
|
56efa10f64 | ||
|
5ff776f90d | ||
|
a25b072bf6 | ||
|
c95951c0e4 | ||
|
4c8193b801 | ||
|
598a544ff8 | ||
|
465ef3fa9a | ||
|
2f876d93e3 | ||
|
84e8c44e4f | ||
|
855d794bdb | ||
|
a3333f8fe1 | ||
|
2e64d62ca4 | ||
|
26a3dbcbe1 | ||
|
fa2e86df29 | ||
|
b4f0ece78f | ||
|
630b319a37 | ||
|
4a37e49798 | ||
|
cfbe98a39a | ||
|
91f097d514 | ||
|
c545521cd9 | ||
|
11e0f49ada | ||
|
19ce53128b | ||
|
deccff623a | ||
|
b2a210ec0d | ||
|
bdb5169a6f | ||
|
0865b702a3 | ||
|
d13b8fd486 | ||
|
494911805e | ||
|
cba3a2be24 | ||
|
0686781359 | ||
|
e5b82dca4d | ||
|
2144a42a22 | ||
|
07a989e004 | ||
|
4f7e8116cb | ||
|
c0f650d7dc | ||
|
d4a0136504 | ||
|
5f25e027c4 | ||
|
9610dcce20 | ||
|
3ee3e7c17b | ||
|
98536250bd | ||
|
e95808e6be | ||
|
9fc819a410 | ||
|
b0f1ce1fa0 | ||
|
503579a638 | ||
|
3ad216751a | ||
|
ca8e3179bb | ||
|
fd84e56c00 | ||
|
8b67fb7290 | ||
|
2d1fdb319d | ||
|
c200e18434 | ||
|
b3ffcd020f | ||
|
9258a3dcd4 | ||
|
57f5478731 | ||
|
973d75ebdd | ||
|
33519b27c8 | ||
|
7da7ff4a69 | ||
|
dbd2f697f9 | ||
|
f435762b88 | ||
|
ae46332e42 | ||
|
d003883de9 | ||
|
4e438a44f1 | ||
|
02e19b3d44 | ||
|
ccc19512e7 | ||
|
c34539e389 | ||
|
f8aeacb949 | ||
|
9940190679 | ||
|
e2498b3e91 | ||
|
82246fd9c7 | ||
|
11538552eb | ||
|
4ce28f54de | ||
|
e887ed74a3 | ||
|
28c086e97c | ||
|
11465e89a3 | ||
|
b2197187c1 | ||
|
2b26a10745 | ||
|
daf726ebbf | ||
|
88dd886687 | ||
|
609da457f7 | ||
|
363e28ff69 | ||
|
bdd6bf9020 | ||
|
5c3dab3466 | ||
|
9b2c8fa25d | ||
|
df2f102d9e | ||
|
95ebb0e6d2 | ||
|
e5d03652a9 | ||
|
56b53e2dd8 | ||
|
cf0b7b213f | ||
|
d214c8e01b | ||
|
fba0f362a5 | ||
|
ec05d0857c | ||
|
54b744b7de | ||
|
1a76780fff | ||
|
58f5c44533 | ||
|
d085da4dbf | ||
|
c4a5c356f7 | ||
|
18fdc5c6a2 | ||
|
35dabaab9c | ||
|
9bf31b10bb | ||
|
7186575cb1 | ||
|
2ecae40130 | ||
|
778ed62a90 | ||
|
5e863a87dc | ||
|
7ce8597c25 | ||
|
1992237ce5 | ||
|
8f247b0f73 | ||
|
e2159d80af | ||
|
057023531e | ||
|
dfed65bf9f | ||
|
739161849a | ||
|
c65b280020 | ||
|
d3bcf25ef0 | ||
|
f6bd3340e7 | ||
|
6a7c09bfe3 | ||
|
8b9f294a5d | ||
|
4a282d9629 | ||
|
909b88864f | ||
|
1346a7992c | ||
|
bba607e987 | ||
|
f9cc490c35 | ||
|
a5aec2d9fa | ||
|
c0df368dc6 | ||
|
aa77433523 | ||
|
44ad99f693 | ||
|
b7f7a82ea9 | ||
|
c69978c9fd | ||
|
6174aa6ee1 | ||
|
74b8d2e908 | ||
|
63a515944f | ||
|
5b5db7b860 | ||
|
b3bbacf2ef | ||
|
3a0429d049 | ||
|
ab539081fa | ||
|
f0d88d4e73 | ||
|
772cbd6ffd | ||
|
17b9dbe9d7 | ||
|
75e5d42d8b | ||
|
031c15fd7d | ||
|
de1924cefc | ||
|
66db0a4751 | ||
|
c309410965 | ||
|
7f461b99e2 | ||
|
7bfe0eeae9 | ||
|
8619bd5be3 | ||
|
56011d37d4 | ||
|
c2852c8a82 | ||
|
6f546a424e | ||
|
447f7530af | ||
|
6136f1206b | ||
|
36a3c5b501 | ||
|
dcd6c1f522 | ||
|
2b074bcdcb | ||
|
b20ec7f0eb | ||
|
58cf69a2fe | ||
|
096c148228 | ||
|
a68005d4ab | ||
|
bf3a281987 | ||
|
2965a6827d | ||
|
9f43a73c36 | ||
|
7551b45da2 | ||
|
bca3685eda | ||
|
6d20175800 | ||
|
a62dd4c020 | ||
|
d1d9620a61 | ||
|
5106d77c77 | ||
|
69cf237d7a | ||
|
7d9d1c82b6 | ||
|
228ff5edf4 | ||
|
2baac618a8 | ||
|
e8f499a938 | ||
|
96ca20d0b4 | ||
|
54acdc86e7 | ||
|
0815e02895 | ||
|
30cbb72b57 | ||
|
0b91397709 | ||
|
89934c17ca | ||
|
e4a38e62eb | ||
|
5e1e09d7bf | ||
|
51ce3a1e42 | ||
|
54b46dfad9 | ||
|
787917ac66 | ||
|
619b49bdc4 | ||
|
4b3a73d440 | ||
|
54f9c59d6e | ||
|
1d123996f6 | ||
|
c4768f6138 | ||
|
5f3551ff34 | ||
|
d3985c2e3b | ||
|
3b1843f3a3 | ||
|
2c36796362 | ||
|
655ccba89b | ||
|
150d72f0f8 | ||
|
6a316b34a2 | ||
|
8e6b600609 | ||
|
5630a4dd67 | ||
|
38ee8aedc1 | ||
|
d594615532 | ||
|
2739fa60be | ||
|
327301782d | ||
|
8781c5db8d | ||
|
31c34ea158 | ||
|
ef9bbaca19 | ||
|
02d89072bc | ||
|
4e8bd640a7 | ||
|
8c71a00600 | ||
|
3d36c70d53 | ||
|
c72479c4d6 | ||
|
4bb88d8e44 | ||
|
0ee0958539 | ||
|
b6dd6f3a94 | ||
|
d11c322e1f | ||
|
cd92b34ef1 | ||
|
b6481cfcda | ||
|
db7eb92638 | ||
|
f2d0477550 | ||
|
776e207f09 | ||
|
733e8a0043 | ||
|
4fa19006ad | ||
|
73a597e3e5 | ||
|
d776f1765d | ||
|
741b6f6f9a | ||
|
b6f4695bcd | ||
|
b7d3b807d2 | ||
|
0cc386bc28 | ||
|
cb155707cd | ||
|
9b6b250cbd | ||
|
5b7e29b8ad | ||
|
b6d748b414 | ||
|
8fc4b338c2 | ||
|
6d3ea19ac5 | ||
|
d01ef48bf0 | ||
|
71103bb7b9 | ||
|
1c9bc00acc | ||
|
bed128e8cf | ||
|
b5c3f18f24 | ||
|
cbccdf5d93 | ||
|
a46f3a31e1 | ||
|
a808f7b04e | ||
|
3a883b9e41 | ||
|
17d8691300 | ||
|
523ce1dbdd | ||
|
4a3a9bf62c | ||
|
3b30177959 | ||
|
7e66f89260 | ||
|
1a93ba634f | ||
|
d93f823fc6 | ||
|
f2198bf938 | ||
|
965f10698b | ||
|
b71367cd2a | ||
|
abe18ac825 | ||
|
bb193d3768 | ||
|
7a7f5cd4a8 | ||
|
ac9f49f8c9 | ||
|
8f53859e00 | ||
|
bfb7ff88d9 | ||
|
c36425fd3a | ||
|
981f9d0b01 | ||
|
fa89fe3e87 | ||
|
d132357c20 | ||
|
a719237556 | ||
|
8955ca5216 | ||
|
f665762cc8 | ||
|
d16fc2b68b | ||
|
e1df32c32d | ||
|
943c6f77dc | ||
|
4964382966 | ||
|
021c6fdbe2 | ||
|
711f9805c9 | ||
|
0aaae3afd6 | ||
|
eadd1042fb | ||
|
16fa2c9f5e | ||
|
d4ab1df870 | ||
|
4a5aa1bcc1 | ||
|
01a9cda99a | ||
|
dcdf606ff6 | ||
|
ea021de5eb | ||
|
5cc3526f8f | ||
|
3060fc2af4 | ||
|
e4395dfeb4 | ||
|
f65dadf1d7 | ||
|
f048762fd9 | ||
|
bb985f826e | ||
|
bb239cba2a | ||
|
e7e66e580a | ||
|
689d689d3b | ||
|
125c3d3e0d | ||
|
ffaa06560e | ||
|
f9b716201f | ||
|
8b14a5f0d8 | ||
|
c5855119d8 | ||
|
a036597f5f | ||
|
b8688e2e66 | ||
|
d8b2e08717 | ||
|
7da78d3312 | ||
|
2292b107dc | ||
|
1fcc74c658 | ||
|
73be027951 | ||
|
132d91c2ab | ||
|
f08ce82c3f | ||
|
8de2712652 | ||
|
55835785c0 | ||
|
cb711b7758 | ||
|
19c75293bf | ||
|
33219f00d5 | ||
|
4aaedbb0e6 | ||
|
ee1f14d4eb | ||
|
feac8085c9 | ||
|
0a5ec11b1b | ||
|
03937174e5 | ||
|
e00529c05f | ||
|
5dbf8e1d2b | ||
|
bebf672186 | ||
|
6d4a8f2a5a | ||
|
14db0ae663 | ||
|
88d1c1d140 | ||
|
9106055be1 | ||
|
2cf52f15ab | ||
|
b7e9d61c72 | ||
|
0bc22db296 | ||
|
6a943acc70 | ||
|
7c97416e7d | ||
|
dd75504a66 | ||
|
04c4ab2289 | ||
|
310a2e2511 | ||
|
f6314431f0 | ||
|
89100d0ca0 | ||
|
2b646636c1 | ||
|
b10c1d5006 | ||
|
225b829c1a | ||
|
5f4c7076ab | ||
|
61c9b304d7 | ||
|
789d7000cf | ||
|
d23ef2bd59 | ||
|
8a77f832a3 | ||
|
5a6d318cfb | ||
|
e9f14de05d | ||
|
2d8da45bda | ||
|
83de33f5b8 | ||
|
e63844f786 | ||
|
0a805c16fd | ||
|
653c7d4430 | ||
|
306c3bea21 | ||
|
389ce60bc9 | ||
|
2d453a1a6c | ||
|
0775560ad2 | ||
|
2680c1e8b3 | ||
|
50ba2e3ad4 | ||
|
c16875d0de | ||
|
a4205cd0c2 | ||
|
2fb3e373c6 | ||
|
ac1fa7209c | ||
|
fd820d6af8 | ||
|
b88486601b | ||
|
92e712a508 | ||
|
64a9079ce4 | ||
|
f18d0ab923 | ||
|
def49e6d20 | ||
|
f142db3d49 | ||
|
e7b04a89e2 | ||
|
1cb59f46e3 | ||
|
a604746e0b | ||
|
aba706a293 | ||
|
c1b9113347 | ||
|
5e7db2807d | ||
|
203f830a30 | ||
|
3dbe9193e2 | ||
|
717cf29595 | ||
|
72300fec5e | ||
|
ec50b1d67a | ||
|
408a4420c9 | ||
|
568218204a | ||
|
bd39e98c66 | ||
|
01be65a624 | ||
|
a59d8a6a17 | ||
|
70b71aa4f9 | ||
|
aaf7991139 | ||
|
265d579fd9 | ||
|
f6dd91e47c | ||
|
cdae552ab0 | ||
|
730e887454 | ||
|
5a3852d82c | ||
|
5b35580d2d | ||
|
4b0705fe36 | ||
|
d219b4d12a | ||
|
6e38331328 | ||
|
c4bc9aea22 | ||
|
6c692d9308 | ||
|
7ce8bd8988 | ||
|
b4ead4076a | ||
|
052893bbf8 | ||
|
a319fe8632 | ||
|
8a84e68c13 | ||
|
0ca7defe83 | ||
|
b704706ee9 | ||
|
23fb634847 | ||
|
0bd37eb8f9 | ||
|
b86294e9cc | ||
|
f11f968f99 | ||
|
ad81642954 | ||
|
ea42103ae7 | ||
|
82435e37be | ||
|
b61d29b22a | ||
|
04392a6a4c | ||
|
6e9798d596 | ||
|
bef553c9ae | ||
|
7a76efa7a0 | ||
|
c5f64374ed | ||
|
b767caa704 | ||
|
63974f97ab | ||
|
37764a7caa | ||
|
fb586f0043 | ||
|
9990f4d8cf | ||
|
80f73cc5d0 | ||
|
8775f67416 | ||
|
2ce302f6f8 | ||
|
69b9944b8e | ||
|
37f35e8b2d | ||
|
8ad9531fa6 | ||
|
5ec37c08bf | ||
|
84cb008421 | ||
|
a744cca35a | ||
|
1950cf5f99 | ||
|
fbf230cd01 | ||
|
05abf1e419 | ||
|
f0c13980ed | ||
|
d136207d2f | ||
|
b331ee5d93 | ||
|
73cd6e981a | ||
|
6826be73c7 | ||
|
0a61a607c9 | ||
|
7444e1566b | ||
|
e401661cfc | ||
|
89d32f7109 | ||
|
5b4c88f933 | ||
|
966bce973a | ||
|
69cbcae7ed | ||
|
e26cf282d6 | ||
|
6ec7d55cbc | ||
|
9888f2a322 | ||
|
15d29b2c79 | ||
|
7e768e0a17 | ||
|
affa41b5a9 | ||
|
c8cf179ed9 | ||
|
9ef7310fc2 | ||
|
cdd7d0f4c5 | ||
|
813d706dac | ||
|
223d4133c5 | ||
|
bb62ebc23e | ||
|
a4c2adf69a | ||
|
0baff0a1e1 | ||
|
51766ad72c | ||
|
bf00b42941 | ||
|
56482acfc2 | ||
|
8898b1a608 | ||
|
525bc37649 | ||
|
9d29450f47 | ||
|
26c98bdace | ||
|
19ccd35f3f | ||
|
efa7529c5c | ||
|
334c11ccd1 | ||
|
c318a5dd79 | ||
|
25d1a8957d | ||
|
9f4853b9d6 | ||
|
ac58f50b0a | ||
|
36f22b660e | ||
|
398046eca6 | ||
|
a6f2ce4bdd | ||
|
950a8ea8df | ||
|
2b6b979f88 | ||
|
4b80e5ff47 | ||
|
a14d18c6b6 | ||
|
fe88c7ce97 | ||
|
c54b00a701 | ||
|
4a4b8c2e12 | ||
|
f1770711cb | ||
|
94757f0f0c | ||
|
15cf9be90d | ||
|
cb1955c217 | ||
|
2ce944034d | ||
|
53a207e859 | ||
|
dbdd2411c3 | ||
|
4ac3fbd726 | ||
|
8f8f62555c | ||
|
73b980c6a9 | ||
|
d2cce3cc40 | ||
|
4139360788 | ||
|
ca7bb50212 | ||
|
48ebf626b8 | ||
|
564eb802b1 | ||
|
c968949f49 | ||
|
74268ecce6 | ||
|
93f9db3af4 | ||
|
4cc0a9d4a9 | ||
|
a04dfba16b | ||
|
7f83b58b92 | ||
|
ca2e6353ae | ||
|
6bf7795529 | ||
|
401f4828b6 | ||
|
7a0ce1efe7 | ||
|
cdc2550e34 | ||
|
2d760241f3 | ||
|
3748e420a0 | ||
|
031a253101 | ||
|
cfce6d548b | ||
|
038e93ea6a | ||
|
8028e145f1 | ||
|
ddfb9fd0cc | ||
|
a36ed6ab1e | ||
|
0c7fe0664e | ||
|
7ae2f1980b | ||
|
0c89583b1b | ||
|
f5ec43276a | ||
|
e55e46011c | ||
|
77c0304faf | ||
|
ea476e26ae | ||
|
2eb47aef62 | ||
|
3e4084d99c | ||
|
955ac92043 | ||
|
5a65a07a39 | ||
|
554d73c6ee | ||
|
7bc7bc7c49 | ||
|
7b8d47cdeb | ||
|
63a8509f1f | ||
|
4e69454f72 | ||
|
ec3e237093 | ||
|
edd224a185 | ||
|
b5142b8ef5 | ||
|
ddfdf1d49d | ||
|
e7675c29da | ||
|
0bf3fef431 | ||
|
8cb0e81e89 | ||
|
d468cb051d | ||
|
0b24be7532 | ||
|
6316a0ede9 | ||
|
3c9d0e02f6 | ||
|
02d4360526 | ||
|
aedf80e382 | ||
|
51b492e1e2 | ||
|
d283f236db | ||
|
371e937a0e | ||
|
5778c25477 | ||
|
d2cd3ec879 | ||
|
ba0a59629b | ||
|
03fea7558d | ||
|
4901673c4e | ||
|
a4d84bd2e8 | ||
|
0289218c92 | ||
|
c3d32e59e7 | ||
|
8cab08351b | ||
|
5de0e58d5d | ||
|
438106f42e | ||
|
6b21f38047 | ||
|
1b07a5f3c0 | ||
|
a39ec5a061 | ||
|
015df9e6dd | ||
|
bf95284b72 | ||
|
07c04f0c2f | ||
|
b9976ee68e | ||
|
c8d9951ae4 | ||
|
97339512e1 | ||
|
95682a7a0b | ||
|
87038c4c75 | ||
|
1e5b5193fe | ||
|
9cc47678a2 | ||
|
198109cd43 | ||
|
16490541e4 | ||
|
6d5ca25e03 | ||
|
0ca44bd859 | ||
|
124b5b0549 | ||
|
fed62fae3c | ||
|
9e2812d55c | ||
|
fdcbc3904a | ||
|
cc0114cd90 | ||
|
f97753c1d0 | ||
|
fcdf545a3c | ||
|
bf28f887eb | ||
|
5f1f04e486 | ||
|
2242174749 | ||
|
e8af077fe2 | ||
|
055252f746 | ||
|
8dc84d7c17 | ||
|
f140c75fb9 | ||
|
c63dd46bac | ||
|
a23b0ba945 | ||
|
7e768aa6e9 | ||
|
18765508c0 | ||
|
efb3464ee2 | ||
|
f3319c5f75 | ||
|
384623fbb7 | ||
|
3bcccdf3dd | ||
|
d9b913fa69 | ||
|
7b9a430e8a | ||
|
4f3ab0dc69 | ||
|
427caaf9aa | ||
|
85eefdebbb | ||
|
b31b70302b | ||
|
c830ea676f | ||
|
9249059cb7 | ||
|
9fee228d1a | ||
|
20718c6ec1 | ||
|
b8754f3f44 | ||
|
6f598ae59e | ||
|
ca81fcaf37 | ||
|
75867da94f | ||
|
53c11701de | ||
|
4741793bb6 | ||
|
ab161a42ee | ||
|
4642b79b5b | ||
|
1cb43fe110 | ||
|
0123135237 | ||
|
1d77f91acc | ||
|
9b89333b1b | ||
|
829361d767 | ||
|
d3fca26499 | ||
|
ff3460e100 | ||
|
1a209a54c6 | ||
|
3bf0ac087f | ||
|
96cf6215ce | ||
|
71ce6120c3 | ||
|
21e82d0daa | ||
|
8c20bb003b | ||
|
90913d2486 | ||
|
ef0de04a0f | ||
|
8879a0ce8a | ||
|
18db46800e | ||
|
955c5b5605 | ||
|
c3b2c88213 | ||
|
7412bb35ad | ||
|
2a6fbc5c5d | ||
|
90c9b87f8d | ||
|
ffe2557e84 | ||
|
a77798e5b4 | ||
|
c1fa7bfce6 | ||
|
bf50da1e6b | ||
|
2cd4aac5ce | ||
|
208b96c092 | ||
|
36a53f8134 | ||
|
e8d8a8737e | ||
|
41b9f90a0b | ||
|
5e39493e89 | ||
|
31f3c60401 | ||
|
519c74e4dd | ||
|
4419770b15 | ||
|
b7fa86c848 | ||
|
4d620c3db9 | ||
|
319c8cb54c | ||
|
868b6f141c | ||
|
589fdd4cfe | ||
|
3f36dd9a14 | ||
|
0a6568bbab | ||
|
4e26d56746 | ||
|
d4e363fbd7 | ||
|
b721b822eb | ||
|
58d6985080 | ||
|
ee2abd415e | ||
|
09c9321159 | ||
|
0900a7cf80 | ||
|
d3ec7f65fb | ||
|
b24a94d6ce | ||
|
65769c7766 | ||
|
cbeef9fe06 | ||
|
b199209d0d | ||
|
cb48545600 | ||
|
5e626e2cc5 | ||
|
2709d1ff6e | ||
|
de3ca6e237 | ||
|
c2253e868c | ||
|
3a77c6f7eb | ||
|
8e3ff7670b | ||
|
7870147c16 | ||
|
64a371e5d8 | ||
|
f837736a20 | ||
|
dec503da81 | ||
|
1328299fda | ||
|
05190a52f1 | ||
|
46cc3105c4 | ||
|
8ba5853cdd | ||
|
e5fbcc4a8c | ||
|
e559194e8e | ||
|
c0d2994b8e | ||
|
89009aa1f8 | ||
|
ee8b580a98 | ||
|
f4656fa493 | ||
|
adf9405fd7 | ||
|
f46db7ce1a | ||
|
f00d726347 | ||
|
e8d8063ebc | ||
|
b4bc4f5ddc | ||
|
0d3ffe210f | ||
|
8eb88a161a | ||
|
346c964419 | ||
|
bc8be2460f | ||
|
a33f24d19c | ||
|
8f839fbf8e | ||
|
10dd9101cd | ||
|
aa5d3af8a1 | ||
|
dace993c21 | ||
|
32b72f0ef6 | ||
|
3dbc54c8ae | ||
|
9dd3b8fd68 | ||
|
5fb1afc681 | ||
|
a4c985a219 | ||
|
a1f819a458 | ||
|
166d7ba1cf | ||
|
a089accd23 | ||
|
38546e557f | ||
|
080dca7ee0 | ||
|
a89e433f3d | ||
|
f30ef61b06 | ||
|
03a36c1100 | ||
|
546eb6e73e | ||
|
bedd3abf8a | ||
|
ce2d4498e1 | ||
|
a786023160 | ||
|
98cf8aa445 | ||
|
d733d9fd4c | ||
|
58e9cb8b93 | ||
|
bb669acf95 | ||
|
4f3751b7ce | ||
|
84c12dee80 | ||
|
f5f865a139 | ||
|
902aed671a | ||
|
1423fe7e16 | ||
|
58ed2e6f33 | ||
|
a90939f46a | ||
|
db7456b9c5 | ||
|
a964b30c34 | ||
|
d566629d51 | ||
|
837422fbb8 | ||
|
afc37c71a6 | ||
|
e8b014ea6d | ||
|
b124512020 | ||
|
158f352328 | ||
|
f1895e32fb | ||
|
6c3be01093 | ||
|
ebe548438c | ||
|
a45c61f19e | ||
|
b07a4b95aa | ||
|
777b4e13e6 | ||
|
92cfc9324d | ||
|
97dfa18b9b | ||
|
515c07ea2b | ||
|
4d17e45f86 | ||
|
32095daf90 | ||
|
aa23ed892b | ||
|
3f079c8501 | ||
|
0aaf4bfde8 | ||
|
b967cfc775 | ||
|
2ca0483bf4 | ||
|
03cedc7b35 | ||
|
35049b5830 | ||
|
b707dde4da | ||
|
1ec8339b13 | ||
|
8bbe04a174 | ||
|
0b6a173ba0 | ||
|
8b41dfe94f | ||
|
a18b1ac99b | ||
|
e007bcb640 | ||
|
1090e7ceae | ||
|
933035dd7e | ||
|
aaaac78170 | ||
|
b5d2392def | ||
|
b88a736735 | ||
|
f238da416b | ||
|
10c7e75491 | ||
|
60af0735f4 | ||
|
273cbf333e | ||
|
189b17ad8f | ||
|
1eecf26429 | ||
|
2f9a3fa942 | ||
|
f166fb132e | ||
|
0adb69139e | ||
|
ab309e4b90 | ||
|
532c6d4fa5 | ||
|
14f627d0d3 | ||
|
947c38c124 | ||
|
f991e49203 | ||
|
4fbb6ed4ff | ||
|
2fa49303de | ||
|
f9527e9d2d | ||
|
80c63cb9d6 | ||
|
a2455eeade | ||
|
5d7ad4b3bd | ||
|
f46fa61cf3 | ||
|
da24a9637b | ||
|
2cdc11c2ad | ||
|
191cd3f25c | ||
|
f158bce8bd | ||
|
94eebb2dd6 | ||
|
375b146690 | ||
|
8c13214ff2 | ||
|
681dfadb57 | ||
|
b8b5d1cb09 | ||
|
e187dd86ed | ||
|
c380d6988c | ||
|
444acc8ef5 | ||
|
068f08aa45 | ||
|
bdc101f69c | ||
|
0ece530b9e | ||
|
5853a86091 | ||
|
20ba87bc88 | ||
|
be166ec158 | ||
|
02147891c2 | ||
|
c08cd588eb | ||
|
882bb564b1 | ||
|
18edf06a20 | ||
|
68b52b6130 | ||
|
3503f3eb4b | ||
|
a75706f329 | ||
|
29fe9d973c | ||
|
87d3320fa1 | ||
|
0b96dbc04c | ||
|
55ed58c4a0 | ||
|
8503e8c9ad | ||
|
fcdf783c40 | ||
|
008b92acb2 | ||
|
baf8ef8516 | ||
|
0cf89467e3 | ||
|
37d8a563c0 | ||
|
f3de93d73a | ||
|
4de23bd160 | ||
|
29397ca04f | ||
|
6038a36532 | ||
|
186a922d06 | ||
|
be1f9e66e3 | ||
|
9ac015e550 | ||
|
f7a9ac0ba2 | ||
|
39a8ddcfec | ||
|
3a9e28ba48 | ||
|
b6a479355e | ||
|
a791c964e8 | ||
|
9686a7f9bf | ||
|
6b65b89350 | ||
|
09a1915ec6 | ||
|
3424c421a4 | ||
|
77b89b6841 | ||
|
09807cfcad | ||
|
6ca81df40c | ||
|
2d00ddad2b | ||
|
04f4934adb | ||
|
0593007fd0 | ||
|
c07cbf9dbb | ||
|
ddd60aa1ff | ||
|
b5ecdbbc48 | ||
|
0cca613b04 | ||
|
b254ad177a | ||
|
4fd9213d2c | ||
|
982d1e219c | ||
|
3923b8631d | ||
|
72dcdf4483 | ||
|
5f3a71ed5f | ||
|
000d6ebcd0 | ||
|
7f0a04e3e3 | ||
|
005c6bb9f1 | ||
|
59c856fc0e | ||
|
5bf26619ee | ||
|
676c1d560b | ||
|
5afaa7c079 | ||
|
b0b49b72b8 | ||
|
e7600458a9 | ||
|
e0fa308984 | ||
|
6ce6fed10d | ||
|
b23ca2e138 | ||
|
2798dc3d42 | ||
|
0586d4d7a9 | ||
|
d2b3a52424 | ||
|
ffb97242d9 | ||
|
13ab400d6d | ||
|
db6d47b3f3 | ||
|
393943bb9f | ||
|
4c72ac14e6 | ||
|
7156871412 | ||
|
20665a49ed | ||
|
ad1dfc3137 | ||
|
8de67411ac | ||
|
2e75c9951b | ||
|
77f66f44a4 | ||
|
3278a397f4 | ||
|
d1c2abbaf3 | ||
|
8a443013c0 | ||
|
d3735c4763 | ||
|
f9887434d1 | ||
|
12afa005d9 | ||
|
7c0129b911 | ||
|
332f92d08e | ||
|
281ac16dca | ||
|
e8a2b9446d | ||
|
4e33f3e722 | ||
|
e46e60153d | ||
|
d3559bb1b7 | ||
|
c8a046996b | ||
|
6c85b8717f | ||
|
142a62e371 | ||
|
ff6abf08b7 | ||
|
2fd921cd60 | ||
|
1d7bd57357 | ||
|
1a4d76b4c3 | ||
|
853d869086 | ||
|
c99d669b06 | ||
|
19d3921637 | ||
|
4ee716457f | ||
|
5fb19bb187 | ||
|
8b83c5349d | ||
|
ac91942452 | ||
|
5ef23ddc1d | ||
|
2ad6c66a8d | ||
|
630f877e95 | ||
|
7dd5ce1356 | ||
|
54b18d15b6 | ||
|
2b0880af80 | ||
|
786ea17f95 | ||
|
c699bae99c | ||
|
588e8e019c | ||
|
1e7a86b7c0 | ||
|
2bb18b18b7 | ||
|
da77c549a7 | ||
|
fc707c157a | ||
|
5be6f550be | ||
|
66abf27edd | ||
|
3d1b6d7de7 | ||
|
115e604627 | ||
|
f9aeec8eb5 | ||
|
acf4d15a8d | ||
|
e5e08d1762 | ||
|
178c7817fa | ||
|
a93a428a89 | ||
|
d126a361e1 | ||
|
9e42ec0c06 | ||
|
1f05654faa | ||
|
3ddef6440a | ||
|
036b47acbc | ||
|
42e585e7a2 | ||
|
93d066bef7 | ||
|
580c6fca00 | ||
|
03300da93a | ||
|
2a1fc38799 | ||
|
646afbfa36 | ||
|
45a1435ce7 | ||
|
f4d7d32fd7 | ||
|
1b00f93091 | ||
|
9c9e5a54a7 | ||
|
4cf8c9c477 | ||
|
a78e1d7f2b | ||
|
00c1d6c42c | ||
|
5d92f09449 | ||
|
ac7d820e6f | ||
|
7d752443ca | ||
|
3311b5a6e3 | ||
|
3801443c9c | ||
|
9b2a2eb229 | ||
|
d165f66815 | ||
|
6715d52b81 | ||
|
ae160556d0 | ||
|
c040e1f9b7 | ||
|
db14a8ac1a | ||
|
9beafd03ee | ||
|
4f081c5e14 | ||
|
ff4e9e42e5 | ||
|
d5e3e6f256 | ||
|
cc9660c0e7 | ||
|
90d92707c8 | ||
|
fe77bc7c37 | ||
|
54a7d8291b | ||
|
7bb924ce5b | ||
|
9f2e25309e | ||
|
b32b39579e | ||
|
00513e66bc | ||
|
8850867b82 | ||
|
d6dfbbf8dd | ||
|
6c4797bce4 | ||
|
2c97eb4115 | ||
|
54b0828c20 | ||
|
0d241a4993 | ||
|
802b0d1a75 | ||
|
e76a7716bc | ||
|
a9cc0a61d9 | ||
|
fe50e47fc6 | ||
|
0006b286ca | ||
|
17f99da45e | ||
|
f7ce03a819 | ||
|
c073501ac0 | ||
|
d4a4747c08 | ||
|
e7b19a5f66 | ||
|
0070ec09a2 | ||
|
b3ec62a3db | ||
|
34ca4ad3b0 | ||
|
e0909731a4 | ||
|
d8d1cb2eda | ||
|
2a24fbadae | ||
|
85e1c4bd47 | ||
|
09fcb0739d | ||
|
0882c2b5c7 | ||
|
bb7763d72b | ||
|
b919d7364a | ||
|
942a37290c | ||
|
ef24c6cbbd | ||
|
cf05571f1c | ||
|
0fa038b4f2 | ||
|
10a33a2978 | ||
|
60a88381cf | ||
|
48d83c7d44 | ||
|
b967b7ed6b | ||
|
b60ecb6171 | ||
|
e8b80216e8 | ||
|
69a5472a16 | ||
|
bd6eb606ca | ||
|
e7f1e83a43 | ||
|
357fe3b793 | ||
|
ad6c06409e | ||
|
2a7feba808 | ||
|
45b7e28db2 | ||
|
daa9479352 | ||
|
0f688bd71a | ||
|
ffe0843bd6 | ||
|
0ee4039853 | ||
|
a3a6e8cdf6 | ||
|
30f60f87f4 | ||
|
0d91657557 | ||
|
ec9b80a64e | ||
|
91909f8886 | ||
|
cc335dae38 | ||
|
0d6ea03ac8 | ||
|
e1370b75e8 | ||
|
c2d0ccea3c | ||
|
9319851118 | ||
|
d45ad76fbc | ||
|
d8b9c3b832 | ||
|
9f2534af3d | ||
|
2c2978b93a | ||
|
c9b0f22a2e | ||
|
2959cd140b | ||
|
7834cfe5f0 | ||
|
2b9c5d0cff | ||
|
cb3b51dcde | ||
|
0c23fe96be | ||
|
9d0bd73ee6 | ||
|
15f096f764 | ||
|
9dd41be608 | ||
|
17f8703c71 | ||
|
5edfd7b6f7 | ||
|
4371772ec7 | ||
|
8e4e08a828 | ||
|
401e6b5a57 | ||
|
01dfe83fcd | ||
|
177c5700ff | ||
|
5d401a4fbb | ||
|
c60ab97103 | ||
|
f3d7a90ac7 | ||
|
7b114f961a | ||
|
8caaf13c84 | ||
|
0218da1ee6 | ||
|
f4786755f1 | ||
|
b9b1fcb4e5 | ||
|
5bfda557bc | ||
|
6e53df403e | ||
|
4a7246ec6e | ||
|
f53a021c08 | ||
|
8fa95d06e1 | ||
|
b34bd9ffd2 | ||
|
fd1a7e34a8 | ||
|
973653a690 | ||
|
11e4dfe940 | ||
|
e7804d39b3 | ||
|
6cfbde43ab | ||
|
30421bf247 | ||
|
ab1b6b45a2 | ||
|
59366c04dd | ||
|
fac437c1ef | ||
|
797f0d92b3 | ||
|
75f3fa5144 | ||
|
6ea1b5c7c3 | ||
|
6a81cc0e83 | ||
|
d3e2c7c51a | ||
|
1c930a2eb9 | ||
|
fb1ea5b37c | ||
|
e82f344a2c | ||
|
10ba205f97 | ||
|
fbc8b97c25 | ||
|
c6a31b21e3 | ||
|
4aa8c4b79d | ||
|
04179ae833 | ||
|
205ecde505 | ||
|
7cb467c960 | ||
|
cbe7c69154 | ||
|
c93cbb614d | ||
|
42c570a6e2 | ||
|
aed47c1178 | ||
|
a1ac4784c8 | ||
|
d8b751d56b | ||
|
213eb2ae88 | ||
|
17b7265a9a | ||
|
3afdf9571e | ||
|
bb8a59d1db | ||
|
d26830d81e | ||
|
1f2c703ddc | ||
|
dd51a6e4d1 | ||
|
caa7597097 | ||
|
409996cb29 | ||
|
a15b37d177 | ||
|
a436ef2126 | ||
|
4c53e6e89d | ||
|
083eb98949 | ||
|
418951415c | ||
|
bac0d0a175 | ||
|
7bde7ebc30 | ||
|
7b7e6555c1 | ||
|
ea8565d0ca | ||
|
1ba2902618 | ||
|
e91e8d565a | ||
|
206e613ace | ||
|
9a88dcf33e | ||
|
9fc9e8972f | ||
|
4fe6703dd4 | ||
|
d613cbb68f | ||
|
be2247b36f | ||
|
66d1f49116 | ||
|
37d35bbbdb | ||
|
321d4ccc8e | ||
|
6e4338d13c | ||
|
677d15d8a7 | ||
|
a7e978dc06 | ||
|
67f5218a73 | ||
|
c2d1329b8b | ||
|
0530ec0651 | ||
|
8a6e18badc | ||
|
1a144a7070 | ||
|
e115432e11 | ||
|
145f6e2d53 | ||
|
9c62311b56 | ||
|
a150d91c48 | ||
|
4730a4e352 | ||
|
67c57be830 | ||
|
d98e72ca25 | ||
|
d81fa1e610 | ||
|
8c12de8b73 | ||
|
9b5218f85b | ||
|
a6cf5c7667 | ||
|
4fca73ffbd | ||
|
46a9023782 | ||
|
5fed443476 | ||
|
0fab0f207c | ||
|
ee67358ee6 | ||
|
cefd44304b | ||
|
2a88cdc76d | ||
|
4c39d37ad5 | ||
|
9b9865f717 | ||
|
3f0456322d | ||
|
16ac005fd6 | ||
|
003fcf446a | ||
|
39703120a4 | ||
|
656c0efc0d | ||
|
63f7c0ed69 | ||
|
5103463524 | ||
|
97254a1e3a | ||
|
9f171a01e8 | ||
|
6c9b6007a3 | ||
|
f4a7c4bd69 | ||
|
5abad18e51 | ||
|
57f70a6d35 | ||
|
cb78f3a707 | ||
|
b331db74ee | ||
|
8fb9ad3fe7 | ||
|
d4b5c55169 | ||
|
a41438d779 | ||
|
0895d8a813 | ||
|
89310d7b7c | ||
|
ca0e936f7c | ||
|
afe767e28a | ||
|
1481b011d9 | ||
|
5f1b968b60 | ||
|
5a729043ce | ||
|
2de0dfcef7 | ||
|
7cc83ed080 | ||
|
31ec03f8e5 | ||
|
0d095c4cf1 | ||
|
dbde09d008 | ||
|
43d33e21e6 | ||
|
c8cf43a255 | ||
|
b2d05672b1 | ||
|
4123c789e6 | ||
|
c65f3c7b68 | ||
|
41e3642e02 | ||
|
eae7602f80 | ||
|
fd12c73cf9 | ||
|
2ef9d70224 | ||
|
3861531fb7 | ||
|
aeff41ff41 | ||
|
b5c8283575 | ||
|
b31cdce073 | ||
|
6040aae12a | ||
|
6ba4afc5bb | ||
|
4707e2b02a | ||
|
08dd73fd72 | ||
|
83bf67b8ca | ||
|
b758ead371 | ||
|
357a0db03e | ||
|
6a3219a5e8 | ||
|
b7e7e82f85 | ||
|
e4c66c8b2b | ||
|
4d4f8b5ee8 | ||
|
5ab75f7eb9 | ||
|
8ff22bc737 | ||
|
1d65287fbb | ||
|
dc24993bf6 | ||
|
ef606d1365 | ||
|
e26ecec545 | ||
|
28e8aa9111 | ||
|
a8ff67e3b8 | ||
|
c311b92ae2 | ||
|
b04e22cd2c | ||
|
4903c70686 | ||
|
04aa963b9a | ||
|
7fdd2e81cf | ||
|
38778c8cac | ||
|
565851b113 | ||
|
cd216b0591 | ||
|
031e2acc63 | ||
|
b3afa6b0a4 | ||
|
40fc2b78d3 | ||
|
24e6c1ea36 | ||
|
275b4e2944 | ||
|
061c93cb7d | ||
|
3b91117724 | ||
|
eb4fac28c4 | ||
|
acb5fc6393 | ||
|
961ed728a1 | ||
|
678fcfc3a3 | ||
|
79d60e6b4a | ||
|
858b2ff6da | ||
|
ba3355c74c | ||
|
6e3028cf86 | ||
|
a28598630d | ||
|
36810b2d42 | ||
|
de9923f2e1 | ||
|
716b1ac528 | ||
|
77663d64a0 | ||
|
49540c7151 | ||
|
03710bc6ba | ||
|
489ae6c9b6 | ||
|
3b54eeb1c5 | ||
|
a9bd753e38 | ||
|
bf15580081 | ||
|
5b5a299b55 | ||
|
051b529c11 | ||
|
fa5fb815fb | ||
|
a4c0a61698 | ||
|
80d8a8190d | ||
|
e69a62c41a | ||
|
46183d4d43 | ||
|
4f2d3cc250 | ||
|
79daf543b0 | ||
|
5b83fb496c | ||
|
3dfe7c27cd | ||
|
e50538c634 | ||
|
92b6ecc4dd | ||
|
588ade0ab2 | ||
|
4375aefb49 | ||
|
c5cba7775a | ||
|
7cf8ec6d61 | ||
|
46cbd3ae7b | ||
|
678cf8a341 | ||
|
3a0f1f6197 | ||
|
902ff23a95 | ||
|
7e2e9aa836 | ||
|
491ef4559f | ||
|
b7bad0f585 | ||
|
ce248bc169 | ||
|
d599be7e7c | ||
|
5b1fc0e6c0 | ||
|
aeece6c201 | ||
|
d7a1b974cd | ||
|
0db1872bd2 | ||
|
4333bc7004 | ||
|
f6caac749e | ||
|
723303e1b6 | ||
|
0daf5bae96 | ||
|
6d59fe2a34 | ||
|
3ed0f005ee | ||
|
66b89ee817 | ||
|
c596c875a6 | ||
|
08c1a29383 | ||
|
ab078d1ea0 | ||
|
b028b09041 | ||
|
44c3eb7b35 | ||
|
f54052c320 | ||
|
d23a055e3b | ||
|
8f76f3184f | ||
|
619c4853fd | ||
|
58fb6ade7c | ||
|
f2c0626c68 | ||
|
3956d2bd63 | ||
|
1d851631d6 | ||
|
517e38e33f | ||
|
a2644a4ebf | ||
|
f7bf6a8080 | ||
|
681c1d9e9a | ||
|
9ab2e69f4f | ||
|
9af2ab57d1 | ||
|
f14e4dc0be | ||
|
39bd9641f4 | ||
|
177c283011 | ||
|
62ff1421e7 | ||
|
2577cbe1ac | ||
|
7542f065a1 | ||
|
aa2abb92ef | ||
|
efc00c1610 | ||
|
7585893cf0 | ||
|
4fd9019c25 | ||
|
f4990aaa95 | ||
|
834fa77385 | ||
|
3acd20c04d | ||
|
878859d7cc | ||
|
7038776ec5 | ||
|
a74001ef6a | ||
|
0bad1794c4 | ||
|
ceec72b869 | ||
|
5c1ed0ec06 | ||
|
ce00fc502b | ||
|
ab82d2af4f | ||
|
2eb3e1aef6 | ||
|
32cb0388cf | ||
|
30f9039a2c | ||
|
8bad63f72d | ||
|
aec4d89550 | ||
|
e8639a37af | ||
|
a92ab2cbe3 | ||
|
fb0bb6e7f3 | ||
|
8dbf870122 | ||
|
50dab48f25 | ||
|
e0c6354ff3 | ||
|
5b03d251fd | ||
|
9c4b69d1ae | ||
|
feba220fff | ||
|
f9efa3bf8f | ||
|
6ebf333982 | ||
|
2c7d07d606 | ||
|
b05185e7cc | ||
|
148547de95 | ||
|
fb6a97789e | ||
|
271f34c7e3 | ||
|
9309f877d1 | ||
|
88db5fb38e | ||
|
4ee0a1b8c5 | ||
|
7617d734e2 | ||
|
a15ae9badb | ||
|
97b6fed909 | ||
|
ab653afae9 | ||
|
df158a741a | ||
|
fb1371ebfa | ||
|
82d5494235 | ||
|
6b754723d9 | ||
|
e003113132 | ||
|
dda492ff87 | ||
|
2459699d27 | ||
|
989acec027 | ||
|
701ab8b24f | ||
|
ed6bfa5fa2 | ||
|
2d2899b68a | ||
|
2d8b80bfa9 | ||
|
c1cc560458 | ||
|
95fb562533 | ||
|
17853ac237 | ||
|
e064a8bbdb | ||
|
8b9e3f1b59 | ||
|
1d38739016 | ||
|
3bb46eaa6f | ||
|
3b1561a99b | ||
|
a61e5c7310 | ||
|
ef8d1ebc0e | ||
|
348b3d3738 | ||
|
3550aa10ba | ||
|
c29eadd9ee | ||
|
aecefaee8d | ||
|
3bab5ec3a7 | ||
|
3c908963ae | ||
|
cba176cdc9 | ||
|
cfdc854409 | ||
|
1d87f78088 | ||
|
63b01376d6 | ||
|
010e518adb | ||
|
6570784b46 | ||
|
63b31f41a7 | ||
|
96eca59afc | ||
|
25978bde8e | ||
|
7691d2b5db | ||
|
edb108322e | ||
|
af3936b68e | ||
|
e76c25f9d3 | ||
|
124f455be2 | ||
|
f679eefe0a |
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -59,7 +59,7 @@ body:
|
|||||||
required: true
|
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
|
||||||
|
21
.github/workflows/alpha.yml
vendored
21
.github/workflows/alpha.yml
vendored
@ -25,7 +25,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
- name: Check if version changed
|
- name: Check if version changed or src changed
|
||||||
id: check
|
id: check
|
||||||
run: |
|
run: |
|
||||||
# For manual workflow_dispatch, always run
|
# For manual workflow_dispatch, always run
|
||||||
@ -50,8 +50,25 @@ jobs:
|
|||||||
if [ "$CURRENT_VERSION" != "$PREVIOUS_VERSION" ]; then
|
if [ "$CURRENT_VERSION" != "$PREVIOUS_VERSION" ]; then
|
||||||
echo "Version changed from $PREVIOUS_VERSION to $CURRENT_VERSION"
|
echo "Version changed from $PREVIOUS_VERSION to $CURRENT_VERSION"
|
||||||
echo "should_run=true" >> $GITHUB_OUTPUT
|
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if src or src-tauri directories changed
|
||||||
|
CURRENT_SRC_HASH=$(git rev-parse HEAD:src)
|
||||||
|
PREVIOUS_SRC_HASH=$(git rev-parse HEAD~1:src 2>/dev/null || echo "")
|
||||||
|
CURRENT_TAURI_HASH=$(git rev-parse HEAD:src-tauri 2>/dev/null || echo "")
|
||||||
|
PREVIOUS_TAURI_HASH=$(git rev-parse HEAD~1:src-tauri 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
echo "Current src hash: $CURRENT_SRC_HASH"
|
||||||
|
echo "Previous src hash: $PREVIOUS_SRC_HASH"
|
||||||
|
echo "Current tauri hash: $CURRENT_TAURI_HASH"
|
||||||
|
echo "Previous tauri hash: $PREVIOUS_TAURI_HASH"
|
||||||
|
|
||||||
|
if [ "$CURRENT_SRC_HASH" != "$PREVIOUS_SRC_HASH" ] || [ "$CURRENT_TAURI_HASH" != "$PREVIOUS_TAURI_HASH" ]; then
|
||||||
|
echo "Source directories changed"
|
||||||
|
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||||
else
|
else
|
||||||
echo "Version unchanged: $CURRENT_VERSION"
|
echo "Version and source directories unchanged"
|
||||||
echo "should_run=false" >> $GITHUB_OUTPUT
|
echo "should_run=false" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
@ -3,12 +3,12 @@
|
|||||||
#pnpm pretty-quick --staged
|
#pnpm pretty-quick --staged
|
||||||
|
|
||||||
# 运行 clippy fmt
|
# 运行 clippy fmt
|
||||||
#cargo fmt --manifest-path ./src-tauri/Cargo.toml
|
cargo fmt --manifest-path ./src-tauri/Cargo.toml
|
||||||
|
|
||||||
# if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
# echo "rustfmt failed to format the code. Please fix the issues and try again."
|
echo "rustfmt failed to format the code. Please fix the issues and try again."
|
||||||
# exit 1
|
exit 1
|
||||||
# fi
|
fi
|
||||||
|
|
||||||
#git add .
|
#git add .
|
||||||
|
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# 运行 clippy
|
# 运行 clippy
|
||||||
#cargo clippy --manifest-path ./src-tauri/Cargo.toml --fix
|
# cargo clippy --manifest-path ./src-tauri/Cargo.toml --fix
|
||||||
|
|
||||||
# 如果 clippy 失败,阻止 push
|
# 如果 clippy 失败,阻止 push
|
||||||
#if [ $? -ne 0 ]; then
|
# if [ $? -ne 0 ]; then
|
||||||
# echo "Clippy found issues in sub_crate. Please fix them before pushing."
|
# echo "Clippy found issues in sub_crate. Please fix them before pushing."
|
||||||
# exit 1
|
# exit 1
|
||||||
#fi
|
# fi
|
||||||
|
|
||||||
# 允许 push
|
# 允许 push
|
||||||
exit 0
|
exit 0
|
||||||
|
76
UPDATELOG.md
76
UPDATELOG.md
@ -1,4 +1,49 @@
|
|||||||
## v2.2.1
|
## 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
|
||||||
|
|
||||||
**发行代号:拓**
|
**发行代号:拓**
|
||||||
|
|
||||||
@ -6,8 +51,24 @@
|
|||||||
|
|
||||||
代号释义: 本次发布在功能上的大幅扩展。新首页设计为用户带来全新交互体验,DNS 覆写功能增强网络控制能力,解锁测试页面助力内容访问自由度提升,轻量模式提供灵活使用选择。此外,macOS 应用菜单集成、sidecar 模式、诊断信息导出等新特性进一步丰富了软件的适用场景。这些新增功能显著拓宽了 Clash Verge 的功能边界,为用户提供了更强大的工具和可能性。
|
代号释义: 本次发布在功能上的大幅扩展。新首页设计为用户带来全新交互体验,DNS 覆写功能增强网络控制能力,解锁测试页面助力内容访问自由度提升,轻量模式提供灵活使用选择。此外,macOS 应用菜单集成、sidecar 模式、诊断信息导出等新特性进一步丰富了软件的适用场景。这些新增功能显著拓宽了 Clash Verge 的功能边界,为用户提供了更强大的工具和可能性。
|
||||||
|
|
||||||
2.2.1 相对于 2.2.0(已下架不在提供)
|
#### 已知问题
|
||||||
修复了:
|
- 仅在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. **首页**
|
1. **首页**
|
||||||
- 修复 Direct 模式首页无法渲染
|
- 修复 Direct 模式首页无法渲染
|
||||||
- 修复 首页启用轻量模式导致 ClashVergeRev 从托盘退出
|
- 修复 首页启用轻量模式导致 ClashVergeRev 从托盘退出
|
||||||
@ -23,7 +84,7 @@
|
|||||||
4. **轻量模式**
|
4. **轻量模式**
|
||||||
- 修复 MacOS 轻量模式下 Dock 栏图标无法隐藏。
|
- 修复 MacOS 轻量模式下 Dock 栏图标无法隐藏。
|
||||||
|
|
||||||
新增了:
|
#### 新增了:
|
||||||
1. **首页**
|
1. **首页**
|
||||||
- 首页文本过长自动截断
|
- 首页文本过长自动截断
|
||||||
2. **轻量模式**
|
2. **轻量模式**
|
||||||
@ -37,7 +98,7 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## v2.2.0(已下架不在提供)
|
## 2.2.0(已下架不再提供)
|
||||||
|
|
||||||
#### 新增功能
|
#### 新增功能
|
||||||
1. **首页**
|
1. **首页**
|
||||||
@ -78,6 +139,7 @@
|
|||||||
- 修复 macOS tray图标错位到左上角的问题。
|
- 修复 macOS tray图标错位到左上角的问题。
|
||||||
- 修复 Windows/Linux 运行时崩溃。
|
- 修复 Windows/Linux 运行时崩溃。
|
||||||
- 修复 Win10 阴影和边框问题。
|
- 修复 Win10 阴影和边框问题。
|
||||||
|
- 修复 升级或重装后开机自启状态检测和同步问题。
|
||||||
|
|
||||||
2. **构建**
|
2. **构建**
|
||||||
- 修复构建失败问题。
|
- 修复构建失败问题。
|
||||||
@ -115,14 +177,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)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "clash-verge",
|
"name": "clash-verge",
|
||||||
"version": "2.2.1",
|
"version": "2.2.3",
|
||||||
"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 -- --profile fast-dev",
|
||||||
@ -17,9 +17,10 @@
|
|||||||
"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/fix-alpha_version.mjs",
|
"fix-alpha-version": "node scripts/fix-alpha_version.mjs",
|
||||||
|
"release-version": "node scripts/release_version.mjs",
|
||||||
"release-alpha-version": "node scripts/release-alpha_version.mjs",
|
"release-alpha-version": "node scripts/release-alpha_version.mjs",
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"clean": "cd ./src-tauri && cargo clean && cd -"
|
"clippy": "cargo clippy --manifest-path ./src-tauri/Cargo.toml"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
197
scripts/release_version.mjs
Normal file
197
scripts/release_version.mjs
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
import fs from "fs/promises";
|
||||||
|
import path from "path";
|
||||||
|
import { program } from "commander";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证版本号格式
|
||||||
|
* @param {string} version
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function isValidVersion(version) {
|
||||||
|
return /^v?\d+\.\d+\.\d+(-(alpha|beta|rc)(\.\d+)?)?$/i.test(version);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标准化版本号(确保v前缀可选)
|
||||||
|
* @param {string} version
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function normalizeVersion(version) {
|
||||||
|
return version.startsWith("v") ? version : `v${version}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新 package.json 文件中的版本号
|
||||||
|
* @param {string} newVersion 新版本号
|
||||||
|
*/
|
||||||
|
async function updatePackageVersion(newVersion) {
|
||||||
|
const _dirname = process.cwd();
|
||||||
|
const packageJsonPath = path.join(_dirname, "package.json");
|
||||||
|
try {
|
||||||
|
const data = await fs.readFile(packageJsonPath, "utf8");
|
||||||
|
const packageJson = JSON.parse(data);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"[INFO]: Current package.json version is: ",
|
||||||
|
packageJson.version,
|
||||||
|
);
|
||||||
|
packageJson.version = newVersion.startsWith("v")
|
||||||
|
? newVersion.slice(1)
|
||||||
|
: newVersion;
|
||||||
|
await fs.writeFile(
|
||||||
|
packageJsonPath,
|
||||||
|
JSON.stringify(packageJson, null, 2),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
`[INFO]: package.json version updated to: ${packageJson.version}`,
|
||||||
|
);
|
||||||
|
return packageJson.version;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating package.json version:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新 Cargo.toml 文件中的版本号
|
||||||
|
* @param {string} newVersion 新版本号
|
||||||
|
*/
|
||||||
|
async function updateCargoVersion(newVersion) {
|
||||||
|
const _dirname = process.cwd();
|
||||||
|
const cargoTomlPath = path.join(_dirname, "src-tauri", "Cargo.toml");
|
||||||
|
try {
|
||||||
|
const data = await fs.readFile(cargoTomlPath, "utf8");
|
||||||
|
const lines = data.split("\n");
|
||||||
|
|
||||||
|
const versionWithoutV = newVersion.startsWith("v")
|
||||||
|
? newVersion.slice(1)
|
||||||
|
: newVersion;
|
||||||
|
|
||||||
|
const updatedLines = lines.map((line) => {
|
||||||
|
if (line.trim().startsWith("version =")) {
|
||||||
|
return line.replace(
|
||||||
|
/version\s*=\s*"[^"]+"/,
|
||||||
|
`version = "${versionWithoutV}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return line;
|
||||||
|
});
|
||||||
|
|
||||||
|
await fs.writeFile(cargoTomlPath, updatedLines.join("\n"), "utf8");
|
||||||
|
console.log(`[INFO]: Cargo.toml version updated to: ${versionWithoutV}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating Cargo.toml version:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新 tauri.conf.json 文件中的版本号
|
||||||
|
* @param {string} newVersion 新版本号
|
||||||
|
*/
|
||||||
|
async function updateTauriConfigVersion(newVersion) {
|
||||||
|
const _dirname = process.cwd();
|
||||||
|
const tauriConfigPath = path.join(_dirname, "src-tauri", "tauri.conf.json");
|
||||||
|
try {
|
||||||
|
const data = await fs.readFile(tauriConfigPath, "utf8");
|
||||||
|
const tauriConfig = JSON.parse(data);
|
||||||
|
|
||||||
|
const versionWithoutV = newVersion.startsWith("v")
|
||||||
|
? newVersion.slice(1)
|
||||||
|
: newVersion;
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"[INFO]: Current tauri.conf.json version is: ",
|
||||||
|
tauriConfig.version,
|
||||||
|
);
|
||||||
|
tauriConfig.version = versionWithoutV;
|
||||||
|
await fs.writeFile(
|
||||||
|
tauriConfigPath,
|
||||||
|
JSON.stringify(tauriConfig, null, 2),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
`[INFO]: tauri.conf.json version updated to: ${versionWithoutV}`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating tauri.conf.json version:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前版本号(从package.json)
|
||||||
|
*/
|
||||||
|
async function getCurrentVersion() {
|
||||||
|
const _dirname = process.cwd();
|
||||||
|
const packageJsonPath = path.join(_dirname, "package.json");
|
||||||
|
try {
|
||||||
|
const data = await fs.readFile(packageJsonPath, "utf8");
|
||||||
|
const packageJson = JSON.parse(data);
|
||||||
|
return packageJson.version;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error getting current version:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主函数,更新所有文件的版本号
|
||||||
|
* @param {string} versionArg 版本参数(可以是标签或完整版本号)
|
||||||
|
*/
|
||||||
|
async function main(versionArg) {
|
||||||
|
if (!versionArg) {
|
||||||
|
console.error("Error: Version argument is required");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let newVersion;
|
||||||
|
const validTags = ["alpha", "beta", "rc"];
|
||||||
|
|
||||||
|
// 判断参数是标签还是完整版本号
|
||||||
|
if (validTags.includes(versionArg.toLowerCase())) {
|
||||||
|
// 标签模式:在当前版本基础上添加标签
|
||||||
|
const currentVersion = await getCurrentVersion();
|
||||||
|
const baseVersion = currentVersion.replace(
|
||||||
|
/-(alpha|beta|rc)(\.\d+)?$/i,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
newVersion = `${baseVersion}-${versionArg.toLowerCase()}`;
|
||||||
|
} else {
|
||||||
|
// 完整版本号模式
|
||||||
|
if (!isValidVersion(versionArg)) {
|
||||||
|
console.error(
|
||||||
|
"Error: Invalid version format. Expected format: vX.X.X or vX.X.X-tag (e.g. v2.2.3 or v2.2.3-alpha)",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
newVersion = normalizeVersion(versionArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[INFO]: Updating versions to: ${newVersion}`);
|
||||||
|
await updatePackageVersion(newVersion);
|
||||||
|
await updateCargoVersion(newVersion);
|
||||||
|
await updateTauriConfigVersion(newVersion);
|
||||||
|
console.log("[SUCCESS]: All version updates completed successfully!");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[ERROR]: Failed to update versions:", error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example:
|
||||||
|
// pnpm release-version 2.2.3-alpha
|
||||||
|
// 设置命令行界面
|
||||||
|
program
|
||||||
|
.name("pnpm release-version")
|
||||||
|
.description(
|
||||||
|
"Update project version numbers. Can add tag (alpha/beta/rc) or set full version (e.g. v2.2.3 or v2.2.3-alpha)",
|
||||||
|
)
|
||||||
|
.argument(
|
||||||
|
"<version>",
|
||||||
|
"version tag (alpha/beta/rc) or full version (e.g. v2.2.3 or v2.2.3-alpha)",
|
||||||
|
)
|
||||||
|
.action(main)
|
||||||
|
.parse(process.argv);
|
@ -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,224 +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(() =>
|
|
||||||
resolveUpdateLogDefault().catch(() => "No changelog available"),
|
|
||||||
),
|
|
||||||
pub_date: new Date().toISOString(),
|
|
||||||
platforms: {
|
|
||||||
win64: { signature: "", url: "" }, // compatible with older formats
|
|
||||||
linux: { signature: "", url: "" }, // compatible with older formats
|
|
||||||
darwin: { signature: "", url: "" }, // compatible with older formats
|
|
||||||
"darwin-aarch64": { signature: "", url: "" },
|
|
||||||
"darwin-intel": { signature: "", url: "" },
|
|
||||||
"darwin-x86_64": { signature: "", url: "" },
|
|
||||||
"linux-x86_64": { signature: "", url: "" },
|
|
||||||
"linux-x86": { signature: "", url: "" },
|
|
||||||
"linux-i686": { signature: "", url: "" },
|
|
||||||
"linux-aarch64": { signature: "", url: "" },
|
|
||||||
"linux-armv7": { signature: "", url: "" },
|
|
||||||
"windows-x86_64": { signature: "", url: "" },
|
|
||||||
"windows-aarch64": { signature: "", url: "" },
|
|
||||||
"windows-x86": { signature: "", url: "" },
|
|
||||||
"windows-i686": { signature: "", url: "" },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const promises = release.assets.map(async (asset) => {
|
|
||||||
const { name, browser_download_url } = asset;
|
|
||||||
|
|
||||||
// Process all the platform URL and signature data
|
|
||||||
// win64 url
|
|
||||||
if (name.endsWith("x64-setup.exe")) {
|
|
||||||
updateData.platforms.win64.url = browser_download_url;
|
|
||||||
updateData.platforms["windows-x86_64"].url = browser_download_url;
|
|
||||||
}
|
|
||||||
// win64 signature
|
|
||||||
if (name.endsWith("x64-setup.exe.sig")) {
|
|
||||||
const sig = await getSignature(browser_download_url);
|
|
||||||
updateData.platforms.win64.signature = sig;
|
|
||||||
updateData.platforms["windows-x86_64"].signature = sig;
|
|
||||||
}
|
|
||||||
|
|
||||||
// win32 url
|
|
||||||
if (name.endsWith("x86-setup.exe")) {
|
|
||||||
updateData.platforms["windows-x86"].url = browser_download_url;
|
|
||||||
updateData.platforms["windows-i686"].url = browser_download_url;
|
|
||||||
}
|
|
||||||
// win32 signature
|
|
||||||
if (name.endsWith("x86-setup.exe.sig")) {
|
|
||||||
const sig = await getSignature(browser_download_url);
|
|
||||||
updateData.platforms["windows-x86"].signature = sig;
|
|
||||||
updateData.platforms["windows-i686"].signature = sig;
|
|
||||||
}
|
|
||||||
|
|
||||||
// win arm url
|
|
||||||
if (name.endsWith("arm64-setup.exe")) {
|
|
||||||
updateData.platforms["windows-aarch64"].url = browser_download_url;
|
|
||||||
}
|
|
||||||
// win arm signature
|
|
||||||
if (name.endsWith("arm64-setup.exe.sig")) {
|
|
||||||
const sig = await getSignature(browser_download_url);
|
|
||||||
updateData.platforms["windows-aarch64"].signature = sig;
|
|
||||||
}
|
|
||||||
|
|
||||||
// darwin url (intel)
|
|
||||||
if (name.endsWith(".app.tar.gz") && !name.includes("aarch")) {
|
|
||||||
updateData.platforms.darwin.url = browser_download_url;
|
|
||||||
updateData.platforms["darwin-intel"].url = browser_download_url;
|
|
||||||
updateData.platforms["darwin-x86_64"].url = browser_download_url;
|
|
||||||
}
|
|
||||||
// darwin signature (intel)
|
|
||||||
if (name.endsWith(".app.tar.gz.sig") && !name.includes("aarch")) {
|
|
||||||
const sig = await getSignature(browser_download_url);
|
|
||||||
updateData.platforms.darwin.signature = sig;
|
|
||||||
updateData.platforms["darwin-intel"].signature = sig;
|
|
||||||
updateData.platforms["darwin-x86_64"].signature = sig;
|
|
||||||
}
|
|
||||||
|
|
||||||
// darwin url (aarch)
|
|
||||||
if (name.endsWith("aarch64.app.tar.gz")) {
|
|
||||||
updateData.platforms["darwin-aarch64"].url = browser_download_url;
|
|
||||||
// 使linux可以检查更新
|
|
||||||
updateData.platforms.linux.url = browser_download_url;
|
|
||||||
updateData.platforms["linux-x86_64"].url = browser_download_url;
|
|
||||||
updateData.platforms["linux-x86"].url = browser_download_url;
|
|
||||||
updateData.platforms["linux-i686"].url = browser_download_url;
|
|
||||||
updateData.platforms["linux-aarch64"].url = browser_download_url;
|
|
||||||
updateData.platforms["linux-armv7"].url = browser_download_url;
|
|
||||||
}
|
|
||||||
// darwin signature (aarch)
|
|
||||||
if (name.endsWith("aarch64.app.tar.gz.sig")) {
|
|
||||||
const sig = await getSignature(browser_download_url);
|
|
||||||
updateData.platforms["darwin-aarch64"].signature = sig;
|
|
||||||
updateData.platforms.linux.signature = sig;
|
|
||||||
updateData.platforms["linux-x86_64"].signature = sig;
|
|
||||||
updateData.platforms["linux-x86"].url = browser_download_url;
|
|
||||||
updateData.platforms["linux-i686"].url = browser_download_url;
|
|
||||||
updateData.platforms["linux-aarch64"].signature = sig;
|
|
||||||
updateData.platforms["linux-armv7"].signature = sig;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.allSettled(promises);
|
|
||||||
console.log(updateData);
|
|
||||||
|
|
||||||
// maybe should test the signature as well
|
|
||||||
// delete the null field
|
|
||||||
Object.entries(updateData.platforms).forEach(([key, value]) => {
|
|
||||||
if (!value.url) {
|
|
||||||
console.log(`[Error]: failed to parse release for "${key}"`);
|
|
||||||
delete updateData.platforms[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Generate a proxy update file for accelerated GitHub resources
|
|
||||||
const updateDataNew = JSON.parse(JSON.stringify(updateData));
|
|
||||||
|
|
||||||
Object.entries(updateDataNew.platforms).forEach(([key, value]) => {
|
|
||||||
if (value.url) {
|
|
||||||
updateDataNew.platforms[key].url =
|
|
||||||
"https://download.clashverge.dev/" + value.url;
|
|
||||||
} else {
|
|
||||||
console.log(`[Error]: updateDataNew.platforms.${key} is null`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get the appropriate updater release based on isAlpha flag
|
|
||||||
const releaseTag = isAlpha ? ALPHA_TAG_NAME : UPDATE_TAG_NAME;
|
|
||||||
console.log(
|
|
||||||
`Processing ${isAlpha ? "alpha" : "stable"} release:`,
|
|
||||||
releaseTag,
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let updateRelease;
|
const { data: release } = await github.rest.repos.getReleaseByTag({
|
||||||
|
...options,
|
||||||
|
tag: tag.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateData = {
|
||||||
|
name: tag.name,
|
||||||
|
notes: await resolveUpdateLog(tag.name).catch(() =>
|
||||||
|
resolveUpdateLogDefault().catch(() => "No changelog available"),
|
||||||
|
),
|
||||||
|
pub_date: new Date().toISOString(),
|
||||||
|
platforms: {
|
||||||
|
win64: { signature: "", url: "" }, // compatible with older formats
|
||||||
|
linux: { signature: "", url: "" }, // compatible with older formats
|
||||||
|
darwin: { signature: "", url: "" }, // compatible with older formats
|
||||||
|
"darwin-aarch64": { signature: "", url: "" },
|
||||||
|
"darwin-intel": { signature: "", url: "" },
|
||||||
|
"darwin-x86_64": { signature: "", url: "" },
|
||||||
|
"linux-x86_64": { signature: "", url: "" },
|
||||||
|
"linux-x86": { signature: "", url: "" },
|
||||||
|
"linux-i686": { signature: "", url: "" },
|
||||||
|
"linux-aarch64": { signature: "", url: "" },
|
||||||
|
"linux-armv7": { signature: "", url: "" },
|
||||||
|
"windows-x86_64": { signature: "", url: "" },
|
||||||
|
"windows-aarch64": { signature: "", url: "" },
|
||||||
|
"windows-x86": { signature: "", url: "" },
|
||||||
|
"windows-i686": { signature: "", url: "" },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const promises = release.assets.map(async (asset) => {
|
||||||
|
const { name, browser_download_url } = asset;
|
||||||
|
|
||||||
|
// Process all the platform URL and signature data
|
||||||
|
// win64 url
|
||||||
|
if (name.endsWith("x64-setup.exe")) {
|
||||||
|
updateData.platforms.win64.url = browser_download_url;
|
||||||
|
updateData.platforms["windows-x86_64"].url = browser_download_url;
|
||||||
|
}
|
||||||
|
// win64 signature
|
||||||
|
if (name.endsWith("x64-setup.exe.sig")) {
|
||||||
|
const sig = await getSignature(browser_download_url);
|
||||||
|
updateData.platforms.win64.signature = sig;
|
||||||
|
updateData.platforms["windows-x86_64"].signature = sig;
|
||||||
|
}
|
||||||
|
|
||||||
|
// win32 url
|
||||||
|
if (name.endsWith("x86-setup.exe")) {
|
||||||
|
updateData.platforms["windows-x86"].url = browser_download_url;
|
||||||
|
updateData.platforms["windows-i686"].url = browser_download_url;
|
||||||
|
}
|
||||||
|
// win32 signature
|
||||||
|
if (name.endsWith("x86-setup.exe.sig")) {
|
||||||
|
const sig = await getSignature(browser_download_url);
|
||||||
|
updateData.platforms["windows-x86"].signature = sig;
|
||||||
|
updateData.platforms["windows-i686"].signature = sig;
|
||||||
|
}
|
||||||
|
|
||||||
|
// win arm url
|
||||||
|
if (name.endsWith("arm64-setup.exe")) {
|
||||||
|
updateData.platforms["windows-aarch64"].url = browser_download_url;
|
||||||
|
}
|
||||||
|
// win arm signature
|
||||||
|
if (name.endsWith("arm64-setup.exe.sig")) {
|
||||||
|
const sig = await getSignature(browser_download_url);
|
||||||
|
updateData.platforms["windows-aarch64"].signature = sig;
|
||||||
|
}
|
||||||
|
|
||||||
|
// darwin url (intel)
|
||||||
|
if (name.endsWith(".app.tar.gz") && !name.includes("aarch")) {
|
||||||
|
updateData.platforms.darwin.url = browser_download_url;
|
||||||
|
updateData.platforms["darwin-intel"].url = browser_download_url;
|
||||||
|
updateData.platforms["darwin-x86_64"].url = browser_download_url;
|
||||||
|
}
|
||||||
|
// darwin signature (intel)
|
||||||
|
if (name.endsWith(".app.tar.gz.sig") && !name.includes("aarch")) {
|
||||||
|
const sig = await getSignature(browser_download_url);
|
||||||
|
updateData.platforms.darwin.signature = sig;
|
||||||
|
updateData.platforms["darwin-intel"].signature = sig;
|
||||||
|
updateData.platforms["darwin-x86_64"].signature = sig;
|
||||||
|
}
|
||||||
|
|
||||||
|
// darwin url (aarch)
|
||||||
|
if (name.endsWith("aarch64.app.tar.gz")) {
|
||||||
|
updateData.platforms["darwin-aarch64"].url = browser_download_url;
|
||||||
|
// 使linux可以检查更新
|
||||||
|
updateData.platforms.linux.url = browser_download_url;
|
||||||
|
updateData.platforms["linux-x86_64"].url = browser_download_url;
|
||||||
|
updateData.platforms["linux-x86"].url = browser_download_url;
|
||||||
|
updateData.platforms["linux-i686"].url = browser_download_url;
|
||||||
|
updateData.platforms["linux-aarch64"].url = browser_download_url;
|
||||||
|
updateData.platforms["linux-armv7"].url = browser_download_url;
|
||||||
|
}
|
||||||
|
// darwin signature (aarch)
|
||||||
|
if (name.endsWith("aarch64.app.tar.gz.sig")) {
|
||||||
|
const sig = await getSignature(browser_download_url);
|
||||||
|
updateData.platforms["darwin-aarch64"].signature = sig;
|
||||||
|
updateData.platforms.linux.signature = sig;
|
||||||
|
updateData.platforms["linux-x86_64"].signature = sig;
|
||||||
|
updateData.platforms["linux-x86"].url = browser_download_url;
|
||||||
|
updateData.platforms["linux-i686"].url = browser_download_url;
|
||||||
|
updateData.platforms["linux-aarch64"].signature = sig;
|
||||||
|
updateData.platforms["linux-armv7"].signature = sig;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.allSettled(promises);
|
||||||
|
console.log(updateData);
|
||||||
|
|
||||||
|
// maybe should test the signature as well
|
||||||
|
// delete the null field
|
||||||
|
Object.entries(updateData.platforms).forEach(([key, value]) => {
|
||||||
|
if (!value.url) {
|
||||||
|
console.log(`[Error]: failed to parse release for "${key}"`);
|
||||||
|
delete updateData.platforms[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generate a proxy update file for accelerated GitHub resources
|
||||||
|
const updateDataNew = JSON.parse(JSON.stringify(updateData));
|
||||||
|
|
||||||
|
Object.entries(updateDataNew.platforms).forEach(([key, value]) => {
|
||||||
|
if (value.url) {
|
||||||
|
updateDataNew.platforms[key].url =
|
||||||
|
"https://download.clashverge.dev/" + value.url;
|
||||||
|
} else {
|
||||||
|
console.log(`[Error]: updateDataNew.platforms.${key} is null`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get the appropriate updater release based on isAlpha flag
|
||||||
|
const releaseTag = isAlpha ? ALPHA_TAG_NAME : UPDATE_TAG_NAME;
|
||||||
|
console.log(
|
||||||
|
`Processing ${isAlpha ? "alpha" : "stable"} release:`,
|
||||||
|
releaseTag,
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Try to get the existing release
|
let updateRelease;
|
||||||
const response = await github.rest.repos.getReleaseByTag({
|
|
||||||
|
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,
|
...options,
|
||||||
tag: releaseTag,
|
release_id: updateRelease.id,
|
||||||
|
name: jsonFile,
|
||||||
|
data: JSON.stringify(updateData, null, 2),
|
||||||
});
|
});
|
||||||
updateRelease = response.data;
|
|
||||||
|
await github.rest.repos.uploadReleaseAsset({
|
||||||
|
...options,
|
||||||
|
release_id: updateRelease.id,
|
||||||
|
name: proxyFile,
|
||||||
|
data: JSON.stringify(updateDataNew, null, 2),
|
||||||
|
});
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`Found existing ${releaseTag} release with ID: ${updateRelease.id}`,
|
`Successfully uploaded ${isAlpha ? "alpha" : "stable"} update files to ${releaseTag}`,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If release doesn't exist, create it
|
console.error(
|
||||||
if (error.status === 404) {
|
`Failed to process ${isAlpha ? "alpha" : "stable"} release:`,
|
||||||
console.log(
|
error.message,
|
||||||
`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) {
|
} 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
641
src-tauri/Cargo.lock
generated
641
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.2.1"
|
version = "2.2.3"
|
||||||
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,73 +13,72 @@ 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.1.0", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
warp = "0.3"
|
warp = "0.3"
|
||||||
anyhow = "1.0.97"
|
anyhow = "1.0.97"
|
||||||
dirs = "6.0"
|
dirs = "6.0"
|
||||||
open = "5.1"
|
open = "5.3"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
dunce = "1.0"
|
dunce = "1.0"
|
||||||
log4rs = "1"
|
log4rs = "1"
|
||||||
nanoid = "0.4"
|
nanoid = "0.4"
|
||||||
chrono = "0.4.40"
|
chrono = "0.4.40"
|
||||||
sysinfo = "0.33.1"
|
sysinfo = "0.34"
|
||||||
boa_engine = "0.20.0"
|
boa_engine = "0.20.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde_yaml = "0.9"
|
serde_yaml = "0.9"
|
||||||
once_cell = "1.20.3"
|
once_cell = "1.21.3"
|
||||||
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"
|
||||||
percent-encoding = "2.3.1"
|
percent-encoding = "2.3.1"
|
||||||
fs2 = "0.4.3"
|
tokio = { version = "1.44", features = [
|
||||||
window-shadows = { version = "0.2.2" }
|
"rt-multi-thread",
|
||||||
tokio = { version = "1.43", features = ["full"] }
|
"macros",
|
||||||
|
"time",
|
||||||
|
"sync",
|
||||||
|
] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
reqwest = { version = "0.12", features = ["json", "rustls-tls", "cookies"] }
|
reqwest = { version = "0.12", features = ["json", "rustls-tls", "cookies"] }
|
||||||
regex = "1.10.5"
|
regex = "1.11.1"
|
||||||
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs", rev = "3d748b5" }
|
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs", rev = "3d748b5" }
|
||||||
image = "0.25.5"
|
image = "0.25.6"
|
||||||
imageproc = "0.25.0"
|
imageproc = "0.25.0"
|
||||||
rusttype = "0.9"
|
tauri = { version = "2.4.0", features = [
|
||||||
tauri = { version = "2.3.1", features = [
|
"protocol-asset",
|
||||||
"protocol-asset",
|
"devtools",
|
||||||
"devtools",
|
"tray-icon",
|
||||||
"tray-icon",
|
"image-ico",
|
||||||
"image-ico",
|
"image-png",
|
||||||
"image-png",
|
|
||||||
] }
|
] }
|
||||||
network-interface = { version = "2.0.0", features = ["serde"] }
|
network-interface = { version = "2.0.1", features = ["serde"] }
|
||||||
tauri-plugin-shell = "2.2.0"
|
tauri-plugin-shell = "2.2.0"
|
||||||
tauri-plugin-dialog = "2.2.0"
|
tauri-plugin-dialog = "2.2.0"
|
||||||
tauri-plugin-fs = "2.2.0"
|
tauri-plugin-fs = "2.2.0"
|
||||||
tauri-plugin-notification = "2.2.1"
|
|
||||||
tauri-plugin-process = "2.2.0"
|
tauri-plugin-process = "2.2.0"
|
||||||
tauri-plugin-clipboard-manager = "2.2.1"
|
tauri-plugin-clipboard-manager = "2.2.2"
|
||||||
tauri-plugin-deep-link = "2.2.0"
|
tauri-plugin-deep-link = "2.2.0"
|
||||||
tauri-plugin-devtools = "2.0.0-rc"
|
tauri-plugin-devtools = "2.0.0"
|
||||||
url = "2.5.4"
|
zip = "2.5.0"
|
||||||
zip = "2.2.3"
|
reqwest_dav = "0.1.15"
|
||||||
reqwest_dav = "0.1.14"
|
|
||||||
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.2"
|
||||||
tokio-tungstenite = "0.26.2"
|
tokio-tungstenite = "0.26.2"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
sys-locale = "0.3.1"
|
sys-locale = "0.3.2"
|
||||||
async-trait = "0.1.87"
|
async-trait = "0.1.88"
|
||||||
mihomo_api = { path = "src_crates/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.26.2"
|
||||||
|
libc = "0.2"
|
||||||
|
|
||||||
[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"
|
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
users = "0.11.0"
|
users = "0.11.0"
|
||||||
@ -87,9 +86,8 @@ 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.2.0"
|
||||||
tauri-plugin-global-shortcut = "2.2.0"
|
tauri-plugin-global-shortcut = "2.2.0"
|
||||||
tauri-plugin-updater = "2.5.1"
|
tauri-plugin-updater = "2.6.1"
|
||||||
tauri-plugin-window-state = "2.2.1"
|
tauri-plugin-window-state = "2.2.1"
|
||||||
#openssl
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["custom-protocol"]
|
default = ["custom-protocol"]
|
||||||
@ -107,31 +105,33 @@ strip = true
|
|||||||
incremental = true
|
incremental = true
|
||||||
|
|
||||||
[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]
|
[profile.fast-dev]
|
||||||
inherits = "dev" # 继承 dev 的配置
|
inherits = "dev" # 继承 dev 的配置
|
||||||
codegen-units = 256 # 增加编译单元,提升编译速度
|
codegen-units = 256 # 增加编译单元,提升编译速度
|
||||||
opt-level = 0 # 禁用优化,进一步提升编译速度
|
opt-level = 0 # 禁用优化,进一步提升编译速度
|
||||||
incremental = true # 启用增量编译
|
incremental = true # 启用增量编译
|
||||||
debug = true # 保留调试信息
|
debug = true # 保留调试信息
|
||||||
strip = false # 不剥离符号,保留调试信息
|
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.19.1"
|
||||||
mockito = "1.7.0"
|
|
||||||
tempfile = "3.17.1"
|
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = ["src_crates/crate_mihomo_api"]
|
||||||
"src_crates/crate_mihomo_api",]
|
|
||||||
|
# [patch.crates-io]
|
||||||
|
# bitflags = { git = "https://github.com/bitflags/bitflags", rev = "2.9.0" }
|
||||||
|
# zerocopy = { git = "https://github.com/google/zerocopy", rev = "v0.8.24" }
|
||||||
|
# tungstenite = { git = "https://github.com/snapview/tungstenite-rs", rev = "v0.26.2" }
|
||||||
|
@ -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"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
|
||||||
/// 打开应用程序所在目录
|
/// 打开应用程序所在目录
|
||||||
@ -194,7 +198,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()),
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
use super::CmdResult;
|
|
||||||
use crate::{core::CoreManager, wrap_err};
|
|
||||||
|
|
||||||
/// 修复系统服务
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn repair_service() -> CmdResult {
|
|
||||||
wrap_err!(CoreManager::global().repair_service().await)
|
|
||||||
}
|
|
@ -2,11 +2,9 @@ use chrono::Local;
|
|||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::{collections::HashMap, sync::Arc};
|
||||||
use std::sync::Arc;
|
|
||||||
use tauri::command;
|
use tauri::command;
|
||||||
use tokio::sync::Mutex;
|
use tokio::{sync::Mutex, task::JoinSet};
|
||||||
use tokio::task::JoinSet;
|
|
||||||
|
|
||||||
// 定义解锁测试项目的结构
|
// 定义解锁测试项目的结构
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@ -464,9 +462,11 @@ async fn check_netflix(client: &Client) -> UnlockItem {
|
|||||||
let url2 = "https://www.netflix.com/title/70143836"; // Breaking Bad
|
let url2 = "https://www.netflix.com/title/70143836"; // Breaking Bad
|
||||||
|
|
||||||
// 创建简单的请求(不添加太多头部信息)
|
// 创建简单的请求(不添加太多头部信息)
|
||||||
let result1 = client.get(url1)
|
let result1 = client
|
||||||
|
.get(url1)
|
||||||
.timeout(std::time::Duration::from_secs(30))
|
.timeout(std::time::Duration::from_secs(30))
|
||||||
.send().await;
|
.send()
|
||||||
|
.await;
|
||||||
|
|
||||||
// 检查连接失败情况
|
// 检查连接失败情况
|
||||||
if let Err(e) = &result1 {
|
if let Err(e) = &result1 {
|
||||||
@ -480,9 +480,11 @@ async fn check_netflix(client: &Client) -> UnlockItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 如果第一个请求成功,尝试第二个请求
|
// 如果第一个请求成功,尝试第二个请求
|
||||||
let result2 = client.get(url2)
|
let result2 = client
|
||||||
|
.get(url2)
|
||||||
.timeout(std::time::Duration::from_secs(30))
|
.timeout(std::time::Duration::from_secs(30))
|
||||||
.send().await;
|
.send()
|
||||||
|
.await;
|
||||||
|
|
||||||
if let Err(e) = &result2 {
|
if let Err(e) = &result2 {
|
||||||
eprintln!("Netflix请求错误: {}", e);
|
eprintln!("Netflix请求错误: {}", e);
|
||||||
@ -521,7 +523,8 @@ async fn check_netflix(client: &Client) -> UnlockItem {
|
|||||||
// 成功解锁,尝试获取地区信息
|
// 成功解锁,尝试获取地区信息
|
||||||
// 使用Netflix测试内容获取区域
|
// 使用Netflix测试内容获取区域
|
||||||
let test_url = "https://www.netflix.com/title/80018499";
|
let test_url = "https://www.netflix.com/title/80018499";
|
||||||
match client.get(test_url)
|
match client
|
||||||
|
.get(test_url)
|
||||||
.timeout(std::time::Duration::from_secs(30))
|
.timeout(std::time::Duration::from_secs(30))
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
@ -561,7 +564,7 @@ async fn check_netflix(client: &Client) -> UnlockItem {
|
|||||||
region: None,
|
region: None,
|
||||||
check_time: Some(get_local_date_string()),
|
check_time: Some(get_local_date_string()),
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 其他未知错误状态
|
// 其他未知错误状态
|
||||||
@ -578,11 +581,13 @@ async fn check_netflix(client: &Client) -> UnlockItem {
|
|||||||
async fn check_netflix_cdn(client: &Client) -> UnlockItem {
|
async fn check_netflix_cdn(client: &Client) -> UnlockItem {
|
||||||
// Fast.com API URL
|
// Fast.com API URL
|
||||||
let url = "https://api.fast.com/netflix/speedtest/v2?https=true&token=YXNkZmFzZGxmbnNkYWZoYXNkZmhrYWxm&urlCount=5";
|
let url = "https://api.fast.com/netflix/speedtest/v2?https=true&token=YXNkZmFzZGxmbnNkYWZoYXNkZmhrYWxm&urlCount=5";
|
||||||
|
|
||||||
let result = client.get(url)
|
let result = client
|
||||||
|
.get(url)
|
||||||
.timeout(std::time::Duration::from_secs(30))
|
.timeout(std::time::Duration::from_secs(30))
|
||||||
.send().await;
|
.send()
|
||||||
|
.await;
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
// 检查状态码
|
// 检查状态码
|
||||||
@ -594,7 +599,7 @@ async fn check_netflix_cdn(client: &Client) -> UnlockItem {
|
|||||||
check_time: Some(get_local_date_string()),
|
check_time: Some(get_local_date_string()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 尝试解析响应
|
// 尝试解析响应
|
||||||
match response.json::<serde_json::Value>().await {
|
match response.json::<serde_json::Value>().await {
|
||||||
Ok(data) => {
|
Ok(data) => {
|
||||||
@ -602,7 +607,9 @@ async fn check_netflix_cdn(client: &Client) -> UnlockItem {
|
|||||||
if let Some(targets) = data.get("targets").and_then(|t| t.as_array()) {
|
if let Some(targets) = data.get("targets").and_then(|t| t.as_array()) {
|
||||||
if !targets.is_empty() {
|
if !targets.is_empty() {
|
||||||
if let Some(location) = targets[0].get("location") {
|
if let Some(location) = targets[0].get("location") {
|
||||||
if let Some(country) = location.get("country").and_then(|c| c.as_str()) {
|
if let Some(country) =
|
||||||
|
location.get("country").and_then(|c| c.as_str())
|
||||||
|
{
|
||||||
let emoji = country_code_to_emoji(country);
|
let emoji = country_code_to_emoji(country);
|
||||||
return UnlockItem {
|
return UnlockItem {
|
||||||
name: "Netflix".to_string(),
|
name: "Netflix".to_string(),
|
||||||
@ -614,14 +621,14 @@ async fn check_netflix_cdn(client: &Client) -> UnlockItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果无法解析区域信息
|
// 如果无法解析区域信息
|
||||||
return UnlockItem {
|
UnlockItem {
|
||||||
name: "Netflix".to_string(),
|
name: "Netflix".to_string(),
|
||||||
status: "Unknown".to_string(),
|
status: "Unknown".to_string(),
|
||||||
region: None,
|
region: None,
|
||||||
check_time: Some(get_local_date_string()),
|
check_time: Some(get_local_date_string()),
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("解析Fast.com API响应失败: {}", e);
|
eprintln!("解析Fast.com API响应失败: {}", e);
|
||||||
|
@ -6,33 +6,33 @@ pub type CmdResult<T = ()> = Result<T, String>;
|
|||||||
// Command modules
|
// Command modules
|
||||||
pub mod app;
|
pub mod app;
|
||||||
pub mod clash;
|
pub mod clash;
|
||||||
pub mod core;
|
pub mod lightweight;
|
||||||
pub mod media_unlock_checker;
|
pub mod media_unlock_checker;
|
||||||
pub mod network;
|
pub mod network;
|
||||||
pub mod profile;
|
pub mod profile;
|
||||||
pub mod proxy;
|
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 uwp;
|
pub mod uwp;
|
||||||
pub mod validate;
|
pub mod validate;
|
||||||
pub mod verge;
|
pub mod verge;
|
||||||
pub mod webdav;
|
pub mod webdav;
|
||||||
pub mod lighteweight;
|
|
||||||
|
|
||||||
// Re-export all command functions for backwards compatibility
|
// Re-export all command functions for backwards compatibility
|
||||||
pub use app::*;
|
pub use app::*;
|
||||||
pub use clash::*;
|
pub use clash::*;
|
||||||
pub use core::*;
|
pub use lightweight::*;
|
||||||
pub use media_unlock_checker::*;
|
pub use media_unlock_checker::*;
|
||||||
pub use network::*;
|
pub use network::*;
|
||||||
pub use profile::*;
|
pub use profile::*;
|
||||||
pub use proxy::*;
|
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 uwp::*;
|
pub use uwp::*;
|
||||||
pub use validate::*;
|
pub use validate::*;
|
||||||
pub use verge::*;
|
pub use verge::*;
|
||||||
pub use webdav::*;
|
pub use webdav::*;
|
||||||
pub use lighteweight::*;
|
|
||||||
|
@ -1,40 +1,25 @@
|
|||||||
use super::CmdResult;
|
use super::CmdResult;
|
||||||
use crate::{
|
use crate::{
|
||||||
config::*,
|
config::{Config, IProfiles, PrfItem, PrfOption},
|
||||||
core::*,
|
core::{handle, tray::Tray, CoreManager},
|
||||||
feat, log_err, ret_err,
|
feat, logging, ret_err,
|
||||||
utils::{dirs, help},
|
utils::{dirs, help, logging::Type},
|
||||||
wrap_err,
|
wrap_err,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// 获取配置文件列表
|
/// 获取配置文件列表
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_profiles() -> CmdResult<IProfiles> {
|
pub fn get_profiles() -> CmdResult<IProfiles> {
|
||||||
let _ = tray::Tray::global().update_menu();
|
let _ = Tray::global().update_menu();
|
||||||
Ok(Config::profiles().data().clone())
|
Ok(Config::profiles().data().clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 增强配置文件
|
/// 增强配置文件
|
||||||
#[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(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 导入配置文件
|
/// 导入配置文件
|
||||||
@ -77,33 +62,113 @@ pub async fn delete_profile(index: String) -> CmdResult {
|
|||||||
/// 修改profiles的配置
|
/// 修改profiles的配置
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||||
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 profiles_config = Config::profiles();
|
||||||
|
let profiles_data = profiles_config.latest();
|
||||||
|
let config_file_result = match profiles_data.get_item(new_profile) {
|
||||||
|
Ok(item) => {
|
||||||
|
if let Some(file) = &item.file {
|
||||||
|
let path = dirs::app_profiles_dir().map(|dir| dir.join(file));
|
||||||
|
path.ok()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
logging!(error, Type::Cmd, true, "获取目标配置信息失败: {}", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 如果获取到文件路径,检查YAML语法
|
||||||
|
if let Some(file_path) = config_file_result {
|
||||||
|
if !file_path.exists() {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Cmd,
|
||||||
|
true,
|
||||||
|
"目标配置文件不存在: {}",
|
||||||
|
file_path.display()
|
||||||
|
);
|
||||||
|
handle::Handle::notice_message(
|
||||||
|
"config_validate::file_not_found",
|
||||||
|
format!("{}", file_path.display()),
|
||||||
|
);
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
match std::fs::read_to_string(&file_path) {
|
||||||
|
Ok(content) => match serde_yaml::from_str::<serde_yaml::Value>(&content) {
|
||||||
|
Ok(_) => {
|
||||||
|
logging!(info, Type::Cmd, true, "目标配置文件语法正确");
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
let error_msg = format!(" {}", err);
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Cmd,
|
||||||
|
true,
|
||||||
|
"目标配置文件存在YAML语法错误:{}",
|
||||||
|
error_msg
|
||||||
|
);
|
||||||
|
handle::Handle::notice_message(
|
||||||
|
"config_validate::yaml_syntax_error",
|
||||||
|
&error_msg,
|
||||||
|
);
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
let error_msg = format!("无法读取目标配置文件: {}", err);
|
||||||
|
logging!(error, Type::Cmd, true, "{}", error_msg);
|
||||||
|
handle::Handle::notice_message(
|
||||||
|
"config_validate::file_read_error",
|
||||||
|
&error_msg,
|
||||||
|
);
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 更新profiles配置
|
// 更新profiles配置
|
||||||
println!("[cmd配置patch] 正在更新配置草稿");
|
logging!(info, Type::Cmd, true, "正在更新配置草稿");
|
||||||
wrap_err!({ Config::profiles().draft().patch_config(profiles) })?;
|
let _ = Config::profiles().draft().patch_config(profiles);
|
||||||
|
|
||||||
// 更新配置并进行验证
|
// 更新配置并进行验证
|
||||||
match CoreManager::global().update_config().await {
|
match CoreManager::global().update_config().await {
|
||||||
Ok((true, _)) => {
|
Ok((true, _)) => {
|
||||||
println!("[cmd配置patch] 配置更新成功");
|
logging!(info, Type::Cmd, true, "配置更新成功");
|
||||||
handle::Handle::refresh_clash();
|
handle::Handle::refresh_clash();
|
||||||
let _ = tray::Tray::global().update_tooltip();
|
let _ = Tray::global().update_tooltip();
|
||||||
Config::profiles().apply();
|
Config::profiles().apply();
|
||||||
wrap_err!(Config::profiles().data().save_file())?;
|
wrap_err!(Config::profiles().data().save_file())?;
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
Ok((false, error_msg)) => {
|
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,
|
||||||
@ -112,7 +177,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
|||||||
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())?;
|
wrap_err!(Config::profiles().data().save_file())?;
|
||||||
println!("[cmd配置patch] 成功恢复到之前的配置");
|
logging!(info, Type::Cmd, true, "成功恢复到之前的配置");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送验证错误通知
|
// 发送验证错误通知
|
||||||
@ -120,7 +185,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
|||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
Err(e) => {
|
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)
|
Ok(false)
|
||||||
@ -134,6 +199,8 @@ 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> {
|
||||||
|
logging!(info, Type::Cmd, true, "切换配置到: {}", profile_index);
|
||||||
|
|
||||||
let profiles = IProfiles {
|
let profiles = IProfiles {
|
||||||
current: Some(profile_index),
|
current: Some(profile_index),
|
||||||
items: None,
|
items: None,
|
||||||
|
@ -1,26 +1,24 @@
|
|||||||
use super::CmdResult;
|
use super::CmdResult;
|
||||||
use crate::{core::CoreManager, module::mihomo::MihomoManager};
|
use crate::module::mihomo::MihomoManager;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_proxies() -> CmdResult<serde_json::Value> {
|
pub async fn get_proxies() -> CmdResult<serde_json::Value> {
|
||||||
CoreManager::global().ensure_running_core().await;
|
|
||||||
let mannager = MihomoManager::global();
|
let mannager = MihomoManager::global();
|
||||||
let proxies = mannager
|
|
||||||
|
mannager
|
||||||
.refresh_proxies()
|
.refresh_proxies()
|
||||||
.await
|
.await
|
||||||
.map(|_| mannager.get_proxies())
|
.map(|_| mannager.get_proxies())
|
||||||
.or_else(|_| Ok(mannager.get_proxies()));
|
.or_else(|_| Ok(mannager.get_proxies()))
|
||||||
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> {
|
||||||
CoreManager::global().ensure_running_core().await;
|
|
||||||
let mannager = MihomoManager::global();
|
let mannager = MihomoManager::global();
|
||||||
let providers = mannager
|
|
||||||
|
mannager
|
||||||
.refresh_providers_proxies()
|
.refresh_providers_proxies()
|
||||||
.await
|
.await
|
||||||
.map(|_| mannager.get_providers_proxies())
|
.map(|_| mannager.get_providers_proxies())
|
||||||
.or_else(|_| Ok(mannager.get_providers_proxies()));
|
.or_else(|_| Ok(mannager.get_providers_proxies()))
|
||||||
providers
|
|
||||||
}
|
}
|
||||||
|
40
src-tauri/src/cmd/service.rs
Normal file
40
src-tauri/src/cmd/service.rs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
use super::CmdResult;
|
||||||
|
use crate::{
|
||||||
|
core::{service, CoreManager},
|
||||||
|
utils::i18n::t,
|
||||||
|
};
|
||||||
|
|
||||||
|
async fn execute_service_operation(
|
||||||
|
service_op: impl std::future::Future<Output = Result<(), impl ToString + std::fmt::Debug>>,
|
||||||
|
op_type: &str,
|
||||||
|
) -> CmdResult {
|
||||||
|
if service_op.await.is_err() {
|
||||||
|
let emsg = format!("{} {} failed", op_type, "Service");
|
||||||
|
return Err(t(emsg.as_str()));
|
||||||
|
}
|
||||||
|
if CoreManager::global().restart_core().await.is_err() {
|
||||||
|
let emsg = format!("{} {} failed", "Restart", "Core");
|
||||||
|
return Err(t(emsg.as_str()));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn install_service() -> CmdResult {
|
||||||
|
execute_service_operation(service::install_service(), "Install").await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn uninstall_service() -> CmdResult {
|
||||||
|
execute_service_operation(service::uninstall_service(), "Uninstall").await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn reinstall_service() -> CmdResult {
|
||||||
|
execute_service_operation(service::reinstall_service(), "Reinstall").await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn repair_service() -> CmdResult {
|
||||||
|
execute_service_operation(service::force_reinstall_service(), "Repair").await
|
||||||
|
}
|
@ -1,8 +1,7 @@
|
|||||||
use super::CmdResult;
|
use super::CmdResult;
|
||||||
use crate::{
|
use crate::{
|
||||||
core::{self, handle, service, CoreManager},
|
core::{handle, CoreManager},
|
||||||
module::sysinfo::PlatformSpecification,
|
module::sysinfo::PlatformSpecification,
|
||||||
wrap_err,
|
|
||||||
};
|
};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use std::{
|
use std::{
|
||||||
@ -24,7 +23,7 @@ static APP_START_TIME: Lazy<AtomicI64> = Lazy::new(|| {
|
|||||||
|
|
||||||
#[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();
|
||||||
@ -37,7 +36,7 @@ pub async fn export_diagnostic_info() -> CmdResult<()> {
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_system_info() -> CmdResult<String> {
|
pub async fn get_system_info() -> CmdResult<String> {
|
||||||
let sysinfo = PlatformSpecification::new();
|
let sysinfo = PlatformSpecification::new_async().await;
|
||||||
let info = format!("{:?}", sysinfo);
|
let info = format!("{:?}", sysinfo);
|
||||||
Ok(info)
|
Ok(info)
|
||||||
}
|
}
|
||||||
@ -45,17 +44,7 @@ pub async fn get_system_info() -> CmdResult<String> {
|
|||||||
/// 获取当前内核运行模式
|
/// 获取当前内核运行模式
|
||||||
#[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]
|
|
||||||
pub async fn install_service() -> CmdResult {
|
|
||||||
wrap_err!(service::reinstall_service().await)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取应用的运行时间(毫秒)
|
/// 获取应用的运行时间(毫秒)
|
||||||
@ -69,3 +58,37 @@ pub fn get_app_uptime() -> CmdResult<i64> {
|
|||||||
|
|
||||||
Ok(now - start_time)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -2,8 +2,8 @@ use super::{Draft, IClashTemp, IProfiles, IRuntime, IVerge};
|
|||||||
use crate::{
|
use crate::{
|
||||||
config::PrfItem,
|
config::PrfItem,
|
||||||
core::{handle, CoreManager},
|
core::{handle, CoreManager},
|
||||||
enhance,
|
enhance, logging,
|
||||||
utils::{dirs, help},
|
utils::{dirs, help, logging::Type},
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
@ -66,21 +66,27 @@ 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 config_result.is_ok() {
|
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!(
|
logging!(
|
||||||
|
warn,
|
||||||
|
Type::Config,
|
||||||
|
true,
|
||||||
"[首次启动] 配置验证失败,使用默认最小配置启动: {}",
|
"[首次启动] 配置验证失败,使用默认最小配置启动: {}",
|
||||||
error_msg
|
error_msg
|
||||||
);
|
);
|
||||||
@ -89,12 +95,12 @@ impl Config {
|
|||||||
.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?;
|
||||||
@ -102,7 +108,7 @@ impl Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
println!("[首次启动] 生成配置文件失败,使用默认配置");
|
logging!(warn, Type::Config, true, "生成配置文件失败,使用默认配置");
|
||||||
CoreManager::global()
|
CoreManager::global()
|
||||||
.use_default_config("config_validate::error", "")
|
.use_default_config("config_validate::error", "")
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -196,7 +196,7 @@ pub struct IVerge {
|
|||||||
|
|
||||||
/// 自动进入轻量模式的延迟(分钟)
|
/// 自动进入轻量模式的延迟(分钟)
|
||||||
pub auto_light_weight_minutes: Option<u64>,
|
pub auto_light_weight_minutes: Option<u64>,
|
||||||
|
|
||||||
/// 服务状态跟踪
|
/// 服务状态跟踪
|
||||||
pub service_state: Option<crate::core::service::ServiceState>,
|
pub service_state: Option<crate::core::service::ServiceState>,
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,17 +1,24 @@
|
|||||||
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::Arc, time::Duration};
|
||||||
use tauri::{AppHandle, Emitter, Manager, WebviewWindow};
|
use tauri::{AppHandle, Emitter, Manager, WebviewWindow};
|
||||||
use tauri_plugin_shell::process::CommandChild;
|
|
||||||
use std::fs::File;
|
use crate::{logging, logging_error, utils::logging::Type};
|
||||||
|
|
||||||
|
/// 存储启动期间的错误消息
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct ErrorMessage {
|
||||||
|
status: String,
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, 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>>>,
|
/// 存储启动过程中产生的错误消息队列
|
||||||
pub core_lock: Arc<RwLock<Option<File>>>,
|
startup_errors: Arc<RwLock<Vec<ErrorMessage>>>,
|
||||||
|
startup_completed: Arc<RwLock<bool>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Handle {
|
impl Handle {
|
||||||
@ -21,8 +28,8 @@ impl Handle {
|
|||||||
HANDLE.get_or_init(|| Handle {
|
HANDLE.get_or_init(|| Handle {
|
||||||
app_handle: Arc::new(RwLock::new(None)),
|
app_handle: Arc::new(RwLock::new(None)),
|
||||||
is_exiting: Arc::new(RwLock::new(false)),
|
is_exiting: Arc::new(RwLock::new(false)),
|
||||||
core_process: Arc::new(RwLock::new(None)),
|
startup_errors: Arc::new(RwLock::new(Vec::new())),
|
||||||
core_lock: Arc::new(RwLock::new(None)),
|
startup_completed: Arc::new(RwLock::new(false)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,26 +53,115 @@ impl Handle {
|
|||||||
|
|
||||||
pub fn refresh_clash() {
|
pub fn refresh_clash() {
|
||||||
if let Some(window) = Self::global().get_window() {
|
if let Some(window) = Self::global().get_window() {
|
||||||
log_err!(window.emit("verge://refresh-clash-config", "yes"));
|
logging_error!(
|
||||||
|
Type::Frontend,
|
||||||
|
true,
|
||||||
|
window.emit("verge://refresh-clash-config", "yes")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn refresh_verge() {
|
pub fn refresh_verge() {
|
||||||
if let Some(window) = Self::global().get_window() {
|
if let Some(window) = Self::global().get_window() {
|
||||||
log_err!(window.emit("verge://refresh-verge-config", "yes"));
|
logging_error!(
|
||||||
|
Type::Frontend,
|
||||||
|
true,
|
||||||
|
window.emit("verge://refresh-verge-config", "yes")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
pub fn refresh_profiles() {
|
pub fn refresh_profiles() {
|
||||||
if let Some(window) = Self::global().get_window() {
|
if let Some(window) = Self::global().get_window() {
|
||||||
log_err!(window.emit("verge://refresh-profiles-config", "yes"));
|
logging_error!(
|
||||||
|
Type::Frontend,
|
||||||
|
true,
|
||||||
|
window.emit("verge://refresh-profiles-config", "yes")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 通知前端显示消息,如果在启动过程中,则将消息存入启动错误队列
|
||||||
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 let Some(window) = handle.get_window() {
|
||||||
|
logging_error!(
|
||||||
|
Type::Frontend,
|
||||||
|
true,
|
||||||
|
window.emit("verge://notice-message", (status_str, 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()
|
||||||
|
);
|
||||||
|
|
||||||
|
// 等待2秒以确保前端已完全加载,延迟发送错误通知
|
||||||
|
if let Some(window) = self.get_window() {
|
||||||
|
let window_clone = window.clone();
|
||||||
|
let errors_clone = errors.clone();
|
||||||
|
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||||
|
|
||||||
|
for error in errors_clone {
|
||||||
|
let _ =
|
||||||
|
window_clone.emit("verge://notice-message", (error.status, error.message));
|
||||||
|
// 每条消息之间间隔500ms,避免消息堆积
|
||||||
|
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,39 +170,7 @@ impl Handle {
|
|||||||
*is_exiting = true;
|
*is_exiting = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_core_process(&self, process: CommandChild) {
|
|
||||||
let mut core_process = self.core_process.write();
|
|
||||||
*core_process = Some(process);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
||||||
*self.is_exiting.read()
|
*self.is_exiting.read()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 设置核心文件锁
|
|
||||||
pub fn set_core_lock(&self, file: File) {
|
|
||||||
let mut core_lock = self.core_lock.write();
|
|
||||||
*core_lock = Some(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 释放核心文件锁
|
|
||||||
pub fn release_core_lock(&self) -> Option<File> {
|
|
||||||
let mut core_lock = self.core_lock.write();
|
|
||||||
core_lock.take()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 检查是否持有核心文件锁
|
|
||||||
pub fn has_core_lock(&self) -> bool {
|
|
||||||
self.core_lock.read().is_some()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
config::Config, core::handle, feat, log_err, module::lightweight::entry_lightweight_mode,
|
config::Config,
|
||||||
utils::resolve,
|
core::handle,
|
||||||
|
feat, logging, logging_error,
|
||||||
|
module::lightweight::entry_lightweight_mode,
|
||||||
|
utils::{logging::Type, resolve},
|
||||||
};
|
};
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
@ -26,22 +29,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!(
|
logging!(
|
||||||
"Initializing hotkeys, global hotkey enabled: {}",
|
debug,
|
||||||
|
Type::Hotkey,
|
||||||
|
true,
|
||||||
|
"Initializing hotkeys with enable: {}",
|
||||||
enable_global_hotkey
|
enable_global_hotkey
|
||||||
);
|
);
|
||||||
log::info!(target: "app", "Initializing hotkeys, global hotkey enabled: {}", 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(',');
|
||||||
@ -50,28 +58,52 @@ 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,
|
||||||
|
true,
|
||||||
|
"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, true, "No hotkeys configured");
|
||||||
log::info!(target: "app", "No hotkeys configured");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -88,45 +120,60 @@ 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!(
|
logging!(
|
||||||
|
debug,
|
||||||
|
Type::Hotkey,
|
||||||
|
true,
|
||||||
"Attempting to register hotkey: {} for function: {}",
|
"Attempting to register hotkey: {} for function: {}",
|
||||||
hotkey, func
|
hotkey,
|
||||||
|
func
|
||||||
);
|
);
|
||||||
log::info!(target: "app", "Attempting to register hotkey: {} for function: {}", hotkey, func);
|
|
||||||
|
|
||||||
if manager.is_registered(hotkey) {
|
if manager.is_registered(hotkey) {
|
||||||
println!(
|
logging!(
|
||||||
|
debug,
|
||||||
|
Type::Hotkey,
|
||||||
|
true,
|
||||||
"Hotkey {} was already registered, unregistering first",
|
"Hotkey {} was already registered, unregistering first",
|
||||||
hotkey
|
hotkey
|
||||||
);
|
);
|
||||||
log::info!(target: "app", "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,
|
||||||
|
true,
|
||||||
|
"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,
|
||||||
|
true,
|
||||||
|
"=== Hotkey Dashboard Window Operation Start ==="
|
||||||
|
);
|
||||||
|
|
||||||
// 使用 spawn_blocking 来确保在正确的线程上执行
|
// 使用 spawn_blocking 来确保在正确的线程上执行
|
||||||
async_runtime::spawn_blocking(|| {
|
async_runtime::spawn_blocking(|| {
|
||||||
println!("Toggle dashboard window visibility");
|
logging!(
|
||||||
log::info!(target: "app", "Toggle dashboard window visibility");
|
debug,
|
||||||
|
Type::Hotkey,
|
||||||
|
true,
|
||||||
|
"Toggle dashboard window visibility"
|
||||||
|
);
|
||||||
|
|
||||||
// 检查窗口是否存在
|
// 检查窗口是否存在
|
||||||
if let Some(window) = handle::Handle::global().get_window() {
|
if let Some(window) = handle::Handle::global().get_window() {
|
||||||
// 如果窗口可见,则隐藏它
|
// 如果窗口可见,则隐藏它
|
||||||
if window.is_visible().unwrap_or(false) {
|
if window.is_visible().unwrap_or(false) {
|
||||||
println!("Window is visible, hiding it");
|
logging!(info, Type::Window, true, "Window is visible, hiding it");
|
||||||
log::info!(target: "app", "Window is visible, hiding it");
|
|
||||||
let _ = window.hide();
|
let _ = window.hide();
|
||||||
} else {
|
} else {
|
||||||
// 如果窗口不可见,则显示它
|
// 如果窗口不可见,则显示它
|
||||||
println!("Window is hidden, showing it");
|
logging!(info, Type::Window, true, "Window is hidden, showing it");
|
||||||
log::info!(target: "app", "Window is hidden, showing it");
|
|
||||||
if window.is_minimized().unwrap_or(false) {
|
if window.is_minimized().unwrap_or(false) {
|
||||||
let _ = window.unminimize();
|
let _ = window.unminimize();
|
||||||
}
|
}
|
||||||
@ -135,14 +182,22 @@ impl Hotkey {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 如果窗口不存在,创建一个新窗口
|
// 如果窗口不存在,创建一个新窗口
|
||||||
println!("Window does not exist, creating a new one");
|
logging!(
|
||||||
log::info!(target: "app", "Window does not exist, creating a new one");
|
info,
|
||||||
resolve::create_window();
|
Type::Window,
|
||||||
|
true,
|
||||||
|
"Window does not exist, creating a new one"
|
||||||
|
);
|
||||||
|
resolve::create_window(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
println!("=== Hotkey Dashboard Window Operation End ===");
|
logging!(
|
||||||
log::info!(target: "app", "=== Hotkey Dashboard Window Operation End ===");
|
debug,
|
||||||
|
Type::Hotkey,
|
||||||
|
true,
|
||||||
|
"=== Hotkey Dashboard Window Operation End ==="
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"clash_mode_rule" => || feat::change_clash_mode("rule".into()),
|
"clash_mode_rule" => || feat::change_clash_mode("rule".into()),
|
||||||
@ -156,8 +211,7 @@ impl Hotkey {
|
|||||||
"hide" => || feat::hide(),
|
"hide" => || feat::hide(),
|
||||||
|
|
||||||
_ => {
|
_ => {
|
||||||
println!("Invalid function: {}", func);
|
logging!(error, Type::Hotkey, true, "Invalid function: {}", func);
|
||||||
log::error!(target: "app", "Invalid function: {}", func);
|
|
||||||
bail!("invalid function \"{func}\"");
|
bail!("invalid function \"{func}\"");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -166,21 +220,18 @@ 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, true, "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, true, "Executing quit function");
|
||||||
log::info!(target: "app", "Executing quit function");
|
|
||||||
f();
|
f();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 直接执行函数,不做任何状态检查
|
// 直接执行函数,不做任何状态检查
|
||||||
println!("Executing function directly");
|
logging!(debug, Type::Hotkey, true, "Executing function directly");
|
||||||
log::info!(target: "app", "Executing function directly");
|
|
||||||
|
|
||||||
// 获取全局热键状态
|
// 获取全局热键状态
|
||||||
let is_enable_global_hotkey = Config::verge()
|
let is_enable_global_hotkey = Config::verge()
|
||||||
@ -203,8 +254,14 @@ impl Hotkey {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
println!("Successfully registered hotkey {} for {}", hotkey, func);
|
logging!(
|
||||||
log::info!(target: "app", "Successfully registered hotkey {} for {}", hotkey, func);
|
debug,
|
||||||
|
Type::Hotkey,
|
||||||
|
true,
|
||||||
|
"Successfully registered hotkey {} for {}",
|
||||||
|
hotkey,
|
||||||
|
func
|
||||||
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -212,7 +269,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, true, "Unregister hotkey {}", hotkey);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -228,7 +285,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, true, self.register(key, func));
|
||||||
});
|
});
|
||||||
|
|
||||||
*current = new_hotkeys;
|
*current = new_hotkeys;
|
||||||
@ -285,7 +342,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
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,17 @@
|
|||||||
use crate::{config::Config, utils::dirs};
|
use crate::{
|
||||||
|
config::Config,
|
||||||
|
logging,
|
||||||
|
utils::{dirs, logging::Type},
|
||||||
|
};
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{collections::HashMap, env::current_exe, path::PathBuf, process::Command as StdCommand, time::{SystemTime, UNIX_EPOCH}};
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
env::current_exe,
|
||||||
|
path::PathBuf,
|
||||||
|
process::Command as StdCommand,
|
||||||
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
|
};
|
||||||
use tokio::time::Duration;
|
use tokio::time::Duration;
|
||||||
|
|
||||||
// Windows only
|
// Windows only
|
||||||
@ -11,15 +21,16 @@ const REQUIRED_SERVICE_VERSION: &str = "1.0.5"; // 定义所需的服务版本
|
|||||||
|
|
||||||
// 限制重装时间和次数的常量
|
// 限制重装时间和次数的常量
|
||||||
const REINSTALL_COOLDOWN_SECS: u64 = 300; // 5分钟冷却期
|
const REINSTALL_COOLDOWN_SECS: u64 = 300; // 5分钟冷却期
|
||||||
const MAX_REINSTALLS_PER_DAY: u32 = 3; // 每24小时最多重装3次
|
const MAX_REINSTALLS_PER_DAY: u32 = 3; // 每24小时最多重装3次
|
||||||
const ONE_DAY_SECS: u64 = 86400; // 24小时的秒数
|
const ONE_DAY_SECS: u64 = 86400; // 24小时的秒数
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
|
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
|
||||||
pub struct ServiceState {
|
pub struct ServiceState {
|
||||||
pub last_install_time: u64, // 上次安装时间戳 (Unix 时间戳,秒)
|
pub last_install_time: u64, // 上次安装时间戳 (Unix 时间戳,秒)
|
||||||
pub install_count: u32, // 24小时内安装次数
|
pub install_count: u32, // 24小时内安装次数
|
||||||
pub last_check_time: u64, // 上次检查时间
|
pub last_check_time: u64, // 上次检查时间
|
||||||
pub last_error: Option<String>, // 上次错误信息
|
pub last_error: Option<String>, // 上次错误信息
|
||||||
|
pub prefer_sidecar: bool, // 用户是否偏好sidecar模式,如拒绝安装服务或安装失败
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServiceState {
|
impl ServiceState {
|
||||||
@ -47,12 +58,12 @@ impl ServiceState {
|
|||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.as_secs();
|
.as_secs();
|
||||||
|
|
||||||
// 检查是否需要重置计数器(24小时已过)
|
// 检查是否需要重置计数器(24小时已过)
|
||||||
if now - self.last_install_time > ONE_DAY_SECS {
|
if now - self.last_install_time > ONE_DAY_SECS {
|
||||||
self.install_count = 0;
|
self.install_count = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.last_install_time = now;
|
self.last_install_time = now;
|
||||||
self.install_count += 1;
|
self.install_count += 1;
|
||||||
}
|
}
|
||||||
@ -63,17 +74,19 @@ impl ServiceState {
|
|||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.as_secs();
|
.as_secs();
|
||||||
|
|
||||||
// 如果在冷却期内,不允许重装
|
// 如果在冷却期内,不允许重装
|
||||||
if now - self.last_install_time < REINSTALL_COOLDOWN_SECS {
|
if now - self.last_install_time < REINSTALL_COOLDOWN_SECS {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果24小时内安装次数过多,也不允许
|
// 如果24小时内安装次数过多,也不允许
|
||||||
if now - self.last_install_time < ONE_DAY_SECS && self.install_count >= MAX_REINSTALLS_PER_DAY {
|
if now - self.last_install_time < ONE_DAY_SECS
|
||||||
|
&& self.install_count >= MAX_REINSTALLS_PER_DAY
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -107,43 +120,56 @@ pub struct VersionJsonResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
pub async fn reinstall_service() -> Result<()> {
|
pub async fn uninstall_service() -> Result<()> {
|
||||||
log::info!(target:"app", "reinstall service");
|
logging!(info, Type::Service, true, "uninstall service");
|
||||||
|
|
||||||
// 获取当前服务状态
|
|
||||||
let mut service_state = ServiceState::get();
|
|
||||||
|
|
||||||
// 检查是否允许重装
|
|
||||||
if !service_state.can_reinstall() {
|
|
||||||
log::warn!(target:"app", "service reinstall rejected: cooldown period or max attempts reached");
|
|
||||||
bail!("Service reinstallation is rate limited. Please try again later.");
|
|
||||||
}
|
|
||||||
|
|
||||||
use deelevate::{PrivilegeLevel, Token};
|
use deelevate::{PrivilegeLevel, Token};
|
||||||
use runas::Command as RunasCommand;
|
use runas::Command as RunasCommand;
|
||||||
use std::os::windows::process::CommandExt;
|
use std::os::windows::process::CommandExt;
|
||||||
|
|
||||||
let binary_path = dirs::service_path()?;
|
let binary_path = dirs::service_path()?;
|
||||||
let install_path = binary_path.with_file_name("install-service.exe");
|
|
||||||
let uninstall_path = binary_path.with_file_name("uninstall-service.exe");
|
let uninstall_path = binary_path.with_file_name("uninstall-service.exe");
|
||||||
|
|
||||||
if !install_path.exists() {
|
|
||||||
bail!(format!("installer not found: {install_path:?}"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if !uninstall_path.exists() {
|
if !uninstall_path.exists() {
|
||||||
bail!(format!("uninstaller not found: {uninstall_path:?}"));
|
bail!(format!("uninstaller not found: {uninstall_path:?}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let token = Token::with_current_process()?;
|
let token = Token::with_current_process()?;
|
||||||
let level = token.privilege_level()?;
|
let level = token.privilege_level()?;
|
||||||
let _ = match level {
|
let status = match level {
|
||||||
PrivilegeLevel::NotPrivileged => RunasCommand::new(uninstall_path).show(false).status()?,
|
PrivilegeLevel::NotPrivileged => RunasCommand::new(uninstall_path).show(false).status()?,
|
||||||
_ => StdCommand::new(uninstall_path)
|
_ => StdCommand::new(uninstall_path)
|
||||||
.creation_flags(0x08000000)
|
.creation_flags(0x08000000)
|
||||||
.status()?,
|
.status()?,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if !status.success() {
|
||||||
|
bail!(
|
||||||
|
"failed to uninstall service with status {}",
|
||||||
|
status.code().unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub async fn install_service() -> Result<()> {
|
||||||
|
logging!(info, Type::Service, true, "install service");
|
||||||
|
|
||||||
|
use deelevate::{PrivilegeLevel, Token};
|
||||||
|
use runas::Command as RunasCommand;
|
||||||
|
use std::os::windows::process::CommandExt;
|
||||||
|
|
||||||
|
let binary_path = dirs::service_path()?;
|
||||||
|
let install_path = binary_path.with_file_name("install-service.exe");
|
||||||
|
|
||||||
|
if !install_path.exists() {
|
||||||
|
bail!(format!("installer not found: {install_path:?}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let token = Token::with_current_process()?;
|
||||||
|
let level = token.privilege_level()?;
|
||||||
let status = match level {
|
let status = match level {
|
||||||
PrivilegeLevel::NotPrivileged => RunasCommand::new(install_path).show(false).status()?,
|
PrivilegeLevel::NotPrivileged => RunasCommand::new(install_path).show(false).status()?,
|
||||||
_ => StdCommand::new(install_path)
|
_ => StdCommand::new(install_path)
|
||||||
@ -152,41 +178,73 @@ pub async fn reinstall_service() -> Result<()> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if !status.success() {
|
if !status.success() {
|
||||||
let error = format!(
|
bail!(
|
||||||
"failed to install service with status {}",
|
"failed to install service with status {}",
|
||||||
status.code().unwrap()
|
status.code().unwrap()
|
||||||
);
|
);
|
||||||
service_state.last_error = Some(error.clone());
|
|
||||||
service_state.save()?;
|
|
||||||
bail!(error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 记录安装信息并保存
|
|
||||||
service_state.record_install();
|
|
||||||
service_state.last_error = None;
|
|
||||||
service_state.save()?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "windows")]
|
||||||
pub async fn reinstall_service() -> Result<()> {
|
pub async fn reinstall_service() -> Result<()> {
|
||||||
log::info!(target:"app", "reinstall service");
|
logging!(info, Type::Service, true, "reinstall service");
|
||||||
|
|
||||||
|
// 获取当前服务状态
|
||||||
|
let mut service_state = ServiceState::get();
|
||||||
|
|
||||||
|
// 检查是否允许重装
|
||||||
|
if !service_state.can_reinstall() {
|
||||||
|
logging!(
|
||||||
|
warn,
|
||||||
|
Type::Service,
|
||||||
|
true,
|
||||||
|
"service reinstall rejected: cooldown period or max attempts reached"
|
||||||
|
);
|
||||||
|
bail!("Service reinstallation is rate limited. Please try again later.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先卸载服务
|
||||||
|
if let Err(err) = uninstall_service().await {
|
||||||
|
logging!(
|
||||||
|
warn,
|
||||||
|
Type::Service,
|
||||||
|
true,
|
||||||
|
"failed to uninstall service: {}",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 再安装服务
|
||||||
|
match install_service().await {
|
||||||
|
Ok(_) => {
|
||||||
|
// 记录安装信息并保存
|
||||||
|
service_state.record_install();
|
||||||
|
service_state.last_error = None;
|
||||||
|
service_state.save()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
let error = format!("failed to install service: {}", err);
|
||||||
|
service_state.last_error = Some(error.clone());
|
||||||
|
service_state.save()?;
|
||||||
|
bail!(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub async fn uninstall_service() -> Result<()> {
|
||||||
|
logging!(info, Type::Service, true, "uninstall service");
|
||||||
use users::get_effective_uid;
|
use users::get_effective_uid;
|
||||||
|
|
||||||
let install_path = tauri::utils::platform::current_exe()?.with_file_name("install-service");
|
|
||||||
|
|
||||||
let uninstall_path = tauri::utils::platform::current_exe()?.with_file_name("uninstall-service");
|
let uninstall_path = tauri::utils::platform::current_exe()?.with_file_name("uninstall-service");
|
||||||
|
|
||||||
if !install_path.exists() {
|
|
||||||
bail!(format!("installer not found: {install_path:?}"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if !uninstall_path.exists() {
|
if !uninstall_path.exists() {
|
||||||
bail!(format!("uninstaller not found: {uninstall_path:?}"));
|
bail!(format!("uninstaller not found: {uninstall_path:?}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let install_shell: String = install_path.to_string_lossy().replace(" ", "\\ ");
|
|
||||||
let uninstall_shell: String = uninstall_path.to_string_lossy().replace(" ", "\\ ");
|
let uninstall_shell: String = uninstall_path.to_string_lossy().replace(" ", "\\ ");
|
||||||
|
|
||||||
let elevator = crate::utils::help::linux_elevator();
|
let elevator = crate::utils::help::linux_elevator();
|
||||||
@ -198,8 +256,38 @@ pub async fn reinstall_service() -> Result<()> {
|
|||||||
.arg(uninstall_shell)
|
.arg(uninstall_shell)
|
||||||
.status()?,
|
.status()?,
|
||||||
};
|
};
|
||||||
log::info!(target:"app", "status code:{}", status.code().unwrap());
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Service,
|
||||||
|
true,
|
||||||
|
"uninstall status code:{}",
|
||||||
|
status.code().unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
if !status.success() {
|
||||||
|
bail!(
|
||||||
|
"failed to uninstall service with status {}",
|
||||||
|
status.code().unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub async fn install_service() -> Result<()> {
|
||||||
|
logging!(info, Type::Service, true, "install service");
|
||||||
|
use users::get_effective_uid;
|
||||||
|
|
||||||
|
let install_path = tauri::utils::platform::current_exe()?.with_file_name("install-service");
|
||||||
|
|
||||||
|
if !install_path.exists() {
|
||||||
|
bail!(format!("installer not found: {install_path:?}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let install_shell: String = install_path.to_string_lossy().replace(" ", "\\ ");
|
||||||
|
|
||||||
|
let elevator = crate::utils::help::linux_elevator();
|
||||||
let status = match get_effective_uid() {
|
let status = match get_effective_uid() {
|
||||||
0 => StdCommand::new(install_shell).status()?,
|
0 => StdCommand::new(install_shell).status()?,
|
||||||
_ => StdCommand::new(elevator.clone())
|
_ => StdCommand::new(elevator.clone())
|
||||||
@ -208,6 +296,132 @@ pub async fn reinstall_service() -> Result<()> {
|
|||||||
.arg(install_shell)
|
.arg(install_shell)
|
||||||
.status()?,
|
.status()?,
|
||||||
};
|
};
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Service,
|
||||||
|
true,
|
||||||
|
"install status code:{}",
|
||||||
|
status.code().unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
if !status.success() {
|
||||||
|
bail!(
|
||||||
|
"failed to install service with status {}",
|
||||||
|
status.code().unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub async fn reinstall_service() -> Result<()> {
|
||||||
|
logging!(info, Type::Service, true, "reinstall service");
|
||||||
|
|
||||||
|
// 获取当前服务状态
|
||||||
|
let mut service_state = ServiceState::get();
|
||||||
|
|
||||||
|
// 检查是否允许重装
|
||||||
|
if !service_state.can_reinstall() {
|
||||||
|
logging!(
|
||||||
|
warn,
|
||||||
|
Type::Service,
|
||||||
|
true,
|
||||||
|
"service reinstall rejected: cooldown period or max attempts reached"
|
||||||
|
);
|
||||||
|
bail!("Service reinstallation is rate limited. Please try again later.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先卸载服务
|
||||||
|
if let Err(err) = uninstall_service().await {
|
||||||
|
logging!(
|
||||||
|
warn,
|
||||||
|
Type::Service,
|
||||||
|
true,
|
||||||
|
"failed to uninstall service: {}",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 再安装服务
|
||||||
|
match install_service().await {
|
||||||
|
Ok(_) => {
|
||||||
|
// 记录安装信息并保存
|
||||||
|
service_state.record_install();
|
||||||
|
service_state.last_error = None;
|
||||||
|
service_state.save()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
let error = format!("failed to install service: {}", err);
|
||||||
|
service_state.last_error = Some(error.clone());
|
||||||
|
service_state.save()?;
|
||||||
|
bail!(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
pub async fn uninstall_service() -> Result<()> {
|
||||||
|
use crate::utils::i18n::t;
|
||||||
|
|
||||||
|
logging!(info, Type::Service, true, "uninstall service");
|
||||||
|
|
||||||
|
let binary_path = dirs::service_path()?;
|
||||||
|
let uninstall_path = binary_path.with_file_name("uninstall-service");
|
||||||
|
|
||||||
|
if !uninstall_path.exists() {
|
||||||
|
bail!(format!("uninstaller not found: {uninstall_path:?}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let uninstall_shell: String = uninstall_path.to_string_lossy().into_owned();
|
||||||
|
|
||||||
|
let prompt = t("Service Administrator Prompt");
|
||||||
|
let command = format!(
|
||||||
|
r#"do shell script "sudo '{uninstall_shell}'" with administrator privileges with prompt "{prompt}""#
|
||||||
|
);
|
||||||
|
|
||||||
|
logging!(debug, Type::Service, true, "uninstall command: {}", command);
|
||||||
|
|
||||||
|
let status = StdCommand::new("osascript")
|
||||||
|
.args(vec!["-e", &command])
|
||||||
|
.status()?;
|
||||||
|
|
||||||
|
if !status.success() {
|
||||||
|
bail!(
|
||||||
|
"failed to uninstall service with status {}",
|
||||||
|
status.code().unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
pub async fn install_service() -> Result<()> {
|
||||||
|
use crate::utils::i18n::t;
|
||||||
|
|
||||||
|
logging!(info, Type::Service, true, "install service");
|
||||||
|
|
||||||
|
let binary_path = dirs::service_path()?;
|
||||||
|
let install_path = binary_path.with_file_name("install-service");
|
||||||
|
|
||||||
|
if !install_path.exists() {
|
||||||
|
bail!(format!("installer not found: {install_path:?}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let install_shell: String = install_path.to_string_lossy().into_owned();
|
||||||
|
|
||||||
|
let prompt = t("Service Administrator Prompt");
|
||||||
|
let command = format!(
|
||||||
|
r#"do shell script "sudo '{install_shell}'" with administrator privileges with prompt "{prompt}""#
|
||||||
|
);
|
||||||
|
|
||||||
|
logging!(debug, Type::Service, true, "install command: {}", command);
|
||||||
|
|
||||||
|
let status = StdCommand::new("osascript")
|
||||||
|
.args(vec!["-e", &command])
|
||||||
|
.status()?;
|
||||||
|
|
||||||
if !status.success() {
|
if !status.success() {
|
||||||
bail!(
|
bail!(
|
||||||
@ -221,54 +435,49 @@ pub async fn reinstall_service() -> Result<()> {
|
|||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
pub async fn reinstall_service() -> Result<()> {
|
pub async fn reinstall_service() -> Result<()> {
|
||||||
log::info!(target:"app", "reinstall service");
|
logging!(info, Type::Service, true, "reinstall service");
|
||||||
|
|
||||||
let binary_path = dirs::service_path()?;
|
// 获取当前服务状态
|
||||||
let install_path = binary_path.with_file_name("install-service");
|
let mut service_state = ServiceState::get();
|
||||||
let uninstall_path = binary_path.with_file_name("uninstall-service");
|
|
||||||
|
|
||||||
if !install_path.exists() {
|
// 检查是否允许重装
|
||||||
bail!(format!("installer not found: {install_path:?}"));
|
if !service_state.can_reinstall() {
|
||||||
|
logging!(
|
||||||
|
warn,
|
||||||
|
Type::Service,
|
||||||
|
true,
|
||||||
|
"service reinstall rejected: cooldown period or max attempts reached"
|
||||||
|
);
|
||||||
|
bail!("Service reinstallation is rate limited. Please try again later.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if !uninstall_path.exists() {
|
// 先卸载服务
|
||||||
bail!(format!("uninstaller not found: {uninstall_path:?}"));
|
if let Err(err) = uninstall_service().await {
|
||||||
}
|
logging!(
|
||||||
|
warn,
|
||||||
let install_shell: String = install_path.to_string_lossy().into_owned();
|
Type::Service,
|
||||||
let uninstall_shell: String = uninstall_path.to_string_lossy().into_owned();
|
true,
|
||||||
|
"failed to uninstall service: {}",
|
||||||
// 获取提示文本,如果 i18n 失败则使用硬编码默认值
|
err
|
||||||
let prompt = crate::utils::i18n::t("Service Administrator Prompt");
|
|
||||||
let prompt = if prompt == "Service Administrator Prompt" {
|
|
||||||
if Config::verge().latest().language.as_deref() == Some("zh")
|
|
||||||
|| Config::verge().latest().language.is_none()
|
|
||||||
{
|
|
||||||
"Clash Verge 需要使用管理员权限来重新安装系统服务"
|
|
||||||
} else {
|
|
||||||
"Clash Verge needs administrator privileges to reinstall the system service"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
&prompt
|
|
||||||
};
|
|
||||||
|
|
||||||
let command = format!(
|
|
||||||
r#"do shell script "sudo '{uninstall_shell}' && sudo '{install_shell}'" with administrator privileges with prompt "{prompt}""#
|
|
||||||
);
|
|
||||||
|
|
||||||
log::debug!(target: "app", "command: {}", command);
|
|
||||||
|
|
||||||
let status = StdCommand::new("osascript")
|
|
||||||
.args(vec!["-e", &command])
|
|
||||||
.status()?;
|
|
||||||
|
|
||||||
if !status.success() {
|
|
||||||
bail!(
|
|
||||||
"failed to install service with status {}",
|
|
||||||
status.code().unwrap()
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
|
// 再安装服务
|
||||||
|
match install_service().await {
|
||||||
|
Ok(_) => {
|
||||||
|
// 记录安装信息并保存
|
||||||
|
service_state.record_install();
|
||||||
|
service_state.last_error = None;
|
||||||
|
service_state.save()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
let error = format!("failed to install service: {}", err);
|
||||||
|
service_state.last_error = Some(error.clone());
|
||||||
|
service_state.save()?;
|
||||||
|
bail!(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// check the windows service status
|
/// check the windows service status
|
||||||
@ -314,7 +523,7 @@ pub async fn check_service_version() -> Result<String> {
|
|||||||
pub async fn check_service_needs_reinstall() -> bool {
|
pub async fn check_service_needs_reinstall() -> bool {
|
||||||
// 获取当前服务状态
|
// 获取当前服务状态
|
||||||
let service_state = ServiceState::get();
|
let service_state = ServiceState::get();
|
||||||
|
|
||||||
// 首先检查是否在冷却期或超过重装次数限制
|
// 首先检查是否在冷却期或超过重装次数限制
|
||||||
if !service_state.can_reinstall() {
|
if !service_state.can_reinstall() {
|
||||||
log::info!(target: "app", "service reinstall check: in cooldown period or max attempts reached");
|
log::info!(target: "app", "service reinstall check: in cooldown period or max attempts reached");
|
||||||
@ -326,21 +535,21 @@ pub async fn check_service_needs_reinstall() -> bool {
|
|||||||
Ok(version) => {
|
Ok(version) => {
|
||||||
// 打印更详细的日志,方便排查问题
|
// 打印更详细的日志,方便排查问题
|
||||||
log::info!(target: "app", "服务版本检测:当前={}, 要求={}", version, REQUIRED_SERVICE_VERSION);
|
log::info!(target: "app", "服务版本检测:当前={}, 要求={}", version, REQUIRED_SERVICE_VERSION);
|
||||||
|
|
||||||
let needs_reinstall = version != REQUIRED_SERVICE_VERSION;
|
let needs_reinstall = version != REQUIRED_SERVICE_VERSION;
|
||||||
if needs_reinstall {
|
if needs_reinstall {
|
||||||
log::warn!(target: "app", "发现服务版本不匹配,需要重装! 当前={}, 要求={}",
|
log::warn!(target: "app", "发现服务版本不匹配,需要重装! 当前={}, 要求={}",
|
||||||
version, REQUIRED_SERVICE_VERSION);
|
version, REQUIRED_SERVICE_VERSION);
|
||||||
|
|
||||||
// 打印版本字符串的原始字节,确认没有隐藏字符
|
// 打印版本字符串的原始字节,确认没有隐藏字符
|
||||||
log::debug!(target: "app", "当前版本字节: {:?}", version.as_bytes());
|
log::debug!(target: "app", "当前版本字节: {:?}", version.as_bytes());
|
||||||
log::debug!(target: "app", "要求版本字节: {:?}", REQUIRED_SERVICE_VERSION.as_bytes());
|
log::debug!(target: "app", "要求版本字节: {:?}", REQUIRED_SERVICE_VERSION.as_bytes());
|
||||||
} else {
|
} else {
|
||||||
log::info!(target: "app", "服务版本匹配,无需重装");
|
log::info!(target: "app", "服务版本匹配,无需重装");
|
||||||
}
|
}
|
||||||
|
|
||||||
needs_reinstall
|
needs_reinstall
|
||||||
},
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
// 检查服务是否可用,如果可用但版本检查失败,可能只是版本API有问题
|
// 检查服务是否可用,如果可用但版本检查失败,可能只是版本API有问题
|
||||||
match is_service_running().await {
|
match is_service_running().await {
|
||||||
@ -371,8 +580,6 @@ pub(super) async fn start_with_existing_service(config_file: &PathBuf) -> Result
|
|||||||
|
|
||||||
let config_dir = dirs::app_home_dir()?;
|
let config_dir = dirs::app_home_dir()?;
|
||||||
let config_dir = dirs::path_to_str(&config_dir)?;
|
let config_dir = dirs::path_to_str(&config_dir)?;
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
let config_dir = &(config_dir.replace("/verge-mihomo", "") + "/resources");
|
|
||||||
|
|
||||||
let log_path = dirs::service_log_file()?;
|
let log_path = dirs::service_log_file()?;
|
||||||
let log_path = dirs::path_to_str(&log_path)?;
|
let log_path = dirs::path_to_str(&log_path)?;
|
||||||
@ -408,9 +615,9 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
|||||||
// 先检查服务版本,不受冷却期限制
|
// 先检查服务版本,不受冷却期限制
|
||||||
let version_check = match check_service_version().await {
|
let version_check = match check_service_version().await {
|
||||||
Ok(version) => {
|
Ok(version) => {
|
||||||
log::info!(target: "app", "检测到服务版本: {}, 要求版本: {}",
|
log::info!(target: "app", "检测到服务版本: {}, 要求版本: {}",
|
||||||
version, REQUIRED_SERVICE_VERSION);
|
version, REQUIRED_SERVICE_VERSION);
|
||||||
|
|
||||||
// 通过字节比较确保完全匹配
|
// 通过字节比较确保完全匹配
|
||||||
if version.as_bytes() != REQUIRED_SERVICE_VERSION.as_bytes() {
|
if version.as_bytes() != REQUIRED_SERVICE_VERSION.as_bytes() {
|
||||||
log::warn!(target: "app", "服务版本不匹配,需要重装");
|
log::warn!(target: "app", "服务版本不匹配,需要重装");
|
||||||
@ -419,7 +626,7 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
|||||||
log::info!(target: "app", "服务版本匹配");
|
log::info!(target: "app", "服务版本匹配");
|
||||||
true // 版本匹配
|
true // 版本匹配
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!(target: "app", "无法获取服务版本: {}", err);
|
log::warn!(target: "app", "无法获取服务版本: {}", err);
|
||||||
false // 无法获取版本
|
false // 无法获取版本
|
||||||
@ -434,11 +641,11 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
|||||||
return start_with_existing_service(config_file).await;
|
return start_with_existing_service(config_file).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 强制执行版本检查,如果版本不匹配则重装
|
// 强制执行版本检查,如果版本不匹配则重装
|
||||||
if !version_check {
|
if !version_check {
|
||||||
log::info!(target: "app", "服务版本不匹配,尝试重装");
|
log::info!(target: "app", "服务版本不匹配,尝试重装");
|
||||||
|
|
||||||
// 获取服务状态,检查是否可以重装
|
// 获取服务状态,检查是否可以重装
|
||||||
let service_state = ServiceState::get();
|
let service_state = ServiceState::get();
|
||||||
if !service_state.can_reinstall() {
|
if !service_state.can_reinstall() {
|
||||||
@ -451,22 +658,22 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
|||||||
bail!("服务版本不匹配且无法重装,启动失败");
|
bail!("服务版本不匹配且无法重装,启动失败");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 尝试重装
|
// 尝试重装
|
||||||
log::info!(target: "app", "开始重装服务");
|
log::info!(target: "app", "开始重装服务");
|
||||||
if let Err(err) = reinstall_service().await {
|
if let Err(err) = reinstall_service().await {
|
||||||
log::warn!(target: "app", "服务重装失败: {}", err);
|
log::warn!(target: "app", "服务重装失败: {}", err);
|
||||||
|
|
||||||
// 尝试使用现有服务
|
// 尝试使用现有服务
|
||||||
log::info!(target: "app", "尝试使用现有服务");
|
log::info!(target: "app", "尝试使用现有服务");
|
||||||
return start_with_existing_service(config_file).await;
|
return start_with_existing_service(config_file).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重装成功,尝试启动
|
// 重装成功,尝试启动
|
||||||
log::info!(target: "app", "服务重装成功,尝试启动");
|
log::info!(target: "app", "服务重装成功,尝试启动");
|
||||||
return start_with_existing_service(config_file).await;
|
return start_with_existing_service(config_file).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查服务状态
|
// 检查服务状态
|
||||||
match check_service().await {
|
match check_service().await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
@ -475,22 +682,22 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
|||||||
if let Ok(()) = start_with_existing_service(config_file).await {
|
if let Ok(()) = start_with_existing_service(config_file).await {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!(target: "app", "服务检查失败: {}", err);
|
log::warn!(target: "app", "服务检查失败: {}", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 服务不可用或启动失败,检查是否需要重装
|
// 服务不可用或启动失败,检查是否需要重装
|
||||||
if check_service_needs_reinstall().await {
|
if check_service_needs_reinstall().await {
|
||||||
log::info!(target: "app", "服务需要重装");
|
log::info!(target: "app", "服务需要重装");
|
||||||
|
|
||||||
// 尝试重装
|
// 尝试重装
|
||||||
if let Err(err) = reinstall_service().await {
|
if let Err(err) = reinstall_service().await {
|
||||||
log::warn!(target: "app", "服务重装失败: {}", err);
|
log::warn!(target: "app", "服务重装失败: {}", err);
|
||||||
bail!("Failed to reinstall service: {}", err);
|
bail!("Failed to reinstall service: {}", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重装后再次尝试启动
|
// 重装后再次尝试启动
|
||||||
log::info!(target: "app", "服务重装完成,尝试启动核心");
|
log::info!(target: "app", "服务重装完成,尝试启动核心");
|
||||||
start_with_existing_service(config_file).await
|
start_with_existing_service(config_file).await
|
||||||
@ -521,12 +728,22 @@ pub async fn is_service_running() -> Result<bool> {
|
|||||||
|
|
||||||
// 检查服务状态码和消息
|
// 检查服务状态码和消息
|
||||||
if resp.code == 0 && resp.msg == "ok" && resp.data.is_some() {
|
if resp.code == 0 && resp.msg == "ok" && resp.data.is_some() {
|
||||||
|
logging!(debug, Type::Service, "Service is running");
|
||||||
Ok(true)
|
Ok(true)
|
||||||
} else {
|
} else {
|
||||||
|
logging!(debug, Type::Service, "Service is not running");
|
||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn is_service_available() -> Result<()> {
|
||||||
|
let resp = check_service().await?;
|
||||||
|
if resp.code == 0 && resp.msg == "ok" && resp.data.is_some() {
|
||||||
|
logging!(debug, Type::Service, "Service is available");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// 强制重装服务(用于UI中的修复服务按钮)
|
/// 强制重装服务(用于UI中的修复服务按钮)
|
||||||
pub async fn force_reinstall_service() -> Result<()> {
|
pub async fn force_reinstall_service() -> Result<()> {
|
||||||
log::info!(target: "app", "用户请求强制重装服务");
|
log::info!(target: "app", "用户请求强制重装服务");
|
||||||
@ -534,15 +751,15 @@ pub async fn force_reinstall_service() -> Result<()> {
|
|||||||
// 创建默认服务状态(重置所有限制)
|
// 创建默认服务状态(重置所有限制)
|
||||||
let service_state = ServiceState::default();
|
let service_state = ServiceState::default();
|
||||||
service_state.save()?;
|
service_state.save()?;
|
||||||
|
|
||||||
log::info!(target: "app", "已重置服务状态,开始执行重装");
|
log::info!(target: "app", "已重置服务状态,开始执行重装");
|
||||||
|
|
||||||
// 执行重装
|
// 执行重装
|
||||||
match reinstall_service().await {
|
match reinstall_service().await {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
log::info!(target: "app", "服务重装成功");
|
log::info!(target: "app", "服务重装成功");
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::error!(target: "app", "强制重装服务失败: {}", err);
|
log::error!(target: "app", "强制重装服务失败: {}", err);
|
||||||
bail!("强制重装服务失败: {}", err)
|
bail!("强制重装服务失败: {}", err)
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
config::{Config, IVerge},
|
config::{Config, IVerge},
|
||||||
core::handle::Handle,
|
core::handle::Handle,
|
||||||
log_err,
|
logging_error,
|
||||||
|
utils::logging::Type,
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
@ -220,9 +221,9 @@ impl Sysopt {
|
|||||||
let enable = enable.unwrap_or(false);
|
let enable = enable.unwrap_or(false);
|
||||||
let app_handle = Handle::global().app_handle().unwrap();
|
let app_handle = Handle::global().app_handle().unwrap();
|
||||||
let autostart_manager = app_handle.autolaunch();
|
let autostart_manager = app_handle.autolaunch();
|
||||||
|
|
||||||
log::info!(target: "app", "Setting auto launch to: {}", enable);
|
log::info!(target: "app", "Setting auto launch to: {}", enable);
|
||||||
|
|
||||||
match enable {
|
match enable {
|
||||||
true => {
|
true => {
|
||||||
let result = autostart_manager.enable();
|
let result = autostart_manager.enable();
|
||||||
@ -231,8 +232,8 @@ impl Sysopt {
|
|||||||
} else {
|
} else {
|
||||||
log::info!(target: "app", "Auto launch enabled successfully");
|
log::info!(target: "app", "Auto launch enabled successfully");
|
||||||
}
|
}
|
||||||
log_err!(result)
|
logging_error!(Type::System, true, result);
|
||||||
},
|
}
|
||||||
false => {
|
false => {
|
||||||
let result = autostart_manager.disable();
|
let result = autostart_manager.disable();
|
||||||
if let Err(ref e) = result {
|
if let Err(ref e) = result {
|
||||||
@ -240,8 +241,8 @@ impl Sysopt {
|
|||||||
} else {
|
} else {
|
||||||
log::info!(target: "app", "Auto launch disabled successfully");
|
log::info!(target: "app", "Auto launch disabled successfully");
|
||||||
}
|
}
|
||||||
log_err!(result)
|
logging_error!(Type::System, true, result);
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -251,12 +252,12 @@ impl Sysopt {
|
|||||||
pub fn get_launch_status(&self) -> Result<bool> {
|
pub fn get_launch_status(&self) -> Result<bool> {
|
||||||
let app_handle = Handle::global().app_handle().unwrap();
|
let app_handle = Handle::global().app_handle().unwrap();
|
||||||
let autostart_manager = app_handle.autolaunch();
|
let autostart_manager = app_handle.autolaunch();
|
||||||
|
|
||||||
match autostart_manager.is_enabled() {
|
match autostart_manager.is_enabled() {
|
||||||
Ok(status) => {
|
Ok(status) => {
|
||||||
log::info!(target: "app", "Auto launch status: {}", status);
|
log::info!(target: "app", "Auto launch status: {}", status);
|
||||||
Ok(status)
|
Ok(status)
|
||||||
},
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!(target: "app", "Failed to get auto launch status: {}", e);
|
log::error!(target: "app", "Failed to get auto launch status: {}", e);
|
||||||
Err(anyhow::anyhow!("Failed to get auto launch status: {}", e))
|
Err(anyhow::anyhow!("Failed to get auto launch status: {}", e))
|
||||||
@ -323,7 +324,7 @@ impl Sysopt {
|
|||||||
enable: true,
|
enable: true,
|
||||||
url: format!("http://127.0.0.1:{pac_port}/commands/pac"),
|
url: format!("http://127.0.0.1:{pac_port}/commands/pac"),
|
||||||
};
|
};
|
||||||
log_err!(autoproxy.set_auto_proxy());
|
logging_error!(Type::System, true, autoproxy.set_auto_proxy());
|
||||||
} else {
|
} else {
|
||||||
let sysproxy = Sysproxy {
|
let sysproxy = Sysproxy {
|
||||||
enable: true,
|
enable: true,
|
||||||
@ -332,7 +333,7 @@ impl Sysopt {
|
|||||||
bypass: get_bypass(),
|
bypass: get_bypass(),
|
||||||
};
|
};
|
||||||
|
|
||||||
log_err!(sysproxy.set_system_proxy());
|
logging_error!(Type::System, true, sysproxy.set_system_proxy());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
use crate::{config::Config, core::CoreManager, feat};
|
use crate::{
|
||||||
|
config::Config, core::CoreManager, feat, logging, logging_error, utils::logging::Type,
|
||||||
|
};
|
||||||
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;
|
||||||
@ -54,18 +56,18 @@ impl Timer {
|
|||||||
)
|
)
|
||||||
.is_err()
|
.is_err()
|
||||||
{
|
{
|
||||||
log::debug!(target: "app", "Timer already initialized, skipping...");
|
logging!(debug, Type::Timer, "Timer already initialized, skipping...");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
log::info!(target: "app", "Initializing timer...");
|
logging!(info, Type::Timer, true, "Initializing timer...");
|
||||||
|
|
||||||
// Initialize timer tasks
|
// Initialize timer tasks
|
||||||
if let Err(e) = self.refresh() {
|
if let Err(e) = self.refresh() {
|
||||||
// Reset initialization flag on error
|
// Reset initialization flag on error
|
||||||
self.initialized
|
self.initialized
|
||||||
.store(false, std::sync::atomic::Ordering::SeqCst);
|
.store(false, std::sync::atomic::Ordering::SeqCst);
|
||||||
log::error!(target: "app", "Failed to initialize timer: {}", e);
|
logging_error!(Type::Timer, false, "Failed to initialize timer: {}", e);
|
||||||
return Err(e);
|
return Err(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,15 +100,15 @@ impl Timer {
|
|||||||
|
|
||||||
for uid in profiles_to_update {
|
for uid in profiles_to_update {
|
||||||
if let Some(task) = timer_map.get(&uid) {
|
if let Some(task) = timer_map.get(&uid) {
|
||||||
log::info!(target: "app", "Advancing task for uid: {}", uid);
|
logging!(info, Type::Timer, "Advancing task for uid: {}", uid);
|
||||||
if let Err(e) = delay_timer.advance_task(task.task_id) {
|
if let Err(e) = delay_timer.advance_task(task.task_id) {
|
||||||
log::warn!(target: "app", "Failed to advance task {}: {}", uid, e);
|
logging!(warn, Type::Timer, "Failed to advance task {}: {}", uid, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log::info!(target: "app", "Timer initialization completed");
|
logging!(info, Type::Timer, "Timer initialization completed");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,11 +118,16 @@ impl Timer {
|
|||||||
let diff_map = self.gen_diff();
|
let diff_map = self.gen_diff();
|
||||||
|
|
||||||
if diff_map.is_empty() {
|
if diff_map.is_empty() {
|
||||||
log::debug!(target: "app", "No timer changes needed");
|
logging!(debug, Type::Timer, "No timer changes needed");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
log::info!(target: "app", "Refreshing {} timer tasks", diff_map.len());
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Timer,
|
||||||
|
"Refreshing {} timer tasks",
|
||||||
|
diff_map.len()
|
||||||
|
);
|
||||||
|
|
||||||
// Apply changes while holding locks
|
// Apply changes while holding locks
|
||||||
let mut timer_map = self.timer_map.write();
|
let mut timer_map = self.timer_map.write();
|
||||||
@ -131,9 +138,16 @@ impl Timer {
|
|||||||
DiffFlag::Del(tid) => {
|
DiffFlag::Del(tid) => {
|
||||||
timer_map.remove(&uid);
|
timer_map.remove(&uid);
|
||||||
if let Err(e) = delay_timer.remove_task(tid) {
|
if let Err(e) = delay_timer.remove_task(tid) {
|
||||||
log::warn!(target: "app", "Failed to remove task {} for uid {}: {}", tid, uid, e);
|
logging!(
|
||||||
|
warn,
|
||||||
|
Type::Timer,
|
||||||
|
"Failed to remove task {} for uid {}: {}",
|
||||||
|
tid,
|
||||||
|
uid,
|
||||||
|
e
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
log::debug!(target: "app", "Removed task {} for uid {}", tid, uid);
|
logging!(debug, Type::Timer, "Removed task {} for uid {}", tid, uid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DiffFlag::Add(tid, interval) => {
|
DiffFlag::Add(tid, interval) => {
|
||||||
@ -146,16 +160,23 @@ impl Timer {
|
|||||||
timer_map.insert(uid.clone(), task);
|
timer_map.insert(uid.clone(), task);
|
||||||
|
|
||||||
if let Err(e) = self.add_task(&mut delay_timer, uid.clone(), tid, interval) {
|
if let Err(e) = self.add_task(&mut delay_timer, uid.clone(), tid, interval) {
|
||||||
log::error!(target: "app", "Failed to add task for uid {}: {}", uid, e);
|
logging_error!(Type::Timer, "Failed to add task for uid {}: {}", uid, e);
|
||||||
timer_map.remove(&uid); // Rollback on failure
|
timer_map.remove(&uid); // Rollback on failure
|
||||||
} else {
|
} else {
|
||||||
log::debug!(target: "app", "Added task {} for uid {}", tid, uid);
|
logging!(debug, Type::Timer, "Added task {} for uid {}", tid, uid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DiffFlag::Mod(tid, interval) => {
|
DiffFlag::Mod(tid, interval) => {
|
||||||
// Remove old task first
|
// Remove old task first
|
||||||
if let Err(e) = delay_timer.remove_task(tid) {
|
if let Err(e) = delay_timer.remove_task(tid) {
|
||||||
log::warn!(target: "app", "Failed to remove old task {} for uid {}: {}", tid, uid, e);
|
logging!(
|
||||||
|
warn,
|
||||||
|
Type::Timer,
|
||||||
|
"Failed to remove old task {} for uid {}: {}",
|
||||||
|
tid,
|
||||||
|
uid,
|
||||||
|
e
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then add the new one
|
// Then add the new one
|
||||||
@ -168,10 +189,10 @@ impl Timer {
|
|||||||
timer_map.insert(uid.clone(), task);
|
timer_map.insert(uid.clone(), task);
|
||||||
|
|
||||||
if let Err(e) = self.add_task(&mut delay_timer, uid.clone(), tid, interval) {
|
if let Err(e) = self.add_task(&mut delay_timer, uid.clone(), tid, interval) {
|
||||||
log::error!(target: "app", "Failed to update task for uid {}: {}", uid, e);
|
logging_error!(Type::Timer, "Failed to update task for uid {}: {}", uid, e);
|
||||||
timer_map.remove(&uid); // Rollback on failure
|
timer_map.remove(&uid); // Rollback on failure
|
||||||
} else {
|
} else {
|
||||||
log::debug!(target: "app", "Updated task {} for uid {}", tid, uid);
|
logging!(debug, Type::Timer, "Updated task {} for uid {}", tid, uid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -250,7 +271,14 @@ impl Timer {
|
|||||||
tid: TaskID,
|
tid: TaskID,
|
||||||
minutes: u64,
|
minutes: u64,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
log::info!(target: "app", "Adding task: uid={}, id={}, interval={}min", uid, tid, minutes);
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Timer,
|
||||||
|
"Adding task: uid={}, id={}, interval={}min",
|
||||||
|
uid,
|
||||||
|
tid,
|
||||||
|
minutes
|
||||||
|
);
|
||||||
|
|
||||||
// Create a task with reasonable retries and backoff
|
// Create a task with reasonable retries and backoff
|
||||||
let task = TaskBuilder::default()
|
let task = TaskBuilder::default()
|
||||||
@ -275,7 +303,7 @@ impl Timer {
|
|||||||
/// Async task with better error handling and logging
|
/// Async task with better error handling and logging
|
||||||
async fn async_task(uid: String) {
|
async fn async_task(uid: String) {
|
||||||
let task_start = std::time::Instant::now();
|
let task_start = std::time::Instant::now();
|
||||||
log::info!(target: "app", "Running timer task for profile: {}", uid);
|
logging!(info, Type::Timer, "Running timer task for profile: {}", uid);
|
||||||
|
|
||||||
// Update profile
|
// Update profile
|
||||||
let profile_result = feat::update_profile(uid.clone(), None).await;
|
let profile_result = feat::update_profile(uid.clone(), None).await;
|
||||||
@ -286,23 +314,26 @@ impl Timer {
|
|||||||
match CoreManager::global().update_config().await {
|
match CoreManager::global().update_config().await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
let duration = task_start.elapsed().as_millis();
|
let duration = task_start.elapsed().as_millis();
|
||||||
log::info!(
|
logging!(
|
||||||
target: "app",
|
info,
|
||||||
|
Type::Timer,
|
||||||
"Timer task completed successfully for uid: {} (took {}ms)",
|
"Timer task completed successfully for uid: {} (took {}ms)",
|
||||||
uid, duration
|
uid,
|
||||||
|
duration
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!(
|
logging_error!(
|
||||||
target: "app",
|
Type::Timer,
|
||||||
"Failed to refresh config after profile update for uid {}: {}",
|
"Failed to refresh config after profile update for uid {}: {}",
|
||||||
uid, e
|
uid,
|
||||||
|
e
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!(target: "app", "Failed to update profile uid {}: {}", uid, e);
|
logging_error!(Type::Timer, "Failed to update profile uid {}: {}", uid, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ use crate::{
|
|||||||
feat,
|
feat,
|
||||||
module::{lightweight::entry_lightweight_mode, mihomo::Rate},
|
module::{lightweight::entry_lightweight_mode, mihomo::Rate},
|
||||||
resolve,
|
resolve,
|
||||||
utils::{dirs, i18n::t, resolve::VERSION},
|
utils::{dirs::find_target_icons, i18n::t, logging::Type, resolve::VERSION},
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
@ -20,10 +20,7 @@ use parking_lot::Mutex;
|
|||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
pub use speed_rate::{SpeedRate, Traffic};
|
pub use speed_rate::{SpeedRate, Traffic};
|
||||||
#[cfg(target_os = "macos")]
|
use std::fs;
|
||||||
use std::collections::hash_map::DefaultHasher;
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
use std::hash::{Hash, Hasher};
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tauri::{
|
use tauri::{
|
||||||
@ -35,19 +32,124 @@ use tauri::{
|
|||||||
use tokio::sync::broadcast;
|
use tokio::sync::broadcast;
|
||||||
|
|
||||||
use super::handle;
|
use super::handle;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct TrayState {}
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
pub struct Tray {
|
pub struct Tray {
|
||||||
pub speed_rate: Arc<Mutex<Option<SpeedRate>>>,
|
pub speed_rate: Arc<Mutex<Option<SpeedRate>>>,
|
||||||
shutdown_tx: Arc<RwLock<Option<broadcast::Sender<()>>>>,
|
shutdown_tx: Arc<RwLock<Option<broadcast::Sender<()>>>>,
|
||||||
is_subscribed: Arc<RwLock<bool>>,
|
is_subscribed: Arc<RwLock<bool>>,
|
||||||
pub icon_hash: Arc<Mutex<Option<u64>>>,
|
|
||||||
pub icon_cache: Arc<Mutex<Option<Vec<u8>>>>,
|
|
||||||
pub rate_cache: Arc<Mutex<Option<Rate>>>,
|
pub rate_cache: Arc<Mutex<Option<Rate>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
pub struct Tray {}
|
pub struct Tray {}
|
||||||
|
|
||||||
|
impl TrayState {
|
||||||
|
pub fn get_common_tray_icon() -> (bool, Vec<u8>) {
|
||||||
|
let verge = Config::verge().latest().clone();
|
||||||
|
let is_common_tray_icon = verge.common_tray_icon.unwrap_or(false);
|
||||||
|
if is_common_tray_icon {
|
||||||
|
if let Some(common_icon_path) = find_target_icons("common").unwrap() {
|
||||||
|
let icon_data = fs::read(common_icon_path).unwrap();
|
||||||
|
return (true, icon_data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
let tray_icon_colorful = verge.tray_icon.unwrap_or("monochrome".to_string());
|
||||||
|
if tray_icon_colorful == "monochrome" {
|
||||||
|
(
|
||||||
|
false,
|
||||||
|
include_bytes!("../../../icons/tray-icon-mono.ico").to_vec(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
false,
|
||||||
|
include_bytes!("../../../icons/tray-icon.ico").to_vec(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
{
|
||||||
|
(
|
||||||
|
false,
|
||||||
|
include_bytes!("../../../icons/tray-icon.ico").to_vec(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_sysproxy_tray_icon() -> (bool, Vec<u8>) {
|
||||||
|
let verge = Config::verge().latest().clone();
|
||||||
|
let is_sysproxy_tray_icon = verge.sysproxy_tray_icon.unwrap_or(false);
|
||||||
|
if is_sysproxy_tray_icon {
|
||||||
|
if let Some(sysproxy_icon_path) = find_target_icons("sysproxy").unwrap() {
|
||||||
|
let icon_data = fs::read(sysproxy_icon_path).unwrap();
|
||||||
|
return (true, icon_data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
let tray_icon_colorful = verge.tray_icon.clone().unwrap_or("monochrome".to_string());
|
||||||
|
if tray_icon_colorful == "monochrome" {
|
||||||
|
(
|
||||||
|
false,
|
||||||
|
include_bytes!("../../../icons/tray-icon-sys-mono.ico").to_vec(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
false,
|
||||||
|
include_bytes!("../../../icons/tray-icon-sys.ico").to_vec(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
{
|
||||||
|
(
|
||||||
|
false,
|
||||||
|
include_bytes!("../../../icons/tray-icon-sys.ico").to_vec(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_tun_tray_icon() -> (bool, Vec<u8>) {
|
||||||
|
let verge = Config::verge().latest().clone();
|
||||||
|
let is_tun_tray_icon = verge.tun_tray_icon.unwrap_or(false);
|
||||||
|
if is_tun_tray_icon {
|
||||||
|
if let Some(tun_icon_path) = find_target_icons("tun").unwrap() {
|
||||||
|
let icon_data = fs::read(tun_icon_path).unwrap();
|
||||||
|
return (true, icon_data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
let tray_icon_colorful = verge.tray_icon.clone().unwrap_or("monochrome".to_string());
|
||||||
|
if tray_icon_colorful == "monochrome" {
|
||||||
|
(
|
||||||
|
false,
|
||||||
|
include_bytes!("../../../icons/tray-icon-tun-mono.ico").to_vec(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
false,
|
||||||
|
include_bytes!("../../../icons/tray-icon-tun.ico").to_vec(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
{
|
||||||
|
(
|
||||||
|
false,
|
||||||
|
include_bytes!("../../../icons/tray-icon-tun.ico").to_vec(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Tray {
|
impl Tray {
|
||||||
pub fn global() -> &'static Tray {
|
pub fn global() -> &'static Tray {
|
||||||
static TRAY: OnceCell<Tray> = OnceCell::new();
|
static TRAY: OnceCell<Tray> = OnceCell::new();
|
||||||
@ -57,8 +159,6 @@ impl Tray {
|
|||||||
speed_rate: Arc::new(Mutex::new(None)),
|
speed_rate: Arc::new(Mutex::new(None)),
|
||||||
shutdown_tx: Arc::new(RwLock::new(None)),
|
shutdown_tx: Arc::new(RwLock::new(None)),
|
||||||
is_subscribed: Arc::new(RwLock::new(false)),
|
is_subscribed: Arc::new(RwLock::new(false)),
|
||||||
icon_hash: Arc::new(Mutex::new(None)),
|
|
||||||
icon_cache: Arc::new(Mutex::new(None)),
|
|
||||||
rate_cache: Arc::new(Mutex::new(None)),
|
rate_cache: Arc::new(Mutex::new(None)),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -105,7 +205,7 @@ impl Tray {
|
|||||||
match tray_event.as_str() {
|
match tray_event.as_str() {
|
||||||
"system_proxy" => feat::toggle_system_proxy(),
|
"system_proxy" => feat::toggle_system_proxy(),
|
||||||
"tun_mode" => feat::toggle_tun_mode(None),
|
"tun_mode" => feat::toggle_tun_mode(None),
|
||||||
"main_window" => resolve::create_window(),
|
"main_window" => resolve::create_window(true),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -159,107 +259,29 @@ impl Tray {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 更新托盘图标
|
/// 更新托盘图标
|
||||||
#[allow(unused_variables)]
|
|
||||||
pub fn update_icon(&self, rate: Option<Rate>) -> Result<()> {
|
pub fn update_icon(&self, rate: Option<Rate>) -> Result<()> {
|
||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
|
||||||
let verge = Config::verge().latest().clone();
|
let verge = Config::verge().latest().clone();
|
||||||
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
|
let system_mode = verge.enable_system_proxy.as_ref().unwrap_or(&false);
|
||||||
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
|
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
|
||||||
|
|
||||||
let common_tray_icon = verge.common_tray_icon.as_ref().unwrap_or(&false);
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||||
let sysproxy_tray_icon = verge.sysproxy_tray_icon.as_ref().unwrap_or(&false);
|
|
||||||
let tun_tray_icon = verge.tun_tray_icon.as_ref().unwrap_or(&false);
|
|
||||||
|
|
||||||
let tray = app_handle.tray_by_id("main").unwrap();
|
let tray = app_handle.tray_by_id("main").unwrap();
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
let (is_custom_icon, icon_bytes) = match (*system_mode, *tun_mode) {
|
||||||
let tray_icon = verge.tray_icon.clone().unwrap_or("monochrome".to_string());
|
(true, true) => TrayState::get_tun_tray_icon(),
|
||||||
|
(true, false) => TrayState::get_sysproxy_tray_icon(),
|
||||||
let icon_bytes = if *system_proxy && !*tun_mode {
|
(false, true) => TrayState::get_tun_tray_icon(),
|
||||||
#[cfg(target_os = "macos")]
|
(false, false) => TrayState::get_common_tray_icon(),
|
||||||
let mut icon = match tray_icon.as_str() {
|
|
||||||
"colorful" => include_bytes!("../../../icons/tray-icon-sys.ico").to_vec(),
|
|
||||||
_ => include_bytes!("../../../icons/tray-icon-sys-mono.ico").to_vec(),
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
|
||||||
let mut icon = include_bytes!("../../../icons/tray-icon-sys.ico").to_vec();
|
|
||||||
if *sysproxy_tray_icon {
|
|
||||||
let icon_dir_path = dirs::app_home_dir()?.join("icons");
|
|
||||||
let png_path = icon_dir_path.join("sysproxy.png");
|
|
||||||
let ico_path = icon_dir_path.join("sysproxy.ico");
|
|
||||||
if ico_path.exists() {
|
|
||||||
icon = std::fs::read(ico_path).unwrap();
|
|
||||||
} else if png_path.exists() {
|
|
||||||
icon = std::fs::read(png_path).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
icon
|
|
||||||
} else if *tun_mode {
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
let mut icon = match tray_icon.as_str() {
|
|
||||||
"colorful" => include_bytes!("../../../icons/tray-icon-tun.ico").to_vec(),
|
|
||||||
_ => include_bytes!("../../../icons/tray-icon-tun-mono.ico").to_vec(),
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
|
||||||
let mut icon = include_bytes!("../../../icons/tray-icon-tun.ico").to_vec();
|
|
||||||
if *tun_tray_icon {
|
|
||||||
let icon_dir_path = dirs::app_home_dir()?.join("icons");
|
|
||||||
let png_path = icon_dir_path.join("tun.png");
|
|
||||||
let ico_path = icon_dir_path.join("tun.ico");
|
|
||||||
if ico_path.exists() {
|
|
||||||
icon = std::fs::read(ico_path).unwrap();
|
|
||||||
} else if png_path.exists() {
|
|
||||||
icon = std::fs::read(png_path).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
icon
|
|
||||||
} else {
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
let mut icon = match tray_icon.as_str() {
|
|
||||||
"colorful" => include_bytes!("../../../icons/tray-icon.ico").to_vec(),
|
|
||||||
_ => include_bytes!("../../../icons/tray-icon-mono.ico").to_vec(),
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
|
||||||
let mut icon = include_bytes!("../../../icons/tray-icon.ico").to_vec();
|
|
||||||
if *common_tray_icon {
|
|
||||||
let icon_dir_path = dirs::app_home_dir()?.join("icons");
|
|
||||||
let png_path = icon_dir_path.join("common.png");
|
|
||||||
let ico_path = icon_dir_path.join("common.ico");
|
|
||||||
if ico_path.exists() {
|
|
||||||
icon = std::fs::read(ico_path).unwrap();
|
|
||||||
} else if png_path.exists() {
|
|
||||||
icon = std::fs::read(png_path).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
icon
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
let enable_tray_speed = verge.enable_tray_speed.unwrap_or(true);
|
let enable_tray_speed = verge.enable_tray_speed.unwrap_or(true);
|
||||||
let enable_tray_icon = verge.enable_tray_icon.unwrap_or(true);
|
let enable_tray_icon = verge.enable_tray_icon.unwrap_or(true);
|
||||||
let is_colorful = tray_icon == "colorful";
|
let colorful = verge.tray_icon.clone().unwrap_or("monochrome".to_string());
|
||||||
|
let is_colorful = colorful == "colorful";
|
||||||
|
|
||||||
let icon_hash = {
|
if !enable_tray_speed {
|
||||||
let mut hasher = DefaultHasher::new();
|
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
|
||||||
icon_bytes.clone().hash(&mut hasher);
|
|
||||||
hasher.finish()
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut icon_hash_guard = self.icon_hash.lock();
|
|
||||||
let mut icon_bytes_guard = self.icon_cache.lock();
|
|
||||||
if *icon_hash_guard != Some(icon_hash) {
|
|
||||||
*icon_hash_guard = Some(icon_hash);
|
|
||||||
*icon_bytes_guard = Some(icon_bytes.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
if !enable_tray_speed || (!enable_tray_speed && !enable_tray_icon) {
|
|
||||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(
|
|
||||||
&(*icon_bytes_guard).clone().unwrap(),
|
|
||||||
)?));
|
|
||||||
let _ = tray.set_icon_as_template(!is_colorful);
|
let _ = tray.set_icon_as_template(!is_colorful);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@ -280,16 +302,15 @@ impl Tray {
|
|||||||
*rate_guard = rate;
|
*rate_guard = rate;
|
||||||
|
|
||||||
let bytes = if enable_tray_icon {
|
let bytes = if enable_tray_icon {
|
||||||
Some(icon_bytes_guard.as_ref().unwrap())
|
Some(icon_bytes)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let rate = rate_guard.as_ref();
|
let rate = rate_guard.as_ref();
|
||||||
let rate_bytes = SpeedRate::add_speed_text(bytes, rate).unwrap();
|
let rate_bytes = SpeedRate::add_speed_text(is_custom_icon, bytes, rate).unwrap();
|
||||||
|
|
||||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&rate_bytes)?));
|
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&rate_bytes)?));
|
||||||
let _ = tray.set_icon_as_template(!is_colorful);
|
let _ = tray.set_icon_as_template(!is_custom_icon && !is_colorful);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -654,18 +675,17 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
|||||||
println!("change mode to: {}", mode);
|
println!("change mode to: {}", mode);
|
||||||
feat::change_clash_mode(mode.into());
|
feat::change_clash_mode(mode.into());
|
||||||
}
|
}
|
||||||
"open_window" => resolve::create_window(),
|
"open_window" => resolve::create_window(true),
|
||||||
"system_proxy" => feat::toggle_system_proxy(),
|
"system_proxy" => feat::toggle_system_proxy(),
|
||||||
"tun_mode" => feat::toggle_tun_mode(None),
|
"tun_mode" => feat::toggle_tun_mode(None),
|
||||||
"copy_env" => feat::copy_clash_env(),
|
"copy_env" => feat::copy_clash_env(),
|
||||||
"open_app_dir" => crate::log_err!(cmd::open_app_dir()),
|
"open_app_dir" => crate::logging_error!(Type::Cmd, true, cmd::open_app_dir()),
|
||||||
"open_core_dir" => crate::log_err!(cmd::open_core_dir()),
|
"open_core_dir" => crate::logging_error!(Type::Cmd, true, cmd::open_core_dir()),
|
||||||
"open_logs_dir" => crate::log_err!(cmd::open_logs_dir()),
|
"open_logs_dir" => crate::logging_error!(Type::Cmd, true, cmd::open_logs_dir()),
|
||||||
"restart_clash" => feat::restart_clash_core(),
|
"restart_clash" => feat::restart_clash_core(),
|
||||||
"restart_app" => feat::restart_app(),
|
"restart_app" => feat::restart_app(),
|
||||||
"entry_lightweight_mode" => entry_lightweight_mode(),
|
"entry_lightweight_mode" => entry_lightweight_mode(),
|
||||||
"quit" => {
|
"quit" => {
|
||||||
println!("quit");
|
|
||||||
feat::quit(Some(0));
|
feat::quit(Some(0));
|
||||||
}
|
}
|
||||||
id if id.starts_with("profiles_") => {
|
id if id.starts_with("profiles_") => {
|
||||||
|
@ -76,15 +76,16 @@ impl SpeedRate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 分离图标加载和速率渲染
|
// 分离图标加载和速率渲染
|
||||||
pub fn add_speed_text<'a>(
|
pub fn add_speed_text(
|
||||||
icon_bytes: Option<&'a Vec<u8>>,
|
is_custom_icon: bool,
|
||||||
rate: Option<&'a Rate>,
|
icon_bytes: Option<Vec<u8>>,
|
||||||
|
rate: Option<&Rate>,
|
||||||
) -> Result<Vec<u8>> {
|
) -> Result<Vec<u8>> {
|
||||||
let rate = rate.unwrap_or(&Rate { up: 0, down: 0 });
|
let rate = rate.unwrap_or(&Rate { up: 0, down: 0 });
|
||||||
|
|
||||||
let (mut icon_width, mut icon_height) = (0, 256);
|
let (mut icon_width, mut icon_height) = (0, 256);
|
||||||
let icon_image = if let Some(bytes) = icon_bytes {
|
let icon_image = if let Some(bytes) = icon_bytes.clone() {
|
||||||
let icon_image = image::load_from_memory(bytes)?;
|
let icon_image = image::load_from_memory(&bytes)?;
|
||||||
icon_width = icon_image.width();
|
icon_width = icon_image.width();
|
||||||
icon_height = icon_image.height();
|
icon_height = icon_image.height();
|
||||||
icon_image
|
icon_image
|
||||||
@ -93,23 +94,17 @@ impl SpeedRate {
|
|||||||
image::DynamicImage::new_rgba8(0, 0)
|
image::DynamicImage::new_rgba8(0, 0)
|
||||||
};
|
};
|
||||||
|
|
||||||
// 判断是否为彩色图标
|
let total_width = match (is_custom_icon, icon_bytes.is_some()) {
|
||||||
let is_colorful = if let Some(bytes) = icon_bytes {
|
(true, true) => 510,
|
||||||
!crate::utils::help::is_monochrome_image_from_bytes(bytes).unwrap_or(false)
|
(true, false) => 740,
|
||||||
} else {
|
(false, false) => 740,
|
||||||
false
|
(false, true) => icon_width + 740,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 增加文本宽度和间距
|
// println!(
|
||||||
let total_width = if icon_bytes.is_some() {
|
// "icon_height: {}, icon_wight: {}, total_width: {}",
|
||||||
if icon_width < 580 {
|
// icon_height, icon_width, total_width
|
||||||
icon_width + 580
|
// );
|
||||||
} else {
|
|
||||||
icon_width
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
580
|
|
||||||
};
|
|
||||||
|
|
||||||
// 创建新的透明画布
|
// 创建新的透明画布
|
||||||
let mut combined_image = RgbaImage::new(total_width, icon_height);
|
let mut combined_image = RgbaImage::new(total_width, icon_height);
|
||||||
@ -124,18 +119,28 @@ impl SpeedRate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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]),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
// 减小字体大小以适应文本区域
|
// 减小字体大小以适应文本区域
|
||||||
@ -145,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;
|
||||||
|
1
src-tauri/src/error/mod.rs
Normal file
1
src-tauri/src/error/mod.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod service;
|
1
src-tauri/src/error/service.rs
Normal file
1
src-tauri/src/error/service.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
@ -1,8 +1,8 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
config::{Config, IVerge},
|
config::{Config, IVerge},
|
||||||
core::backup,
|
core::backup,
|
||||||
log_err,
|
logging_error,
|
||||||
utils::dirs::app_home_dir,
|
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;
|
||||||
@ -69,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,9 +1,9 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
config::Config,
|
config::Config,
|
||||||
core::{handle, tray, CoreManager},
|
core::{handle, tray, CoreManager},
|
||||||
log_err,
|
logging_error,
|
||||||
module::mihomo::MihomoManager,
|
module::mihomo::MihomoManager,
|
||||||
utils::resolve,
|
utils::{logging::Type, resolve},
|
||||||
};
|
};
|
||||||
use serde_yaml::{Mapping, Value};
|
use serde_yaml::{Mapping, Value};
|
||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
@ -28,12 +28,31 @@ pub fn restart_clash_core() {
|
|||||||
pub fn restart_app() {
|
pub fn restart_app() {
|
||||||
tauri::async_runtime::spawn_blocking(|| {
|
tauri::async_runtime::spawn_blocking(|| {
|
||||||
tauri::async_runtime::block_on(async {
|
tauri::async_runtime::block_on(async {
|
||||||
log_err!(CoreManager::global().stop_core().await);
|
logging_error!(Type::Core, true, CoreManager::global().stop_core().await);
|
||||||
|
resolve::resolve_reset_async().await;
|
||||||
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||||
|
tauri::process::restart(&app_handle.env());
|
||||||
});
|
});
|
||||||
resolve::resolve_reset();
|
});
|
||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
}
|
||||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
|
||||||
tauri::process::restart(&app_handle.env());
|
fn after_change_clash_mode() {
|
||||||
|
tauri::async_runtime::spawn(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,8 +66,6 @@ pub fn change_clash_mode(mode: String) {
|
|||||||
});
|
});
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
log::debug!(target: "app", "change clash mode to {mode}");
|
log::debug!(target: "app", "change clash mode to {mode}");
|
||||||
CoreManager::global().ensure_running_core().await;
|
|
||||||
|
|
||||||
match MihomoManager::global().patch_configs(json_value).await {
|
match MihomoManager::global().patch_configs(json_value).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// 更新订阅
|
// 更新订阅
|
||||||
@ -56,8 +73,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) => println!("{err}"),
|
Err(err) => println!("{err}"),
|
||||||
@ -67,7 +92,6 @@ 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> {
|
||||||
CoreManager::global().ensure_running_core().await;
|
|
||||||
use tokio::time::{Duration, Instant};
|
use tokio::time::{Duration, Instant};
|
||||||
let mut builder = reqwest::ClientBuilder::new().use_rustls_tls().no_proxy();
|
let mut builder = reqwest::ClientBuilder::new().use_rustls_tls().no_proxy();
|
||||||
|
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
config::{Config, IVerge},
|
config::{Config, IVerge},
|
||||||
core::{handle, hotkey, sysopt, tray, CoreManager},
|
core::{handle, hotkey, sysopt, tray, CoreManager},
|
||||||
log_err,
|
logging_error,
|
||||||
module::lightweight,
|
module::lightweight,
|
||||||
|
utils::logging::Type,
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use serde_yaml::Mapping;
|
use serde_yaml::Mapping;
|
||||||
@ -18,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?;
|
||||||
|
@ -82,3 +82,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(|_| ())
|
||||||
|
}
|
||||||
|
@ -28,6 +28,19 @@ pub fn toggle_system_proxy() {
|
|||||||
|
|
||||||
/// Toggle TUN mode on/off
|
/// Toggle TUN mode on/off
|
||||||
pub fn toggle_tun_mode(not_save_file: Option<bool>) {
|
pub fn toggle_tun_mode(not_save_file: Option<bool>) {
|
||||||
|
// tauri::async_runtime::spawn(async move {
|
||||||
|
// logging!(
|
||||||
|
// info,
|
||||||
|
// Type::Service,
|
||||||
|
// true,
|
||||||
|
// "Toggle TUN mode need install service"
|
||||||
|
// );
|
||||||
|
// if is_service_available().await.is_err() {
|
||||||
|
// logging_error!(Type::Service, true, install_service().await);
|
||||||
|
// }
|
||||||
|
// logging_error!(Type::Core, true, CoreManager::global().restart_core().await);
|
||||||
|
// });
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
|
@ -6,8 +6,6 @@ use crate::{
|
|||||||
module::mihomo::MihomoManager,
|
module::mihomo::MihomoManager,
|
||||||
utils::resolve,
|
utils::resolve,
|
||||||
};
|
};
|
||||||
use tauri::Manager;
|
|
||||||
use tauri_plugin_window_state::{AppHandleExt, StateFlags};
|
|
||||||
|
|
||||||
/// Open or close the dashboard window
|
/// Open or close the dashboard window
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@ -47,40 +45,10 @@ pub fn open_or_close_dashboard() {
|
|||||||
} else {
|
} else {
|
||||||
println!("No existing window found, creating new window");
|
println!("No existing window found, creating new window");
|
||||||
log::info!(target: "app", "No existing window found, creating new window");
|
log::info!(target: "app", "No existing window found, creating new window");
|
||||||
resolve::create_window();
|
resolve::create_window(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Setup window state monitor to save window position and size in real-time
|
|
||||||
pub fn setup_window_state_monitor(app_handle: &tauri::AppHandle) {
|
|
||||||
let window = app_handle.get_webview_window("main").unwrap();
|
|
||||||
let app_handle_clone = app_handle.clone();
|
|
||||||
|
|
||||||
// 监听窗口移动事件
|
|
||||||
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>) {
|
pub fn quit(code: Option<i32>) {
|
||||||
log::debug!(target: "app", "启动退出流程");
|
log::debug!(target: "app", "启动退出流程");
|
||||||
|
@ -2,6 +2,7 @@ mod cmd;
|
|||||||
mod config;
|
mod config;
|
||||||
mod core;
|
mod core;
|
||||||
mod enhance;
|
mod enhance;
|
||||||
|
mod error;
|
||||||
mod feat;
|
mod feat;
|
||||||
mod module;
|
mod module;
|
||||||
mod utils;
|
mod utils;
|
||||||
@ -16,6 +17,7 @@ use tauri::AppHandle;
|
|||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
use tauri_plugin_autostart::MacosLauncher;
|
use tauri_plugin_autostart::MacosLauncher;
|
||||||
use tauri_plugin_deep_link::DeepLinkExt;
|
use tauri_plugin_deep_link::DeepLinkExt;
|
||||||
|
use utils::logging::Type;
|
||||||
|
|
||||||
/// A global singleton handle to the application.
|
/// A global singleton handle to the application.
|
||||||
pub struct AppHandleManager {
|
pub struct AppHandleManager {
|
||||||
@ -110,7 +112,6 @@ pub fn run() {
|
|||||||
.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())
|
||||||
@ -120,13 +121,13 @@ pub fn run() {
|
|||||||
#[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_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 {
|
tauri::async_runtime::spawn(async move {
|
||||||
if let Some(url) = event.urls().first() {
|
if let Some(url) = event.urls().first() {
|
||||||
log_err!(resolve_scheme(url.to_string()).await);
|
logging_error!(Type::Setup, true, resolve_scheme(url.to_string()).await);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -151,10 +152,14 @@ pub fn run() {
|
|||||||
cmd::restart_app,
|
cmd::restart_app,
|
||||||
// 添加新的命令
|
// 添加新的命令
|
||||||
cmd::get_running_mode,
|
cmd::get_running_mode,
|
||||||
cmd::install_service,
|
|
||||||
cmd::repair_service,
|
|
||||||
cmd::get_app_uptime,
|
cmd::get_app_uptime,
|
||||||
cmd::get_auto_launch_status,
|
cmd::get_auto_launch_status,
|
||||||
|
cmd::is_admin,
|
||||||
|
// service 管理
|
||||||
|
cmd::install_service,
|
||||||
|
cmd::uninstall_service,
|
||||||
|
cmd::reinstall_service,
|
||||||
|
cmd::repair_service,
|
||||||
// clash
|
// clash
|
||||||
cmd::get_clash_info,
|
cmd::get_clash_info,
|
||||||
cmd::patch_clash_config,
|
cmd::patch_clash_config,
|
||||||
@ -277,13 +282,25 @@ pub fn run() {
|
|||||||
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!(
|
||||||
log_err!(hotkey::Hotkey::global().register("CMD+W", "hide"));
|
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"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
{
|
{
|
||||||
log_err!(hotkey::Hotkey::global().register("Control+Q", "quit"));
|
logging_error!(
|
||||||
|
Type::Hotkey,
|
||||||
|
true,
|
||||||
|
hotkey::Hotkey::global().register("Control+Q", "quit")
|
||||||
|
);
|
||||||
};
|
};
|
||||||
{
|
{
|
||||||
let is_enable_global_hotkey = Config::verge()
|
let is_enable_global_hotkey = Config::verge()
|
||||||
@ -291,19 +308,31 @@ pub fn run() {
|
|||||||
.enable_global_hotkey
|
.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!(
|
||||||
log_err!(hotkey::Hotkey::global().unregister("CMD+W"));
|
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"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
{
|
{
|
||||||
log_err!(hotkey::Hotkey::global().unregister("Control+Q"));
|
logging_error!(
|
||||||
|
Type::Hotkey,
|
||||||
|
true,
|
||||||
|
hotkey::Hotkey::global().unregister("Control+Q")
|
||||||
|
);
|
||||||
};
|
};
|
||||||
{
|
{
|
||||||
let is_enable_global_hotkey = Config::verge()
|
let is_enable_global_hotkey = Config::verge()
|
||||||
@ -311,20 +340,32 @@ pub fn run() {
|
|||||||
.enable_global_hotkey
|
.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!(
|
||||||
log_err!(hotkey::Hotkey::global().unregister("CMD+W"));
|
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"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
{
|
{
|
||||||
log_err!(hotkey::Hotkey::global().unregister("Control+Q"));
|
logging_error!(
|
||||||
|
Type::Hotkey,
|
||||||
|
true,
|
||||||
|
hotkey::Hotkey::global().unregister("Control+Q")
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
@ -5,21 +5,22 @@ use tauri::{Listener, Manager};
|
|||||||
use crate::{
|
use crate::{
|
||||||
config::Config,
|
config::Config,
|
||||||
core::{handle, timer::Timer},
|
core::{handle, timer::Timer},
|
||||||
log_err, AppHandleManager,
|
log_err, logging,
|
||||||
|
utils::logging::Type,
|
||||||
|
AppHandleManager,
|
||||||
};
|
};
|
||||||
|
|
||||||
const LIGHT_WEIGHT_TASK_UID: &str = "light_weight_task";
|
const LIGHT_WEIGHT_TASK_UID: &str = "light_weight_task";
|
||||||
|
|
||||||
pub fn enable_auto_light_weight_mode() {
|
pub fn enable_auto_light_weight_mode() {
|
||||||
println!("[lightweight_mode] 开启自动轻量模式");
|
Timer::global().init().unwrap();
|
||||||
log::info!(target: "app", "[lightweight_mode] 开启自动轻量模式");
|
logging!(info, Type::Lightweight, true, "开启自动轻量模式");
|
||||||
setup_window_close_listener();
|
setup_window_close_listener();
|
||||||
setup_webview_focus_listener();
|
setup_webview_focus_listener();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn disable_auto_light_weight_mode() {
|
pub fn disable_auto_light_weight_mode() {
|
||||||
println!("[lightweight_mode] 关闭自动轻量模式");
|
logging!(info, Type::Lightweight, true, "关闭自动轻量模式");
|
||||||
log::info!(target: "app", "[lightweight_mode] 关闭自动轻量模式");
|
|
||||||
let _ = cancel_light_weight_timer();
|
let _ = cancel_light_weight_timer();
|
||||||
cancel_window_close_listener();
|
cancel_window_close_listener();
|
||||||
}
|
}
|
||||||
@ -34,8 +35,7 @@ pub fn entry_lightweight_mode() {
|
|||||||
}
|
}
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
AppHandleManager::global().set_activation_policy_accessory();
|
AppHandleManager::global().set_activation_policy_accessory();
|
||||||
println!("[lightweight_mode] 轻量模式已开启");
|
logging!(info, Type::Lightweight, true, "轻量模式已开启");
|
||||||
log::info!(target: "app", "[lightweight_mode] 轻量模式已开启");
|
|
||||||
}
|
}
|
||||||
let _ = cancel_light_weight_timer();
|
let _ = cancel_light_weight_timer();
|
||||||
}
|
}
|
||||||
@ -44,8 +44,12 @@ fn setup_window_close_listener() -> u32 {
|
|||||||
if let Some(window) = handle::Handle::global().get_window() {
|
if let Some(window) = handle::Handle::global().get_window() {
|
||||||
let handler = window.listen("tauri://close-requested", move |_event| {
|
let handler = window.listen("tauri://close-requested", move |_event| {
|
||||||
let _ = setup_light_weight_timer();
|
let _ = setup_light_weight_timer();
|
||||||
println!("[lightweight_mode] 监听到关闭请求,开始轻量模式计时");
|
logging!(
|
||||||
log::info!(target: "app", "[lightweight_mode] 监听到关闭请求,开始轻量模式计时");
|
info,
|
||||||
|
Type::Lightweight,
|
||||||
|
true,
|
||||||
|
"监听到关闭请求,开始轻量模式计时"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
return handler;
|
return handler;
|
||||||
}
|
}
|
||||||
@ -56,8 +60,12 @@ fn setup_webview_focus_listener() -> u32 {
|
|||||||
if let Some(window) = handle::Handle::global().get_window() {
|
if let Some(window) = handle::Handle::global().get_window() {
|
||||||
let handler = window.listen("tauri://focus", move |_event| {
|
let handler = window.listen("tauri://focus", move |_event| {
|
||||||
log_err!(cancel_light_weight_timer());
|
log_err!(cancel_light_weight_timer());
|
||||||
println!("[lightweight_mode] 监听到窗口获得焦点,取消轻量模式计时");
|
logging!(
|
||||||
log::info!(target: "app", "[lightweight_mode] 监听到窗口获得焦点,取消轻量模式计时");
|
info,
|
||||||
|
Type::Lightweight,
|
||||||
|
true,
|
||||||
|
"监听到窗口获得焦点,取消轻量模式计时"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
return handler;
|
return handler;
|
||||||
}
|
}
|
||||||
@ -67,8 +75,7 @@ fn setup_webview_focus_listener() -> u32 {
|
|||||||
fn cancel_window_close_listener() {
|
fn cancel_window_close_listener() {
|
||||||
if let Some(window) = handle::Handle::global().get_window() {
|
if let Some(window) = handle::Handle::global().get_window() {
|
||||||
window.unlisten(setup_window_close_listener());
|
window.unlisten(setup_window_close_listener());
|
||||||
println!("[lightweight_mode] 取消了窗口关闭监听");
|
logging!(info, Type::Lightweight, true, "取消了窗口关闭监听");
|
||||||
log::info!(target: "app", "[lightweight_mode] 取消了窗口关闭监听");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,7 +92,6 @@ fn setup_light_weight_timer() -> Result<()> {
|
|||||||
let once_by_minutes = Config::verge()
|
let once_by_minutes = Config::verge()
|
||||||
.latest()
|
.latest()
|
||||||
.auto_light_weight_minutes
|
.auto_light_weight_minutes
|
||||||
.clone()
|
|
||||||
.unwrap_or(10);
|
.unwrap_or(10);
|
||||||
|
|
||||||
let task = TaskBuilder::default()
|
let task = TaskBuilder::default()
|
||||||
@ -93,17 +99,14 @@ fn setup_light_weight_timer() -> Result<()> {
|
|||||||
.set_maximum_parallel_runnable_num(1)
|
.set_maximum_parallel_runnable_num(1)
|
||||||
.set_frequency_once_by_minutes(once_by_minutes)
|
.set_frequency_once_by_minutes(once_by_minutes)
|
||||||
.spawn_async_routine(move || async move {
|
.spawn_async_routine(move || async move {
|
||||||
println!("[lightweight_mode] 计时器到期,开始进入轻量模式");
|
logging!(info, Type::Timer, true, "计时器到期,开始进入轻量模式");
|
||||||
log::info!(target: "app",
|
|
||||||
"[lightweight_mode] 计时器到期,开始进入轻量模式"
|
|
||||||
);
|
|
||||||
entry_lightweight_mode();
|
entry_lightweight_mode();
|
||||||
})
|
})
|
||||||
.context("failed to create light weight timer task")?;
|
.context("failed to create timer task")?;
|
||||||
|
|
||||||
delay_timer
|
delay_timer
|
||||||
.add_task(task)
|
.add_task(task)
|
||||||
.context("failed to add light weight timer task")?;
|
.context("failed to add timer task")?;
|
||||||
|
|
||||||
let timer_task = crate::core::timer::TimerTask {
|
let timer_task = crate::core::timer::TimerTask {
|
||||||
task_id,
|
task_id,
|
||||||
@ -113,12 +116,11 @@ fn setup_light_weight_timer() -> Result<()> {
|
|||||||
|
|
||||||
timer_map.insert(LIGHT_WEIGHT_TASK_UID.to_string(), timer_task);
|
timer_map.insert(LIGHT_WEIGHT_TASK_UID.to_string(), timer_task);
|
||||||
|
|
||||||
println!(
|
logging!(
|
||||||
"[lightweight_mode] 轻量模式计时器已设置,{} 分钟后将自动进入轻量模式",
|
info,
|
||||||
once_by_minutes
|
Type::Timer,
|
||||||
);
|
true,
|
||||||
log::info!(target: "app",
|
"计时器已设置,{} 分钟后将自动进入轻量模式",
|
||||||
"[lightweight_mode] 轻量模式计时器已设置,{} 分钟后将自动进入轻量模式",
|
|
||||||
once_by_minutes
|
once_by_minutes
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -132,9 +134,8 @@ fn cancel_light_weight_timer() -> Result<()> {
|
|||||||
if let Some(task) = timer_map.remove(LIGHT_WEIGHT_TASK_UID) {
|
if let Some(task) = timer_map.remove(LIGHT_WEIGHT_TASK_UID) {
|
||||||
delay_timer
|
delay_timer
|
||||||
.remove_task(task.task_id)
|
.remove_task(task.task_id)
|
||||||
.context("failed to remove light weight timer task")?;
|
.context("failed to remove timer task")?;
|
||||||
println!("[lightweight_mode] 轻量模式计时器已取消");
|
logging!(info, Type::Timer, true, "计时器已取消");
|
||||||
log::info!(target: "app", "[lightweight_mode] 轻量模式计时器已取消");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
|
pub mod lightweight;
|
||||||
pub mod mihomo;
|
pub mod mihomo;
|
||||||
pub mod sysinfo;
|
pub mod sysinfo;
|
||||||
pub mod lightweight;
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -32,16 +36,10 @@ impl PlatformSpecification {
|
|||||||
let config = handler.config();
|
let config = handler.config();
|
||||||
let verge_version = config.version.clone().unwrap_or("Null".into());
|
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 {
|
let is_admin = system::is_admin().unwrap_or_default();
|
||||||
crate::core::RunningMode::Service => "Service".to_string(),
|
|
||||||
crate::core::RunningMode::Sidecar => "Sidecar".to_string(),
|
|
||||||
crate::core::RunningMode::NotRunning => "Not Running".to_string(),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
system_name,
|
system_name,
|
||||||
@ -50,6 +48,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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,6 +77,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"))
|
||||||
|
@ -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 {
|
||||||
@ -213,15 +189,16 @@ macro_rules! t {
|
|||||||
/// ```
|
/// ```
|
||||||
#[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")]
|
||||||
|
@ -177,6 +177,7 @@ fn init_dns_config() -> Result<()> {
|
|||||||
(
|
(
|
||||||
"default-nameserver".into(),
|
"default-nameserver".into(),
|
||||||
Value::Sequence(vec![
|
Value::Sequence(vec![
|
||||||
|
Value::String("system".into()),
|
||||||
Value::String("223.6.6.6".into()),
|
Value::String("223.6.6.6".into()),
|
||||||
Value::String("8.8.8.8".into()),
|
Value::String("8.8.8.8".into()),
|
||||||
]),
|
]),
|
||||||
@ -189,14 +190,7 @@ fn init_dns_config() -> Result<()> {
|
|||||||
Value::String("https://dns.alidns.com/dns-query".into()),
|
Value::String("https://dns.alidns.com/dns-query".into()),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
(
|
("fallback".into(), Value::Sequence(vec![])),
|
||||||
"fallback".into(),
|
|
||||||
Value::Sequence(vec![
|
|
||||||
Value::String("https://dns.alidns.com/dns-query".into()),
|
|
||||||
Value::String("https://dns.google/dns-query".into()),
|
|
||||||
Value::String("https://cloudflare-dns.com/dns-query".into()),
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
"nameserver-policy".into(),
|
"nameserver-policy".into(),
|
||||||
Value::Mapping(serde_yaml::Mapping::new()),
|
Value::Mapping(serde_yaml::Mapping::new()),
|
||||||
@ -206,6 +200,7 @@ fn init_dns_config() -> Result<()> {
|
|||||||
Value::Sequence(vec![
|
Value::Sequence(vec![
|
||||||
Value::String("https://doh.pub/dns-query".into()),
|
Value::String("https://doh.pub/dns-query".into()),
|
||||||
Value::String("https://dns.alidns.com/dns-query".into()),
|
Value::String("https://dns.alidns.com/dns-query".into()),
|
||||||
|
Value::String("tls://223.5.5.5".into()),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
("direct-nameserver".into(), Value::Sequence(vec![])),
|
("direct-nameserver".into(), Value::Sequence(vec![])),
|
||||||
@ -300,15 +295,11 @@ pub fn init_config() -> Result<()> {
|
|||||||
/// after tauri setup
|
/// after tauri setup
|
||||||
pub fn init_resources() -> Result<()> {
|
pub fn init_resources() -> Result<()> {
|
||||||
let app_dir = dirs::app_home_dir()?;
|
let app_dir = dirs::app_home_dir()?;
|
||||||
let test_dir = app_dir.join("test");
|
|
||||||
let res_dir = dirs::app_resources_dir()?;
|
let res_dir = dirs::app_resources_dir()?;
|
||||||
|
|
||||||
if !app_dir.exists() {
|
if !app_dir.exists() {
|
||||||
let _ = fs::create_dir_all(&app_dir);
|
let _ = fs::create_dir_all(&app_dir);
|
||||||
}
|
}
|
||||||
if !test_dir.exists() {
|
|
||||||
let _ = fs::create_dir_all(&test_dir);
|
|
||||||
}
|
|
||||||
if !res_dir.exists() {
|
if !res_dir.exists() {
|
||||||
let _ = fs::create_dir_all(&res_dir);
|
let _ = fs::create_dir_all(&res_dir);
|
||||||
}
|
}
|
||||||
@ -320,7 +311,6 @@ pub fn init_resources() -> Result<()> {
|
|||||||
for file in file_list.iter() {
|
for file in file_list.iter() {
|
||||||
let src_path = res_dir.join(file);
|
let src_path = res_dir.join(file);
|
||||||
let dest_path = app_dir.join(file);
|
let dest_path = app_dir.join(file);
|
||||||
let test_dest_path = test_dir.join(file);
|
|
||||||
log::debug!(target: "app", "src_path: {src_path:?}, dest_path: {dest_path:?}");
|
log::debug!(target: "app", "src_path: {src_path:?}, dest_path: {dest_path:?}");
|
||||||
|
|
||||||
let handle_copy = |dest: &PathBuf| {
|
let handle_copy = |dest: &PathBuf| {
|
||||||
@ -332,9 +322,6 @@ pub fn init_resources() -> Result<()> {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
if src_path.exists() && !test_dest_path.exists() {
|
|
||||||
handle_copy(&test_dest_path);
|
|
||||||
}
|
|
||||||
if src_path.exists() && !dest_path.exists() {
|
if src_path.exists() && !dest_path.exists() {
|
||||||
handle_copy(&dest_path);
|
handle_copy(&dest_path);
|
||||||
continue;
|
continue;
|
||||||
|
139
src-tauri/src/utils/logging.rs
Normal file
139
src-tauri/src/utils/logging.rs
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Type {
|
||||||
|
Cmd,
|
||||||
|
Core,
|
||||||
|
Config,
|
||||||
|
Setup,
|
||||||
|
System,
|
||||||
|
Service,
|
||||||
|
Hotkey,
|
||||||
|
Window,
|
||||||
|
Tray,
|
||||||
|
Timer,
|
||||||
|
Frontend,
|
||||||
|
Backup,
|
||||||
|
Lightweight,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Type {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Type::Cmd => write!(f, "[Cmd]"),
|
||||||
|
Type::Core => write!(f, "[Core]"),
|
||||||
|
Type::Config => write!(f, "[Config]"),
|
||||||
|
Type::Setup => write!(f, "[Setup]"),
|
||||||
|
Type::System => write!(f, "[System]"),
|
||||||
|
Type::Service => write!(f, "[Service]"),
|
||||||
|
Type::Hotkey => write!(f, "[Hotkey]"),
|
||||||
|
Type::Window => write!(f, "[Window]"),
|
||||||
|
Type::Tray => write!(f, "[Tray]"),
|
||||||
|
Type::Timer => write!(f, "[Timer]"),
|
||||||
|
Type::Frontend => write!(f, "[Frontend]"),
|
||||||
|
Type::Backup => write!(f, "[Backup]"),
|
||||||
|
Type::Lightweight => write!(f, "[Lightweight]"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! logging {
|
||||||
|
// 带 println 的版本(支持格式化参数)
|
||||||
|
($level:ident, $type:expr, true, $($arg:tt)*) => {
|
||||||
|
println!("{} {}", $type, format_args!($($arg)*));
|
||||||
|
log::$level!(target: "app", "{} {}", $type, format_args!($($arg)*));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 带 println 的版本(使用 false 明确不打印)
|
||||||
|
($level:ident, $type:expr, false, $($arg:tt)*) => {
|
||||||
|
log::$level!(target: "app", "{} {}", $type, format_args!($($arg)*));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 不带 print 参数的版本(默认不打印)
|
||||||
|
($level:ident, $type:expr, $($arg:tt)*) => {
|
||||||
|
log::$level!(target: "app", "{} {}", $type, format_args!($($arg)*));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! logging_error {
|
||||||
|
// 1. 处理 Result<T, E>,带打印控制
|
||||||
|
($type:expr, $print:expr, $expr:expr) => {
|
||||||
|
match $expr {
|
||||||
|
Ok(_) => {},
|
||||||
|
Err(err) => {
|
||||||
|
if $print {
|
||||||
|
println!("[{}] Error: {}", $type, err);
|
||||||
|
}
|
||||||
|
log::error!(target: "app", "[{}] {}", $type, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. 处理 Result<T, E>,默认不打印
|
||||||
|
($type:expr, $expr:expr) => {
|
||||||
|
if let Err(err) = $expr {
|
||||||
|
log::error!(target: "app", "[{}] {}", $type, err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. 处理格式化字符串,带打印控制
|
||||||
|
($type:expr, $print:expr, $fmt:literal $(, $arg:expr)*) => {
|
||||||
|
if $print {
|
||||||
|
println!("[{}] {}", $type, format_args!($fmt $(, $arg)*));
|
||||||
|
}
|
||||||
|
log::error!(target: "app", "[{}] {}", $type, format_args!($fmt $(, $arg)*));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 4. 处理格式化字符串,不带 bool 时,默认 `false`
|
||||||
|
($type:expr, $fmt:literal $(, $arg:expr)*) => {
|
||||||
|
logging_error!($type, false, $fmt $(, $arg)*);
|
||||||
|
};
|
||||||
|
}
|
@ -3,6 +3,7 @@ pub mod error;
|
|||||||
pub mod help;
|
pub mod help;
|
||||||
pub mod i18n;
|
pub mod i18n;
|
||||||
pub mod init;
|
pub mod init;
|
||||||
|
pub mod logging;
|
||||||
pub mod resolve;
|
pub mod resolve;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
pub mod tmpl;
|
pub mod tmpl;
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
use crate::AppHandleManager;
|
use crate::AppHandleManager;
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{Config, IVerge, PrfItem}, core::*, log_err, module::lightweight, utils::{error, init, server}, wrap_err
|
config::{Config, IVerge, PrfItem},
|
||||||
|
core::*,
|
||||||
|
logging, logging_error,
|
||||||
|
module::lightweight,
|
||||||
|
utils::{error, init, logging::Type, server},
|
||||||
|
wrap_err,
|
||||||
};
|
};
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
@ -10,10 +15,9 @@ use serde_yaml::Mapping;
|
|||||||
use std::net::TcpListener;
|
use std::net::TcpListener;
|
||||||
use tauri::{App, Manager};
|
use tauri::{App, Manager};
|
||||||
|
|
||||||
use url::Url;
|
use tauri::Url;
|
||||||
//#[cfg(not(target_os = "linux"))]
|
//#[cfg(not(target_os = "linux"))]
|
||||||
// use window_shadows::set_shadow;
|
// use window_shadows::set_shadow;
|
||||||
use tauri_plugin_notification::NotificationExt;
|
|
||||||
|
|
||||||
pub static VERSION: OnceCell<String> = OnceCell::new();
|
pub static VERSION: OnceCell<String> = OnceCell::new();
|
||||||
|
|
||||||
@ -47,106 +51,98 @@ pub async fn resolve_setup(app: &mut App) {
|
|||||||
handle::Handle::global().init(app.app_handle());
|
handle::Handle::global().init(app.app_handle());
|
||||||
VERSION.get_or_init(|| version.clone());
|
VERSION.get_or_init(|| version.clone());
|
||||||
|
|
||||||
log_err!(init::init_config());
|
logging_error!(Type::Config, true, init::init_config());
|
||||||
log_err!(init::init_resources());
|
logging_error!(Type::Setup, true, init::init_resources());
|
||||||
log_err!(init::init_scheme());
|
logging_error!(Type::Setup, true, init::init_scheme());
|
||||||
log_err!(init::startup_script().await);
|
logging_error!(Type::Setup, true, init::startup_script().await);
|
||||||
// 处理随机端口
|
// 处理随机端口
|
||||||
log_err!(resolve_random_port_config());
|
logging_error!(Type::System, true, resolve_random_port_config());
|
||||||
// 启动核心
|
// 启动核心
|
||||||
log::trace!(target:"app", "init config");
|
logging!(trace, Type::Config, true, "Initial config");
|
||||||
log_err!(Config::init_config().await);
|
logging_error!(Type::Config, true, Config::init_config().await);
|
||||||
|
|
||||||
if service::check_service().await.is_err() {
|
logging!(trace, Type::Core, "Starting CoreManager");
|
||||||
match service::reinstall_service().await {
|
logging_error!(Type::Core, true, CoreManager::global().init().await);
|
||||||
Ok(_) => {
|
|
||||||
log::info!(target:"app", "install service susccess.");
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(1000));
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
{
|
|
||||||
let mut service_runing = false;
|
|
||||||
for _ in 0..40 {
|
|
||||||
if service::check_service().await.is_ok() {
|
|
||||||
service_runing = true;
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
log::warn!(target: "app", "service not runing, sleep 500ms and check again.");
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !service_runing {
|
|
||||||
log::warn!(target: "app", "service not running, will fallback to user mode");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::warn!(target: "app", "failed to install service: {e:?}, will fallback to user mode");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log::trace!(target: "app", "launch core");
|
|
||||||
log_err!(CoreManager::global().init().await);
|
|
||||||
|
|
||||||
// setup a simple http server for singleton
|
// setup a simple http server for singleton
|
||||||
log::trace!(target: "app", "launch embed server");
|
log::trace!(target: "app", "launch embed server");
|
||||||
server::embed_server();
|
server::embed_server();
|
||||||
|
|
||||||
log::trace!(target: "app", "init system tray");
|
log::trace!(target: "app", "Initial system tray");
|
||||||
log_err!(tray::Tray::global().init());
|
logging_error!(Type::Tray, true, tray::Tray::global().init());
|
||||||
log_err!(tray::Tray::global().create_systray(app));
|
logging_error!(Type::Tray, true, tray::Tray::global().create_systray(app));
|
||||||
|
|
||||||
log_err!(sysopt::Sysopt::global().update_sysproxy().await);
|
logging_error!(
|
||||||
log_err!(sysopt::Sysopt::global().init_guard_sysproxy());
|
Type::System,
|
||||||
|
true,
|
||||||
|
sysopt::Sysopt::global().update_sysproxy().await
|
||||||
|
);
|
||||||
|
logging_error!(
|
||||||
|
Type::System,
|
||||||
|
true,
|
||||||
|
sysopt::Sysopt::global().init_guard_sysproxy()
|
||||||
|
);
|
||||||
|
|
||||||
// 初始化热键
|
let is_silent_start = { Config::verge().data().enable_silent_start }.unwrap_or(false);
|
||||||
log::trace!(target: "app", "init hotkeys");
|
create_window(!is_silent_start);
|
||||||
log_err!(hotkey::Hotkey::global().init());
|
|
||||||
|
|
||||||
let silent_start = { Config::verge().data().enable_silent_start };
|
logging_error!(Type::System, true, timer::Timer::global().init());
|
||||||
if !silent_start.unwrap_or(false) {
|
|
||||||
create_window();
|
|
||||||
}
|
|
||||||
|
|
||||||
log_err!(tray::Tray::global().update_part());
|
|
||||||
log_err!(timer::Timer::global().init());
|
|
||||||
|
|
||||||
let enable_auto_light_weight_mode = { Config::verge().data().enable_auto_light_weight_mode };
|
let enable_auto_light_weight_mode = { Config::verge().data().enable_auto_light_weight_mode };
|
||||||
if enable_auto_light_weight_mode.unwrap_or(false) {
|
if enable_auto_light_weight_mode.unwrap_or(false) {
|
||||||
lightweight::enable_auto_light_weight_mode();
|
lightweight::enable_auto_light_weight_mode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logging_error!(Type::Tray, true, tray::Tray::global().update_part());
|
||||||
|
|
||||||
|
// 初始化热键
|
||||||
|
logging!(trace, Type::System, true, "Initial hotkeys");
|
||||||
|
logging_error!(Type::System, true, hotkey::Hotkey::global().init());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// reset system proxy
|
/// reset system proxy (异步版)
|
||||||
pub fn resolve_reset() {
|
pub async fn resolve_reset_async() {
|
||||||
tauri::async_runtime::block_on(async move {
|
#[cfg(target_os = "macos")]
|
||||||
#[cfg(target_os = "macos")]
|
logging!(info, Type::Tray, true, "Unsubscribing from traffic updates");
|
||||||
tray::Tray::global().unsubscribe_traffic();
|
#[cfg(target_os = "macos")]
|
||||||
|
tray::Tray::global().unsubscribe_traffic();
|
||||||
|
|
||||||
log_err!(sysopt::Sysopt::global().reset_sysproxy().await);
|
logging_error!(
|
||||||
log_err!(CoreManager::global().stop_core().await);
|
Type::System,
|
||||||
#[cfg(target_os = "macos")]
|
true,
|
||||||
|
sysopt::Sysopt::global().reset_sysproxy().await
|
||||||
|
);
|
||||||
|
logging_error!(Type::Core, true, CoreManager::global().stop_core().await);
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
logging!(info, Type::System, true, "Restoring system DNS settings");
|
||||||
restore_public_dns().await;
|
restore_public_dns().await;
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// create main window
|
/// create main window
|
||||||
pub fn create_window() {
|
pub fn create_window(is_showup: bool) {
|
||||||
println!("Starting to create window");
|
logging!(info, Type::Window, true, "Creating window");
|
||||||
log::info!(target: "app", "Starting to create window");
|
|
||||||
|
|
||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
AppHandleManager::global().set_activation_policy_regular();
|
AppHandleManager::global().set_activation_policy_regular();
|
||||||
|
|
||||||
if let Some(window) = handle::Handle::global().get_window() {
|
if let Some(window) = handle::Handle::global().get_window() {
|
||||||
println!("Found existing window, trying to show it");
|
logging!(
|
||||||
log::info!(target: "app", "Found existing window, trying to show it");
|
info,
|
||||||
|
Type::Window,
|
||||||
|
true,
|
||||||
|
"Found existing window, attempting to restore visibility"
|
||||||
|
);
|
||||||
|
|
||||||
if window.is_minimized().unwrap_or(false) {
|
if window.is_minimized().unwrap_or(false) {
|
||||||
println!("Window is minimized, unminimizing");
|
logging!(
|
||||||
log::info!(target: "app", "Window is minimized, unminimizing");
|
info,
|
||||||
|
Type::Window,
|
||||||
|
true,
|
||||||
|
"Window is minimized, restoring window state"
|
||||||
|
);
|
||||||
let _ = window.unminimize();
|
let _ = window.unminimize();
|
||||||
}
|
}
|
||||||
let _ = window.show();
|
let _ = window.show();
|
||||||
@ -154,8 +150,7 @@ pub fn create_window() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("Creating new window");
|
logging!(info, Type::Window, true, "Creating new application window");
|
||||||
log::info!(target: "app", "Creating new window");
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
let window = tauri::WebviewWindowBuilder::new(
|
let window = tauri::WebviewWindowBuilder::new(
|
||||||
@ -201,17 +196,46 @@ pub fn create_window() {
|
|||||||
|
|
||||||
match window {
|
match window {
|
||||||
Ok(window) => {
|
Ok(window) => {
|
||||||
println!("Window created successfully, attempting to show");
|
logging!(info, Type::Window, true, "Window created successfully");
|
||||||
log::info!(target: "app", "Window created successfully, attempting to show");
|
if is_showup {
|
||||||
let _ = window.show();
|
println!("is showup");
|
||||||
let _ = window.set_focus();
|
let _ = window.show();
|
||||||
|
let _ = window.set_focus();
|
||||||
|
} else {
|
||||||
|
let _ = window.hide();
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
AppHandleManager::global().set_activation_policy_accessory();
|
||||||
|
}
|
||||||
|
|
||||||
// 设置窗口状态监控,实时保存窗口位置和大小
|
// 设置窗口状态监控,实时保存窗口位置和大小
|
||||||
crate::feat::setup_window_state_monitor(&app_handle);
|
// crate::feat::setup_window_state_monitor(&app_handle);
|
||||||
|
|
||||||
|
// 标记前端UI已准备就绪,向前端发送启动完成事件
|
||||||
|
let app_handle_clone = app_handle.clone();
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
use tauri::Emitter;
|
||||||
|
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Window,
|
||||||
|
true,
|
||||||
|
"标记前端UI已准备就绪,开始处理启动错误队列"
|
||||||
|
);
|
||||||
|
handle::Handle::global().mark_startup_completed();
|
||||||
|
|
||||||
|
if let Some(window) = app_handle_clone.get_webview_window("main") {
|
||||||
|
let _ = window.emit("verge://startup-completed", ());
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("Failed to create window: {:?}", e);
|
logging!(
|
||||||
log::error!(target: "app", "Failed to create window: {:?}", e);
|
error,
|
||||||
|
Type::Window,
|
||||||
|
true,
|
||||||
|
"Failed to create window: {:?}",
|
||||||
|
e
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -219,8 +243,6 @@ pub fn create_window() {
|
|||||||
pub async fn resolve_scheme(param: String) -> Result<()> {
|
pub async fn resolve_scheme(param: String) -> Result<()> {
|
||||||
log::info!(target:"app", "received deep link: {}", param);
|
log::info!(target:"app", "received deep link: {}", param);
|
||||||
|
|
||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
|
||||||
|
|
||||||
let param_str = if param.starts_with("[") && param.len() > 4 {
|
let param_str = if param.starts_with("[") && param.len() > 4 {
|
||||||
param
|
param
|
||||||
.get(2..param.len() - 2)
|
.get(2..param.len() - 2)
|
||||||
@ -243,41 +265,32 @@ pub async fn resolve_scheme(param: String) -> Result<()> {
|
|||||||
.find(|(key, _)| key == "name")
|
.find(|(key, _)| key == "name")
|
||||||
.map(|(_, value)| value.into_owned());
|
.map(|(_, value)| value.into_owned());
|
||||||
|
|
||||||
let encode_url = link_parsed
|
// 通过直接获取查询部分并解析特定参数来避免 URL 转义问题
|
||||||
.query_pairs()
|
let url_param = if let Some(query) = link_parsed.query() {
|
||||||
.find(|(key, _)| key == "url")
|
let prefix = "url=";
|
||||||
.map(|(_, value)| value.into_owned());
|
if let Some(pos) = query.find(prefix) {
|
||||||
|
let raw_url = &query[pos + prefix.len()..];
|
||||||
|
Some(percent_decode_str(raw_url).decode_utf8_lossy().to_string())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
match encode_url {
|
match url_param {
|
||||||
Some(url) => {
|
Some(url) => {
|
||||||
let url = percent_decode_str(url.as_ref())
|
log::info!(target:"app", "decoded subscription url: {}", url);
|
||||||
.decode_utf8_lossy()
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
create_window();
|
create_window(false);
|
||||||
match PrfItem::from_url(url.as_ref(), name, None, None).await {
|
match PrfItem::from_url(url.as_ref(), name, None, None).await {
|
||||||
Ok(item) => {
|
Ok(item) => {
|
||||||
let uid = item.uid.clone().unwrap();
|
let uid = item.uid.clone().unwrap();
|
||||||
let _ = wrap_err!(Config::profiles().data().append_item(item));
|
let _ = wrap_err!(Config::profiles().data().append_item(item));
|
||||||
handle::Handle::notice_message("import_sub_url::ok", uid);
|
handle::Handle::notice_message("import_sub_url::ok", uid);
|
||||||
|
|
||||||
app_handle
|
|
||||||
.notification()
|
|
||||||
.builder()
|
|
||||||
.title("Clash Verge")
|
|
||||||
.body("Import profile success")
|
|
||||||
.show()
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
handle::Handle::notice_message("import_sub_url::error", e.to_string());
|
handle::Handle::notice_message("import_sub_url::error", e.to_string());
|
||||||
app_handle
|
|
||||||
.notification()
|
|
||||||
.builder()
|
|
||||||
.title("Clash Verge")
|
|
||||||
.body(format!("Import profile failed: {e}"))
|
|
||||||
.show()
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,8 @@ extern crate warp;
|
|||||||
use super::resolve;
|
use super::resolve;
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{Config, IVerge, DEFAULT_PAC},
|
config::{Config, IVerge, DEFAULT_PAC},
|
||||||
log_err,
|
logging_error,
|
||||||
|
utils::logging::Type,
|
||||||
};
|
};
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use port_scanner::local_port_available;
|
use port_scanner::local_port_available;
|
||||||
@ -48,7 +49,7 @@ pub fn embed_server() {
|
|||||||
|
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
let visible = warp::path!("commands" / "visible").map(move || {
|
let visible = warp::path!("commands" / "visible").map(move || {
|
||||||
resolve::create_window();
|
resolve::create_window(false);
|
||||||
"ok"
|
"ok"
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -69,7 +70,11 @@ pub fn embed_server() {
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
});
|
});
|
||||||
async fn scheme_handler(query: QueryParam) -> Result<impl warp::Reply, Infallible> {
|
async fn scheme_handler(query: QueryParam) -> Result<impl warp::Reply, Infallible> {
|
||||||
log_err!(resolve::resolve_scheme(query.param).await);
|
logging_error!(
|
||||||
|
Type::Setup,
|
||||||
|
true,
|
||||||
|
resolve::resolve_scheme(query.param).await
|
||||||
|
);
|
||||||
Ok("ok")
|
Ok("ok")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,9 +6,9 @@ edition = "2024"
|
|||||||
debug = []
|
debug = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
reqwest = { version = "0.12.12", features = ["json"] }
|
reqwest = { version = "0.12.15", features = ["json"] }
|
||||||
serde = { version = "1.0.218", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
serde_json = "1.0.140"
|
serde_json = "1.0.140"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { version = "1.43.0", features = ["rt", "macros"] }
|
tokio = { version = "1.44.1", features = ["rt", "macros"] }
|
||||||
|
@ -140,4 +140,23 @@ impl MihomoManager {
|
|||||||
let response = self.send_request(Method::GET, url, None).await?;
|
let response = self.send_request(Method::GET, url, None).await?;
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_connections(&self) -> Result<serde_json::Value, String> {
|
||||||
|
let url = format!("{}/connections", self.mihomo_server);
|
||||||
|
let response = self.send_request(Method::GET, url, None).await?;
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_connection(&self, id: &str) -> Result<(), String> {
|
||||||
|
let url = format!("{}/connections/{}", self.mihomo_server, id);
|
||||||
|
let response = self.send_request(Method::DELETE, url, None).await?;
|
||||||
|
if response["code"] == 204 {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(response["message"]
|
||||||
|
.as_str()
|
||||||
|
.unwrap_or("unknown error")
|
||||||
|
.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "2.2.1",
|
"version": "2.2.3",
|
||||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"active": true,
|
"active": true,
|
||||||
@ -11,9 +11,15 @@
|
|||||||
"icons/icon.icns",
|
"icons/icon.icns",
|
||||||
"icons/icon.ico"
|
"icons/icon.ico"
|
||||||
],
|
],
|
||||||
"resources": ["resources", "resources/locales/*"],
|
"resources": [
|
||||||
|
"resources",
|
||||||
|
"resources/locales/*"
|
||||||
|
],
|
||||||
"publisher": "Clash Verge Rev",
|
"publisher": "Clash Verge Rev",
|
||||||
"externalBin": ["sidecar/verge-mihomo", "sidecar/verge-mihomo-alpha"],
|
"externalBin": [
|
||||||
|
"sidecar/verge-mihomo",
|
||||||
|
"sidecar/verge-mihomo-alpha"
|
||||||
|
],
|
||||||
"copyright": "GNU General Public License v3.0",
|
"copyright": "GNU General Public License v3.0",
|
||||||
"category": "DeveloperTool",
|
"category": "DeveloperTool",
|
||||||
"shortDescription": "Clash Verge Rev",
|
"shortDescription": "Clash Verge Rev",
|
||||||
@ -30,24 +36,42 @@
|
|||||||
"plugins": {
|
"plugins": {
|
||||||
"updater": {
|
"updater": {
|
||||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK",
|
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK",
|
||||||
|
"endpoints": [
|
||||||
|
"https://download.clashverge.dev/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json",
|
||||||
|
"https://gh-proxy.com/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json",
|
||||||
|
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update.json",
|
||||||
|
"https://download.clashverge.dev/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater-alpha/update-alpha-proxy.json",
|
||||||
|
"https://gh-proxy.com/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater-alpha/update-alpha-proxy.json",
|
||||||
|
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater-alpha/update-alpha.json"
|
||||||
|
],
|
||||||
"windows": {
|
"windows": {
|
||||||
"installMode": "basicUi"
|
"installMode": "basicUi"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"deep-link": {
|
"deep-link": {
|
||||||
"desktop": {
|
"desktop": {
|
||||||
"schemes": ["clash", "clash-verge"]
|
"schemes": [
|
||||||
|
"clash",
|
||||||
|
"clash-verge"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"security": {
|
"security": {
|
||||||
"capabilities": ["desktop-capability", "migrated"],
|
"capabilities": [
|
||||||
|
"desktop-capability",
|
||||||
|
"migrated"
|
||||||
|
],
|
||||||
"assetProtocol": {
|
"assetProtocol": {
|
||||||
"scope": ["$APPDATA/**", "$RESOURCE/../**", "**"],
|
"scope": [
|
||||||
|
"$APPDATA/**",
|
||||||
|
"$RESOURCE/../**",
|
||||||
|
"**"
|
||||||
|
],
|
||||||
"enable": true
|
"enable": true
|
||||||
},
|
},
|
||||||
"csp": null
|
"csp": null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
13
src/App.tsx
Normal file
13
src/App.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { AppDataProvider } from "./providers/app-data-provider";
|
||||||
|
import React from "react";
|
||||||
|
import Layout from "./pages/_layout";
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<AppDataProvider>
|
||||||
|
<Layout />
|
||||||
|
</AppDataProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
@ -1,13 +1,10 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Typography, Stack, Divider } from "@mui/material";
|
import { Typography, Stack, Divider } from "@mui/material";
|
||||||
import { DeveloperBoardOutlined } from "@mui/icons-material";
|
import { DeveloperBoardOutlined } from "@mui/icons-material";
|
||||||
import { useClashInfo } from "@/hooks/use-clash";
|
|
||||||
import { useClash } from "@/hooks/use-clash";
|
import { useClash } from "@/hooks/use-clash";
|
||||||
import { EnhancedCard } from "./enhanced-card";
|
import { EnhancedCard } from "./enhanced-card";
|
||||||
import useSWR from "swr";
|
import { useMemo } from "react";
|
||||||
import { getRules } from "@/services/api";
|
import { useAppData } from "@/providers/app-data-provider";
|
||||||
import { getAppUptime, getSystemProxy } from "@/services/cmds";
|
|
||||||
import { useMemo, useState, useEffect } from "react";
|
|
||||||
|
|
||||||
// 将毫秒转换为时:分:秒格式的函数
|
// 将毫秒转换为时:分:秒格式的函数
|
||||||
const formatUptime = (uptimeMs: number) => {
|
const formatUptime = (uptimeMs: number) => {
|
||||||
@ -19,37 +16,15 @@ const formatUptime = (uptimeMs: number) => {
|
|||||||
|
|
||||||
export const ClashInfoCard = () => {
|
export const ClashInfoCard = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { clashInfo } = useClashInfo();
|
|
||||||
const { version: clashVersion } = useClash();
|
const { version: clashVersion } = useClash();
|
||||||
const [sysproxy, setSysproxy] = useState<{ server: string; enable: boolean; bypass: string } | null>(null);
|
const { clashConfig, sysproxy, rules, uptime } = useAppData();
|
||||||
const [rules, setRules] = useState<any[]>([]);
|
|
||||||
|
|
||||||
// 使用SWR获取应用运行时间,降低更新频率
|
|
||||||
const { data: uptimeMs = 0 } = useSWR(
|
|
||||||
"appUptime",
|
|
||||||
getAppUptime,
|
|
||||||
{
|
|
||||||
refreshInterval: 1000,
|
|
||||||
revalidateOnFocus: false,
|
|
||||||
dedupingInterval: 1000,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// 在组件加载时获取系统代理信息和规则数据
|
|
||||||
useEffect(() => {
|
|
||||||
// 获取系统代理信息
|
|
||||||
getSystemProxy().then(setSysproxy);
|
|
||||||
|
|
||||||
// 获取规则数据
|
|
||||||
getRules().then(setRules).catch(() => setRules([]));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 使用useMemo缓存格式化后的uptime,避免频繁计算
|
// 使用useMemo缓存格式化后的uptime,避免频繁计算
|
||||||
const uptime = useMemo(() => formatUptime(uptimeMs), [uptimeMs]);
|
const formattedUptime = useMemo(() => formatUptime(uptime), [uptime]);
|
||||||
|
|
||||||
// 使用备忘录组件内容,减少重新渲染
|
// 使用备忘录组件内容,减少重新渲染
|
||||||
const cardContent = useMemo(() => {
|
const cardContent = useMemo(() => {
|
||||||
if (!clashInfo) return null;
|
if (!clashConfig) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing={1.5}>
|
<Stack spacing={1.5}>
|
||||||
@ -76,7 +51,7 @@ export const ClashInfoCard = () => {
|
|||||||
{t("Mixed Port")}
|
{t("Mixed Port")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" fontWeight="medium">
|
<Typography variant="body2" fontWeight="medium">
|
||||||
{clashInfo.mixed_port || "-"}
|
{clashConfig["mixed-port"] || "-"}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Divider />
|
<Divider />
|
||||||
@ -85,7 +60,7 @@ export const ClashInfoCard = () => {
|
|||||||
{t("Uptime")}
|
{t("Uptime")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" fontWeight="medium">
|
<Typography variant="body2" fontWeight="medium">
|
||||||
{uptime}
|
{formattedUptime}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Divider />
|
<Divider />
|
||||||
@ -99,7 +74,7 @@ export const ClashInfoCard = () => {
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}, [clashInfo, clashVersion, t, uptime, rules.length, sysproxy]);
|
}, [clashConfig, clashVersion, t, formattedUptime, rules.length, sysproxy]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EnhancedCard
|
<EnhancedCard
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Box, Typography, Paper, Stack, Fade } from "@mui/material";
|
import { Box, Typography, Paper, Stack, Fade } from "@mui/material";
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from "ahooks";
|
||||||
import useSWR from "swr";
|
import { closeAllConnections } from "@/services/api";
|
||||||
import { closeAllConnections, getClashConfig } from "@/services/api";
|
|
||||||
import { patchClashMode } from "@/services/cmds";
|
import { patchClashMode } from "@/services/cmds";
|
||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
import {
|
import {
|
||||||
@ -11,22 +10,12 @@ import {
|
|||||||
DirectionsRounded,
|
DirectionsRounded,
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
import { useAppData } from "@/providers/app-data-provider";
|
||||||
|
|
||||||
export const ClashModeCard = () => {
|
export const ClashModeCard = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { verge } = useVerge();
|
const { verge } = useVerge();
|
||||||
|
const { clashConfig, refreshProxy } = useAppData();
|
||||||
// 获取当前Clash配置
|
|
||||||
const { data: clashConfig, mutate: mutateClash } = useSWR(
|
|
||||||
"getClashConfig",
|
|
||||||
getClashConfig,
|
|
||||||
{
|
|
||||||
revalidateOnFocus: false,
|
|
||||||
revalidateIfStale: true,
|
|
||||||
dedupingInterval: 1000,
|
|
||||||
errorRetryInterval: 5000
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 支持的模式列表
|
// 支持的模式列表
|
||||||
const modeList = useMemo(() => ["rule", "global", "direct"] as const, []);
|
const modeList = useMemo(() => ["rule", "global", "direct"] as const, []);
|
||||||
@ -50,7 +39,8 @@ export const ClashModeCard = () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await patchClashMode(mode);
|
await patchClashMode(mode);
|
||||||
mutateClash();
|
// 使用共享的刷新方法
|
||||||
|
refreshProxy();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to change mode:", error);
|
console.error("Failed to change mode:", error);
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ import {
|
|||||||
SelectChangeEvent,
|
SelectChangeEvent,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { useEffect, useState, useMemo, useCallback, useRef } from "react";
|
import { useEffect, useState, useMemo, useCallback } from "react";
|
||||||
import {
|
import {
|
||||||
SignalWifi4Bar as SignalStrong,
|
SignalWifi4Bar as SignalStrong,
|
||||||
SignalWifi3Bar as SignalGood,
|
SignalWifi3Bar as SignalGood,
|
||||||
@ -24,16 +24,11 @@ import {
|
|||||||
ChevronRight,
|
ChevronRight,
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useCurrentProxy } from "@/hooks/use-current-proxy";
|
|
||||||
import { EnhancedCard } from "@/components/home/enhanced-card";
|
import { EnhancedCard } from "@/components/home/enhanced-card";
|
||||||
import {
|
import { updateProxy, deleteConnection } from "@/services/api";
|
||||||
getProxies,
|
|
||||||
updateProxy,
|
|
||||||
getConnections,
|
|
||||||
deleteConnection,
|
|
||||||
} from "@/services/api";
|
|
||||||
import delayManager from "@/services/delay";
|
import delayManager from "@/services/delay";
|
||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
|
import { useAppData } from "@/providers/app-data-provider";
|
||||||
|
|
||||||
// 本地存储的键名
|
// 本地存储的键名
|
||||||
const STORAGE_KEY_GROUP = "clash-verge-selected-proxy-group";
|
const STORAGE_KEY_GROUP = "clash-verge-selected-proxy-group";
|
||||||
@ -92,21 +87,16 @@ function debounce(fn: Function, ms = 100) {
|
|||||||
|
|
||||||
export const CurrentProxyCard = () => {
|
export const CurrentProxyCard = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { currentProxy, primaryGroupName, mode, refreshProxy } =
|
|
||||||
useCurrentProxy();
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { verge } = useVerge();
|
const { verge } = useVerge();
|
||||||
|
const { proxies, connections, clashConfig, refreshProxy } = useAppData();
|
||||||
|
|
||||||
// 判断模式
|
// 判断模式
|
||||||
|
const mode = clashConfig?.mode?.toLowerCase() || "rule";
|
||||||
const isGlobalMode = mode === "global";
|
const isGlobalMode = mode === "global";
|
||||||
const isDirectMode = mode === "direct";
|
const isDirectMode = mode === "direct";
|
||||||
|
|
||||||
// 使用 useRef 存储最后一次刷新时间和是否正在刷新
|
|
||||||
const lastRefreshRef = useRef<number>(0);
|
|
||||||
const isRefreshingRef = useRef<boolean>(false);
|
|
||||||
const pendingRefreshRef = useRef<boolean>(false);
|
|
||||||
|
|
||||||
// 定义状态类型
|
// 定义状态类型
|
||||||
type ProxyState = {
|
type ProxyState = {
|
||||||
proxyData: {
|
proxyData: {
|
||||||
@ -139,6 +129,32 @@ export const CurrentProxyCard = () => {
|
|||||||
|
|
||||||
// 初始化选择的组
|
// 初始化选择的组
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!proxies) return;
|
||||||
|
|
||||||
|
// 提取primaryGroupName
|
||||||
|
const getPrimaryGroupName = () => {
|
||||||
|
if (!proxies?.groups?.length) return "";
|
||||||
|
|
||||||
|
// 查找主要的代理组(优先级:包含关键词 > 第一个非GLOBAL组)
|
||||||
|
const primaryKeywords = [
|
||||||
|
"auto",
|
||||||
|
"select",
|
||||||
|
"proxy",
|
||||||
|
"节点选择",
|
||||||
|
"自动选择",
|
||||||
|
];
|
||||||
|
const primaryGroup =
|
||||||
|
proxies.groups.find((group: { name: string }) =>
|
||||||
|
primaryKeywords.some((keyword) =>
|
||||||
|
group.name.toLowerCase().includes(keyword.toLowerCase()),
|
||||||
|
),
|
||||||
|
) || proxies.groups.filter((g: { name: string }) => g.name !== "GLOBAL")[0];
|
||||||
|
|
||||||
|
return primaryGroup?.name || "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const primaryGroupName = getPrimaryGroupName();
|
||||||
|
|
||||||
// 根据模式确定初始组
|
// 根据模式确定初始组
|
||||||
if (isGlobalMode) {
|
if (isGlobalMode) {
|
||||||
setState((prev) => ({
|
setState((prev) => ({
|
||||||
@ -166,153 +182,85 @@ export const CurrentProxyCard = () => {
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}, [isGlobalMode, isDirectMode, primaryGroupName]);
|
}, [isGlobalMode, isDirectMode, proxies]);
|
||||||
|
|
||||||
// 带锁的代理数据获取函数,防止并发请求
|
// 监听代理数据变化,更新状态
|
||||||
const fetchProxyData = useCallback(
|
useEffect(() => {
|
||||||
async (force = false) => {
|
if (!proxies) return;
|
||||||
// 防止重复请求
|
|
||||||
if (isRefreshingRef.current) {
|
// 使用函数式更新确保状态更新的原子性
|
||||||
pendingRefreshRef.current = true;
|
setState((prev) => {
|
||||||
return;
|
// 过滤和格式化组
|
||||||
}
|
const filteredGroups = proxies.groups
|
||||||
|
.filter((g: { name: string }) => g.name !== "DIRECT" && g.name !== "REJECT")
|
||||||
|
.map((g: { name: string; now: string; all: Array<{ name: string }> }) => ({
|
||||||
|
name: g.name,
|
||||||
|
now: g.now || "",
|
||||||
|
all: g.all.map((p: { name: string }) => p.name),
|
||||||
|
}));
|
||||||
|
|
||||||
|
let newProxy = "";
|
||||||
|
let newDisplayProxy = null;
|
||||||
|
let newGroup = prev.selection.group;
|
||||||
|
|
||||||
// 检查刷新间隔
|
// 根据模式确定新代理
|
||||||
const now = Date.now();
|
if (isDirectMode) {
|
||||||
if (!force && now - lastRefreshRef.current < 1000) {
|
newGroup = "DIRECT";
|
||||||
return;
|
newProxy = "DIRECT";
|
||||||
}
|
newDisplayProxy = proxies.records?.DIRECT || null;
|
||||||
|
} else if (isGlobalMode && proxies.global) {
|
||||||
|
newGroup = "GLOBAL";
|
||||||
|
newProxy = proxies.global.now || "";
|
||||||
|
newDisplayProxy = proxies.records?.[newProxy] || null;
|
||||||
|
} else {
|
||||||
|
// 普通模式 - 检查当前选择的组是否存在
|
||||||
|
const currentGroup = filteredGroups.find(
|
||||||
|
(g: { name: string }) => g.name === prev.selection.group,
|
||||||
|
);
|
||||||
|
|
||||||
isRefreshingRef.current = true;
|
// 如果当前组不存在或为空,自动选择第一个组
|
||||||
lastRefreshRef.current = now;
|
if (!currentGroup && filteredGroups.length > 0) {
|
||||||
|
newGroup = filteredGroups[0].name;
|
||||||
|
const firstGroup = filteredGroups[0];
|
||||||
|
newProxy = firstGroup.now;
|
||||||
|
newDisplayProxy = proxies.records?.[newProxy] || null;
|
||||||
|
|
||||||
try {
|
// 保存到本地存储
|
||||||
const data = await getProxies();
|
if (!isGlobalMode && !isDirectMode) {
|
||||||
|
localStorage.setItem(STORAGE_KEY_GROUP, newGroup);
|
||||||
// 过滤和格式化组
|
if (newProxy) {
|
||||||
const filteredGroups = data.groups
|
localStorage.setItem(STORAGE_KEY_PROXY, newProxy);
|
||||||
.filter((g) => g.name !== "DIRECT" && g.name !== "REJECT")
|
|
||||||
.map((g) => ({
|
|
||||||
name: g.name,
|
|
||||||
now: g.now || "",
|
|
||||||
all: g.all.map((p) => p.name),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 使用函数式更新确保状态更新的原子性
|
|
||||||
setState((prev) => {
|
|
||||||
let newProxy = "";
|
|
||||||
let newDisplayProxy = null;
|
|
||||||
let newGroup = prev.selection.group;
|
|
||||||
|
|
||||||
// 根据模式确定新代理
|
|
||||||
if (isDirectMode) {
|
|
||||||
newGroup = "DIRECT";
|
|
||||||
newProxy = "DIRECT";
|
|
||||||
newDisplayProxy = data.records?.DIRECT || null;
|
|
||||||
} else if (isGlobalMode && data.global) {
|
|
||||||
newGroup = "GLOBAL";
|
|
||||||
newProxy = data.global.now || "";
|
|
||||||
newDisplayProxy = data.records?.[newProxy] || null;
|
|
||||||
} else {
|
|
||||||
// 普通模式 - 检查当前选择的组是否存在
|
|
||||||
const currentGroup = filteredGroups.find(
|
|
||||||
(g) => g.name === prev.selection.group,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 如果当前组不存在或为空,自动选择第一个组
|
|
||||||
if (!currentGroup && filteredGroups.length > 0) {
|
|
||||||
newGroup = filteredGroups[0].name;
|
|
||||||
const firstGroup = filteredGroups[0];
|
|
||||||
newProxy = firstGroup.now;
|
|
||||||
newDisplayProxy = data.records?.[newProxy] || null;
|
|
||||||
|
|
||||||
// 保存到本地存储
|
|
||||||
if (!isGlobalMode && !isDirectMode) {
|
|
||||||
localStorage.setItem(STORAGE_KEY_GROUP, newGroup);
|
|
||||||
if (newProxy) {
|
|
||||||
localStorage.setItem(STORAGE_KEY_PROXY, newProxy);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (currentGroup) {
|
|
||||||
// 使用当前组的代理
|
|
||||||
newProxy = currentGroup.now;
|
|
||||||
newDisplayProxy = data.records?.[newProxy] || null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (currentGroup) {
|
||||||
// 返回新状态
|
// 使用当前组的代理
|
||||||
return {
|
newProxy = currentGroup.now;
|
||||||
proxyData: {
|
newDisplayProxy = proxies.records?.[newProxy] || null;
|
||||||
groups: filteredGroups,
|
|
||||||
records: data.records || {},
|
|
||||||
globalProxy: data.global?.now || "",
|
|
||||||
directProxy: data.records?.DIRECT || null,
|
|
||||||
},
|
|
||||||
selection: {
|
|
||||||
group: newGroup,
|
|
||||||
proxy: newProxy,
|
|
||||||
},
|
|
||||||
displayProxy: newDisplayProxy,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("获取代理信息失败", error);
|
|
||||||
} finally {
|
|
||||||
isRefreshingRef.current = false;
|
|
||||||
|
|
||||||
// 处理待处理的刷新请求
|
|
||||||
if (pendingRefreshRef.current) {
|
|
||||||
pendingRefreshRef.current = false;
|
|
||||||
setTimeout(() => fetchProxyData(), 100);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
[isGlobalMode, isDirectMode],
|
|
||||||
);
|
|
||||||
|
|
||||||
// 响应 currentProxy 变化
|
// 返回新状态
|
||||||
useEffect(() => {
|
return {
|
||||||
if (
|
proxyData: {
|
||||||
currentProxy &&
|
groups: filteredGroups,
|
||||||
(!state.displayProxy || currentProxy.name !== state.displayProxy.name)
|
records: proxies.records || {},
|
||||||
) {
|
globalProxy: proxies.global?.now || "",
|
||||||
fetchProxyData(true);
|
directProxy: proxies.records?.DIRECT || null,
|
||||||
}
|
},
|
||||||
}, [currentProxy, fetchProxyData, state.displayProxy]);
|
selection: {
|
||||||
|
group: newGroup,
|
||||||
|
proxy: newProxy,
|
||||||
|
},
|
||||||
|
displayProxy: newDisplayProxy,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [proxies, isGlobalMode, isDirectMode]);
|
||||||
|
|
||||||
// 监听模式变化,mode变化时刷新
|
// 使用防抖包装状态更新,避免快速连续更新,增加防抖时间
|
||||||
useEffect(() => {
|
|
||||||
fetchProxyData(true);
|
|
||||||
}, [mode, fetchProxyData]);
|
|
||||||
|
|
||||||
|
|
||||||
// 计算要显示的代理选项 - 使用 useMemo 优化
|
|
||||||
const proxyOptions = useMemo(() => {
|
|
||||||
if (isDirectMode) {
|
|
||||||
return [{ name: "DIRECT" }];
|
|
||||||
}
|
|
||||||
if (isGlobalMode && state.proxyData.records) {
|
|
||||||
// 全局模式下的选项
|
|
||||||
return Object.keys(state.proxyData.records)
|
|
||||||
.filter((name) => name !== "DIRECT" && name !== "REJECT")
|
|
||||||
.map((name) => ({ name }));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 普通模式
|
|
||||||
const group = state.proxyData.groups.find(
|
|
||||||
(g) => g.name === state.selection.group,
|
|
||||||
);
|
|
||||||
if (group) {
|
|
||||||
return group.all.map((name) => ({ name }));
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}, [isDirectMode, isGlobalMode, state.proxyData, state.selection.group]);
|
|
||||||
|
|
||||||
// 使用防抖包装状态更新,避免快速连续更新
|
|
||||||
const debouncedSetState = useCallback(
|
const debouncedSetState = useCallback(
|
||||||
debounce((updateFn: (prev: ProxyState) => ProxyState) => {
|
debounce((updateFn: (prev: ProxyState) => ProxyState) => {
|
||||||
setState(updateFn);
|
setState(updateFn);
|
||||||
}, 50),
|
}, 300),
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -328,7 +276,7 @@ export const CurrentProxyCard = () => {
|
|||||||
|
|
||||||
// 获取该组当前选中的代理
|
// 获取该组当前选中的代理
|
||||||
setState((prev) => {
|
setState((prev) => {
|
||||||
const group = prev.proxyData.groups.find((g) => g.name === newGroup);
|
const group = prev.proxyData.groups.find((g: { name: string }) => g.name === newGroup);
|
||||||
if (group) {
|
if (group) {
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
@ -381,20 +329,17 @@ export const CurrentProxyCard = () => {
|
|||||||
|
|
||||||
// 自动关闭连接设置
|
// 自动关闭连接设置
|
||||||
if (verge?.auto_close_connection && previousProxy) {
|
if (verge?.auto_close_connection && previousProxy) {
|
||||||
getConnections().then(({ connections }) => {
|
connections.data.forEach((conn: any) => {
|
||||||
connections.forEach((conn) => {
|
if (conn.chains.includes(previousProxy)) {
|
||||||
if (conn.chains.includes(previousProxy)) {
|
deleteConnection(conn.id);
|
||||||
deleteConnection(conn.id);
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 刷新代理信息,使用较短的延迟
|
// 延长刷新延迟时间
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
refreshProxy();
|
refreshProxy();
|
||||||
fetchProxyData(true);
|
}, 500);
|
||||||
}, 200);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("更新代理失败", error);
|
console.error("更新代理失败", error);
|
||||||
}
|
}
|
||||||
@ -406,8 +351,8 @@ export const CurrentProxyCard = () => {
|
|||||||
state.selection,
|
state.selection,
|
||||||
verge?.auto_close_connection,
|
verge?.auto_close_connection,
|
||||||
refreshProxy,
|
refreshProxy,
|
||||||
fetchProxyData,
|
|
||||||
debouncedSetState,
|
debouncedSetState,
|
||||||
|
connections.data,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -417,11 +362,14 @@ export const CurrentProxyCard = () => {
|
|||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
|
|
||||||
// 获取要显示的代理节点
|
// 获取要显示的代理节点
|
||||||
const proxyToDisplay = state.displayProxy || currentProxy;
|
const currentProxy = useMemo(() => {
|
||||||
|
// 从state中获取当前代理信息
|
||||||
|
return state.displayProxy;
|
||||||
|
}, [state.displayProxy]);
|
||||||
|
|
||||||
// 获取当前节点的延迟
|
// 获取当前节点的延迟
|
||||||
const currentDelay = proxyToDisplay
|
const currentDelay = currentProxy
|
||||||
? delayManager.getDelayFix(proxyToDisplay, state.selection.group)
|
? delayManager.getDelayFix(currentProxy, state.selection.group)
|
||||||
: -1;
|
: -1;
|
||||||
|
|
||||||
// 获取信号图标
|
// 获取信号图标
|
||||||
@ -451,23 +399,45 @@ export const CurrentProxyCard = () => {
|
|||||||
[state.proxyData.records, state.selection.group],
|
[state.proxyData.records, state.selection.group],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 计算要显示的代理选项 - 使用 useMemo 优化
|
||||||
|
const proxyOptions = useMemo(() => {
|
||||||
|
if (isDirectMode) {
|
||||||
|
return [{ name: "DIRECT" }];
|
||||||
|
}
|
||||||
|
if (isGlobalMode && state.proxyData.records) {
|
||||||
|
// 全局模式下的选项
|
||||||
|
return Object.keys(state.proxyData.records)
|
||||||
|
.filter((name) => name !== "DIRECT" && name !== "REJECT")
|
||||||
|
.map((name) => ({ name }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 普通模式
|
||||||
|
const group = state.proxyData.groups.find(
|
||||||
|
(g: { name: string }) => g.name === state.selection.group,
|
||||||
|
);
|
||||||
|
if (group) {
|
||||||
|
return group.all.map((name) => ({ name }));
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}, [isDirectMode, isGlobalMode, state.proxyData, state.selection.group]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EnhancedCard
|
<EnhancedCard
|
||||||
title={t("Current Node")}
|
title={t("Current Node")}
|
||||||
icon={
|
icon={
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={
|
title={
|
||||||
proxyToDisplay
|
currentProxy
|
||||||
? `${signalInfo.text}: ${delayManager.formatDelay(currentDelay)}`
|
? `${signalInfo.text}: ${delayManager.formatDelay(currentDelay)}`
|
||||||
: "无代理节点"
|
: "无代理节点"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Box sx={{ color: signalInfo.color }}>
|
<Box sx={{ color: signalInfo.color }}>
|
||||||
{proxyToDisplay ? signalInfo.icon : <SignalNone color="disabled" />}
|
{currentProxy ? signalInfo.icon : <SignalNone color="disabled" />}
|
||||||
</Box>
|
</Box>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
iconColor={proxyToDisplay ? "primary" : undefined}
|
iconColor={currentProxy ? "primary" : undefined}
|
||||||
action={
|
action={
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@ -480,7 +450,7 @@ export const CurrentProxyCard = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{proxyToDisplay ? (
|
{currentProxy ? (
|
||||||
<Box>
|
<Box>
|
||||||
{/* 代理节点信息显示 */}
|
{/* 代理节点信息显示 */}
|
||||||
<Box
|
<Box
|
||||||
@ -497,7 +467,7 @@ export const CurrentProxyCard = () => {
|
|||||||
>
|
>
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="body1" fontWeight="medium">
|
<Typography variant="body1" fontWeight="medium">
|
||||||
{proxyToDisplay.name}
|
{currentProxy.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
@ -508,7 +478,7 @@ export const CurrentProxyCard = () => {
|
|||||||
color="text.secondary"
|
color="text.secondary"
|
||||||
sx={{ mr: 1 }}
|
sx={{ mr: 1 }}
|
||||||
>
|
>
|
||||||
{proxyToDisplay.type}
|
{currentProxy.type}
|
||||||
</Typography>
|
</Typography>
|
||||||
{isGlobalMode && (
|
{isGlobalMode && (
|
||||||
<Chip
|
<Chip
|
||||||
@ -527,26 +497,26 @@ export const CurrentProxyCard = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/* 节点特性 */}
|
{/* 节点特性 */}
|
||||||
{proxyToDisplay.udp && (
|
{currentProxy.udp && (
|
||||||
<Chip size="small" label="UDP" variant="outlined" />
|
<Chip size="small" label="UDP" variant="outlined" />
|
||||||
)}
|
)}
|
||||||
{proxyToDisplay.tfo && (
|
{currentProxy.tfo && (
|
||||||
<Chip size="small" label="TFO" variant="outlined" />
|
<Chip size="small" label="TFO" variant="outlined" />
|
||||||
)}
|
)}
|
||||||
{proxyToDisplay.xudp && (
|
{currentProxy.xudp && (
|
||||||
<Chip size="small" label="XUDP" variant="outlined" />
|
<Chip size="small" label="XUDP" variant="outlined" />
|
||||||
)}
|
)}
|
||||||
{proxyToDisplay.mptcp && (
|
{currentProxy.mptcp && (
|
||||||
<Chip size="small" label="MPTCP" variant="outlined" />
|
<Chip size="small" label="MPTCP" variant="outlined" />
|
||||||
)}
|
)}
|
||||||
{proxyToDisplay.smux && (
|
{currentProxy.smux && (
|
||||||
<Chip size="small" label="SMUX" variant="outlined" />
|
<Chip size="small" label="SMUX" variant="outlined" />
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* 显示延迟 */}
|
{/* 显示延迟 */}
|
||||||
{proxyToDisplay && !isDirectMode && (
|
{currentProxy && !isDirectMode && (
|
||||||
<Chip
|
<Chip
|
||||||
size="small"
|
size="small"
|
||||||
label={delayManager.formatDelay(currentDelay)}
|
label={delayManager.formatDelay(currentDelay)}
|
||||||
@ -597,14 +567,14 @@ export const CurrentProxyCard = () => {
|
|||||||
>
|
>
|
||||||
{isDirectMode
|
{isDirectMode
|
||||||
? null
|
? null
|
||||||
: proxyOptions.map((proxy) => {
|
: proxyOptions.map((proxy, index) => {
|
||||||
const delayValue = delayManager.getDelayFix(
|
const delayValue = delayManager.getDelayFix(
|
||||||
state.proxyData.records[proxy.name],
|
state.proxyData.records[proxy.name],
|
||||||
state.selection.group,
|
state.selection.group,
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
key={proxy.name}
|
key={`${proxy.name}-${index}`}
|
||||||
value={proxy.name}
|
value={proxy.name}
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
@ -39,69 +39,35 @@ export interface EnhancedTrafficGraphRef {
|
|||||||
// 时间范围类型
|
// 时间范围类型
|
||||||
type TimeRange = 1 | 5 | 10; // 分钟
|
type TimeRange = 1 | 5 | 10; // 分钟
|
||||||
|
|
||||||
// 创建一个明确的类型
|
// 数据点类型
|
||||||
type DataPoint = ITrafficItem & { name: string; timestamp: number };
|
type DataPoint = ITrafficItem & { name: string; timestamp: number };
|
||||||
|
|
||||||
// 控制帧率的工具函数
|
|
||||||
const FPS_LIMIT = 1; // 限制为1fps,因为数据每秒才更新一次
|
|
||||||
const FRAME_MIN_TIME = 1000 / FPS_LIMIT; // 每帧最小时间间隔,即1000ms
|
|
||||||
|
|
||||||
// 全局存储流量数据历史记录
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
trafficHistoryData?: DataPoint[];
|
|
||||||
trafficHistoryStyle?: "line" | "area";
|
|
||||||
trafficHistoryTimeRange?: TimeRange;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化全局存储
|
|
||||||
if (typeof window !== "undefined" && !window.trafficHistoryData) {
|
|
||||||
window.trafficHistoryData = [];
|
|
||||||
window.trafficHistoryStyle = "area";
|
|
||||||
window.trafficHistoryTimeRange = 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 增强型流量图表组件
|
* 增强型流量图表组件
|
||||||
* 基于 Recharts 实现,支持线图和面积图两种模式
|
|
||||||
*/
|
*/
|
||||||
export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||||
(props, ref) => {
|
(props, ref) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
// 从全局变量恢复状态
|
// 基础状态
|
||||||
const [timeRange, setTimeRange] = useState<TimeRange>(
|
const [timeRange, setTimeRange] = useState<TimeRange>(10);
|
||||||
window.trafficHistoryTimeRange || 10
|
const [chartStyle, setChartStyle] = useState<"line" | "area">("area");
|
||||||
);
|
|
||||||
const [chartStyle, setChartStyle] = useState<"line" | "area">(
|
|
||||||
window.trafficHistoryStyle || "area"
|
|
||||||
);
|
|
||||||
|
|
||||||
// 使用useRef存储数据,避免不必要的重渲染
|
|
||||||
const dataBufferRef = useRef<DataPoint[]>([]);
|
|
||||||
// 只为渲染目的的状态
|
|
||||||
const [displayData, setDisplayData] = useState<DataPoint[]>([]);
|
const [displayData, setDisplayData] = useState<DataPoint[]>([]);
|
||||||
|
|
||||||
// 帧率控制
|
// 数据缓冲区
|
||||||
const lastUpdateTimeRef = useRef<number>(0);
|
const dataBufferRef = useRef<DataPoint[]>([]);
|
||||||
const pendingUpdateRef = useRef<boolean>(false);
|
|
||||||
const rafIdRef = useRef<number | null>(null);
|
|
||||||
|
|
||||||
// 根据时间范围计算保留的数据点数量
|
// 根据时间范围计算保留的数据点数量
|
||||||
const getMaxPointsByTimeRange = useCallback(
|
const getMaxPointsByTimeRange = useCallback(
|
||||||
(minutes: TimeRange): number => {
|
(minutes: TimeRange): number => minutes * 60,
|
||||||
// 使用更低的采样率来减少点的数量,每2秒一个点而不是每秒一个点
|
[]
|
||||||
return minutes * 30; // 每分钟30个点(每2秒1个点)
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// 最大数据点数量 - 基于选择的时间范围
|
// 最大数据点数量
|
||||||
const MAX_BUFFER_SIZE = useMemo(
|
const MAX_BUFFER_SIZE = useMemo(
|
||||||
() => getMaxPointsByTimeRange(10),
|
() => getMaxPointsByTimeRange(10),
|
||||||
[getMaxPointsByTimeRange],
|
[getMaxPointsByTimeRange]
|
||||||
);
|
);
|
||||||
|
|
||||||
// 颜色配置
|
// 颜色配置
|
||||||
@ -113,149 +79,50 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
|||||||
tooltip: theme.palette.background.paper,
|
tooltip: theme.palette.background.paper,
|
||||||
text: theme.palette.text.primary,
|
text: theme.palette.text.primary,
|
||||||
}),
|
}),
|
||||||
[theme],
|
[theme]
|
||||||
);
|
);
|
||||||
|
|
||||||
// 切换时间范围
|
// 切换时间范围
|
||||||
const handleTimeRangeClick = useCallback(() => {
|
const handleTimeRangeClick = useCallback(() => {
|
||||||
setTimeRange((prevRange) => {
|
setTimeRange((prevRange) => {
|
||||||
// 在1、5、10分钟之间循环切换
|
// 在1、5、10分钟之间循环切换
|
||||||
const newRange = prevRange === 1 ? 5 : prevRange === 5 ? 10 : 1;
|
return prevRange === 1 ? 5 : prevRange === 5 ? 10 : 1;
|
||||||
window.trafficHistoryTimeRange = newRange; // 保存到全局
|
|
||||||
return newRange;
|
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 初始化数据缓冲区
|
// 初始化数据缓冲区
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let initialBuffer: DataPoint[] = [];
|
// 创建初始空数据
|
||||||
|
const now = Date.now();
|
||||||
// 如果全局有保存的数据,优先使用
|
const tenMinutesAgo = now - 10 * 60 * 1000;
|
||||||
if (window.trafficHistoryData && window.trafficHistoryData.length > 0) {
|
|
||||||
initialBuffer = [...window.trafficHistoryData];
|
const initialBuffer = Array.from(
|
||||||
|
{ length: MAX_BUFFER_SIZE },
|
||||||
// 确保数据长度符合要求
|
(_, index) => {
|
||||||
if (initialBuffer.length > MAX_BUFFER_SIZE) {
|
const pointTime =
|
||||||
initialBuffer = initialBuffer.slice(-MAX_BUFFER_SIZE);
|
tenMinutesAgo + index * ((10 * 60 * 1000) / MAX_BUFFER_SIZE);
|
||||||
} else if (initialBuffer.length < MAX_BUFFER_SIZE) {
|
const date = new Date(pointTime);
|
||||||
// 如果历史数据不足,则在前面补充空数据
|
|
||||||
const now = Date.now();
|
return {
|
||||||
const oldestTimestamp = initialBuffer.length > 0
|
up: 0,
|
||||||
? initialBuffer[0].timestamp
|
down: 0,
|
||||||
: now - 10 * 60 * 1000;
|
timestamp: pointTime,
|
||||||
|
name: date.toLocaleTimeString("en-US", {
|
||||||
const additionalPoints = MAX_BUFFER_SIZE - initialBuffer.length;
|
hour12: false,
|
||||||
const timeInterval = initialBuffer.length > 0
|
hour: "2-digit",
|
||||||
? (initialBuffer[0].timestamp - (now - 10 * 60 * 1000)) / additionalPoints
|
minute: "2-digit",
|
||||||
: (10 * 60 * 1000) / MAX_BUFFER_SIZE;
|
second: "2-digit",
|
||||||
|
}),
|
||||||
const emptyPrefix: DataPoint[] = Array.from(
|
};
|
||||||
{ length: additionalPoints },
|
|
||||||
(_, index) => {
|
|
||||||
const pointTime = oldestTimestamp - (additionalPoints - index) * timeInterval;
|
|
||||||
const date = new Date(pointTime);
|
|
||||||
|
|
||||||
return {
|
|
||||||
up: 0,
|
|
||||||
down: 0,
|
|
||||||
timestamp: pointTime,
|
|
||||||
name: date.toLocaleTimeString("en-US", {
|
|
||||||
hour12: false,
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
second: "2-digit",
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
initialBuffer = [...emptyPrefix, ...initialBuffer];
|
|
||||||
}
|
}
|
||||||
} else {
|
);
|
||||||
// 没有历史数据时,创建空的初始缓冲区
|
|
||||||
const now = Date.now();
|
|
||||||
const tenMinutesAgo = now - 10 * 60 * 1000;
|
|
||||||
|
|
||||||
initialBuffer = Array.from(
|
|
||||||
{ length: MAX_BUFFER_SIZE },
|
|
||||||
(_, index) => {
|
|
||||||
const pointTime =
|
|
||||||
tenMinutesAgo + index * ((10 * 60 * 1000) / MAX_BUFFER_SIZE);
|
|
||||||
const date = new Date(pointTime);
|
|
||||||
|
|
||||||
return {
|
|
||||||
up: 0,
|
|
||||||
down: 0,
|
|
||||||
timestamp: pointTime,
|
|
||||||
name: date.toLocaleTimeString("en-US", {
|
|
||||||
hour12: false,
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
second: "2-digit",
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
dataBufferRef.current = initialBuffer;
|
dataBufferRef.current = initialBuffer;
|
||||||
window.trafficHistoryData = initialBuffer; // 保存到全局
|
|
||||||
|
|
||||||
// 更新显示数据
|
// 更新显示数据
|
||||||
const pointsToShow = getMaxPointsByTimeRange(timeRange);
|
const pointsToShow = getMaxPointsByTimeRange(timeRange);
|
||||||
setDisplayData(initialBuffer.slice(-pointsToShow));
|
setDisplayData(initialBuffer.slice(-pointsToShow));
|
||||||
|
}, [MAX_BUFFER_SIZE, getMaxPointsByTimeRange]);
|
||||||
// 清理函数,取消任何未完成的动画帧
|
|
||||||
return () => {
|
|
||||||
if (rafIdRef.current !== null) {
|
|
||||||
cancelAnimationFrame(rafIdRef.current);
|
|
||||||
rafIdRef.current = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [MAX_BUFFER_SIZE, getMaxPointsByTimeRange, timeRange]);
|
|
||||||
|
|
||||||
// 处理数据更新并控制帧率的函数
|
|
||||||
const updateDisplayData = useCallback(() => {
|
|
||||||
if (pendingUpdateRef.current) {
|
|
||||||
pendingUpdateRef.current = false;
|
|
||||||
|
|
||||||
// 根据当前时间范围计算需要显示的点数
|
|
||||||
const pointsToShow = getMaxPointsByTimeRange(timeRange);
|
|
||||||
// 从缓冲区中获取最新的数据点
|
|
||||||
const newDisplayData = dataBufferRef.current.slice(-pointsToShow);
|
|
||||||
setDisplayData(newDisplayData);
|
|
||||||
}
|
|
||||||
|
|
||||||
rafIdRef.current = null;
|
|
||||||
}, [timeRange, getMaxPointsByTimeRange]);
|
|
||||||
|
|
||||||
// 节流更新函数
|
|
||||||
const throttledUpdateData = useCallback(() => {
|
|
||||||
pendingUpdateRef.current = true;
|
|
||||||
|
|
||||||
const now = performance.now();
|
|
||||||
const timeSinceLastUpdate = now - lastUpdateTimeRef.current;
|
|
||||||
|
|
||||||
if (rafIdRef.current === null) {
|
|
||||||
if (timeSinceLastUpdate >= FRAME_MIN_TIME) {
|
|
||||||
// 如果距离上次更新已经超过最小帧时间,立即更新
|
|
||||||
lastUpdateTimeRef.current = now;
|
|
||||||
rafIdRef.current = requestAnimationFrame(updateDisplayData);
|
|
||||||
} else {
|
|
||||||
// 否则,在适当的时间进行更新
|
|
||||||
const timeToWait = FRAME_MIN_TIME - timeSinceLastUpdate;
|
|
||||||
setTimeout(() => {
|
|
||||||
lastUpdateTimeRef.current = performance.now();
|
|
||||||
rafIdRef.current = requestAnimationFrame(updateDisplayData);
|
|
||||||
}, timeToWait);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [updateDisplayData]);
|
|
||||||
|
|
||||||
// 监听时间范围变化,更新显示数据
|
|
||||||
useEffect(() => {
|
|
||||||
throttledUpdateData();
|
|
||||||
}, [timeRange, throttledUpdateData]);
|
|
||||||
|
|
||||||
// 添加数据点方法
|
// 添加数据点方法
|
||||||
const appendData = useCallback((data: ITrafficItem) => {
|
const appendData = useCallback((data: ITrafficItem) => {
|
||||||
@ -281,24 +148,24 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
|||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 更新ref,但保持原数组大小
|
// 更新缓冲区,保持原数组大小
|
||||||
const newBuffer = [...dataBufferRef.current.slice(1), newPoint];
|
const newBuffer = [...dataBufferRef.current.slice(1), newPoint];
|
||||||
dataBufferRef.current = newBuffer;
|
dataBufferRef.current = newBuffer;
|
||||||
|
|
||||||
// 保存到全局变量
|
// 更新显示数据
|
||||||
window.trafficHistoryData = newBuffer;
|
const pointsToShow = getMaxPointsByTimeRange(timeRange);
|
||||||
|
setDisplayData(newBuffer.slice(-pointsToShow));
|
||||||
// 使用节流更新显示数据
|
}, [timeRange, getMaxPointsByTimeRange]);
|
||||||
throttledUpdateData();
|
|
||||||
}, [throttledUpdateData]);
|
// 监听时间范围变化,更新显示数据
|
||||||
|
useEffect(() => {
|
||||||
|
const pointsToShow = getMaxPointsByTimeRange(timeRange);
|
||||||
|
setDisplayData(dataBufferRef.current.slice(-pointsToShow));
|
||||||
|
}, [timeRange, getMaxPointsByTimeRange]);
|
||||||
|
|
||||||
// 切换图表样式
|
// 切换图表样式
|
||||||
const toggleStyle = useCallback(() => {
|
const toggleStyle = useCallback(() => {
|
||||||
setChartStyle((prev) => {
|
setChartStyle((prev) => prev === "line" ? "area" : "line");
|
||||||
const newStyle = prev === "line" ? "area" : "line";
|
|
||||||
window.trafficHistoryStyle = newStyle; // 保存到全局
|
|
||||||
return newStyle;
|
|
||||||
});
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 暴露方法给父组件
|
// 暴露方法给父组件
|
||||||
@ -308,13 +175,12 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
|||||||
appendData,
|
appendData,
|
||||||
toggleStyle,
|
toggleStyle,
|
||||||
}),
|
}),
|
||||||
[appendData, toggleStyle],
|
[appendData, toggleStyle]
|
||||||
);
|
);
|
||||||
|
|
||||||
// 格式化工具提示内容
|
// 格式化工具提示内容
|
||||||
const formatTooltip = useCallback((value: number, name: string, props: any) => {
|
const formatTooltip = useCallback((value: number, name: string, props: any) => {
|
||||||
const [num, unit] = parseTraffic(value);
|
const [num, unit] = parseTraffic(value);
|
||||||
// 使用props.dataKey判断是上传还是下载
|
|
||||||
return [`${num} ${unit}/s`, props?.dataKey === "up" ? t("Upload") : t("Download")];
|
return [`${num} ${unit}/s`, props?.dataKey === "up" ? t("Upload") : t("Download")];
|
||||||
}, [t]);
|
}, [t]);
|
||||||
|
|
||||||
@ -327,7 +193,6 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
|||||||
// 格式化X轴标签
|
// 格式化X轴标签
|
||||||
const formatXLabel = useCallback((value: string) => {
|
const formatXLabel = useCallback((value: string) => {
|
||||||
if (!value) return "";
|
if (!value) return "";
|
||||||
// 只显示小时和分钟
|
|
||||||
const parts = value.split(":");
|
const parts = value.split(":");
|
||||||
return `${parts[0]}:${parts[1]}`;
|
return `${parts[0]}:${parts[1]}`;
|
||||||
}, []);
|
}, []);
|
||||||
@ -340,7 +205,7 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
|||||||
// 共享图表配置
|
// 共享图表配置
|
||||||
const chartConfig = useMemo(() => ({
|
const chartConfig = useMemo(() => ({
|
||||||
data: displayData,
|
data: displayData,
|
||||||
margin: { top: 10, right: 20, left: 0, bottom: 0 },
|
margin: { top: 20, right: 10, left: 0, bottom: -10 },
|
||||||
}), [displayData]);
|
}), [displayData]);
|
||||||
|
|
||||||
// 共享的线条/区域配置
|
// 共享的线条/区域配置
|
||||||
@ -352,7 +217,7 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
|||||||
isAnimationActive: false, // 禁用动画以减少CPU使用
|
isAnimationActive: false, // 禁用动画以减少CPU使用
|
||||||
}), []);
|
}), []);
|
||||||
|
|
||||||
// 曲线类型 - 使用线性曲线避免错位
|
// 曲线类型
|
||||||
const curveType = "monotone";
|
const curveType = "monotone";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -363,186 +228,138 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
|||||||
position: "relative",
|
position: "relative",
|
||||||
bgcolor: "action.hover",
|
bgcolor: "action.hover",
|
||||||
borderRadius: 1,
|
borderRadius: 1,
|
||||||
padding: 1,
|
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
}}
|
}}
|
||||||
onClick={toggleStyle}
|
onClick={toggleStyle}
|
||||||
>
|
>
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
{chartStyle === "line" ? (
|
{/* 根据chartStyle动态选择图表类型 */}
|
||||||
<LineChart {...chartConfig}>
|
{(() => {
|
||||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke={colors.grid} opacity={0.3} />
|
// 创建共享的图表组件
|
||||||
<XAxis
|
const commonChartComponents = (
|
||||||
dataKey="name"
|
<>
|
||||||
tick={{ fontSize: 10, fill: colors.text }}
|
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke={colors.grid} opacity={0.3} />
|
||||||
tickLine={{ stroke: colors.grid }}
|
<XAxis
|
||||||
axisLine={{ stroke: colors.grid }}
|
dataKey="name"
|
||||||
interval="preserveStart"
|
tick={{ fontSize: 10, fill: colors.text }}
|
||||||
tickFormatter={formatXLabel}
|
tickLine={{ stroke: colors.grid }}
|
||||||
minTickGap={30}
|
axisLine={{ stroke: colors.grid }}
|
||||||
/>
|
interval="preserveStart"
|
||||||
<YAxis
|
tickFormatter={formatXLabel}
|
||||||
tickFormatter={formatYAxis}
|
minTickGap={30}
|
||||||
tick={{ fontSize: 10, fill: colors.text }}
|
/>
|
||||||
tickLine={{ stroke: colors.grid }}
|
<YAxis
|
||||||
axisLine={{ stroke: colors.grid }}
|
tickFormatter={formatYAxis}
|
||||||
width={40}
|
tick={{ fontSize: 10, fill: colors.text }}
|
||||||
domain={[0, "auto"]}
|
tickLine={{ stroke: colors.grid }}
|
||||||
/>
|
axisLine={{ stroke: colors.grid }}
|
||||||
<Tooltip
|
width={44}
|
||||||
formatter={formatTooltip}
|
domain={[0, "auto"]}
|
||||||
labelFormatter={(label) => `${t("Time")}: ${label}`}
|
padding={{ top: 8, bottom: 0 }}
|
||||||
contentStyle={{
|
/>
|
||||||
backgroundColor: colors.tooltip,
|
<Tooltip
|
||||||
borderColor: colors.grid,
|
formatter={formatTooltip}
|
||||||
borderRadius: 4,
|
labelFormatter={(label) => `${t("Time")}: ${label}`}
|
||||||
}}
|
contentStyle={{
|
||||||
itemStyle={{ color: colors.text }}
|
backgroundColor: colors.tooltip,
|
||||||
isAnimationActive={false}
|
borderColor: colors.grid,
|
||||||
/>
|
borderRadius: 4,
|
||||||
<Line
|
}}
|
||||||
type={curveType}
|
itemStyle={{ color: colors.text }}
|
||||||
{...commonLineProps}
|
isAnimationActive={false}
|
||||||
dataKey="up"
|
/>
|
||||||
name={t("Upload")}
|
|
||||||
stroke={colors.up}
|
{/* 可点击的时间范围标签 */}
|
||||||
/>
|
<text
|
||||||
<Line
|
x="1%"
|
||||||
type={curveType}
|
y="11%"
|
||||||
{...commonLineProps}
|
textAnchor="start"
|
||||||
dataKey="down"
|
fill={theme.palette.text.secondary}
|
||||||
name={t("Download")}
|
fontSize={11}
|
||||||
stroke={colors.down}
|
fontWeight="bold"
|
||||||
/>
|
onClick={handleTimeRangeClick}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
{/* 可点击的时间范围标签 */}
|
>
|
||||||
<text
|
{getTimeRangeText()}
|
||||||
x="1%"
|
</text>
|
||||||
y="6%"
|
|
||||||
textAnchor="start"
|
{/* 上传标签 - 右上角 */}
|
||||||
fill={theme.palette.text.secondary}
|
<text
|
||||||
fontSize={11}
|
x="99%"
|
||||||
fontWeight="bold"
|
y="11%"
|
||||||
onClick={handleTimeRangeClick}
|
textAnchor="end"
|
||||||
style={{ cursor: "pointer" }}
|
fill={colors.up}
|
||||||
>
|
fontSize={12}
|
||||||
{getTimeRangeText()}
|
fontWeight="bold"
|
||||||
</text>
|
onClick={toggleStyle}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
{/* 上传标签 - 右上角 */}
|
>
|
||||||
<text
|
{t("Upload")}
|
||||||
x="98%"
|
</text>
|
||||||
y="7%"
|
|
||||||
textAnchor="end"
|
|
||||||
fill={colors.up}
|
|
||||||
fontSize={12}
|
|
||||||
fontWeight="bold"
|
|
||||||
>
|
|
||||||
{t("Upload")}
|
|
||||||
</text>
|
|
||||||
|
|
||||||
{/* 下载标签 - 右上角下方 */}
|
{/* 下载标签 - 右上角下方 */}
|
||||||
<text
|
<text
|
||||||
x="98%"
|
x="99%"
|
||||||
y="16%"
|
y="19%"
|
||||||
textAnchor="end"
|
textAnchor="end"
|
||||||
fill={colors.down}
|
fill={colors.down}
|
||||||
fontSize={12}
|
fontSize={12}
|
||||||
fontWeight="bold"
|
fontWeight="bold"
|
||||||
>
|
onClick={toggleStyle}
|
||||||
{t("Download")}
|
style={{ cursor: "pointer" }}
|
||||||
</text>
|
>
|
||||||
</LineChart>
|
{t("Download")}
|
||||||
) : (
|
</text>
|
||||||
<AreaChart {...chartConfig}>
|
</>
|
||||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke={colors.grid} opacity={0.3} />
|
);
|
||||||
<XAxis
|
|
||||||
dataKey="name"
|
|
||||||
tick={{ fontSize: 10, fill: colors.text }}
|
|
||||||
tickLine={{ stroke: colors.grid }}
|
|
||||||
axisLine={{ stroke: colors.grid }}
|
|
||||||
interval="preserveStart"
|
|
||||||
tickFormatter={formatXLabel}
|
|
||||||
minTickGap={30}
|
|
||||||
/>
|
|
||||||
<YAxis
|
|
||||||
tickFormatter={formatYAxis}
|
|
||||||
tick={{ fontSize: 10, fill: colors.text }}
|
|
||||||
tickLine={{ stroke: colors.grid }}
|
|
||||||
axisLine={{ stroke: colors.grid }}
|
|
||||||
width={40}
|
|
||||||
domain={[0, "auto"]}
|
|
||||||
padding={{ top: 10, bottom: 0 }}
|
|
||||||
/>
|
|
||||||
<Tooltip
|
|
||||||
formatter={formatTooltip}
|
|
||||||
labelFormatter={(label) => `${t("Time")}: ${label}`}
|
|
||||||
contentStyle={{
|
|
||||||
backgroundColor: colors.tooltip,
|
|
||||||
borderColor: colors.grid,
|
|
||||||
borderRadius: 4,
|
|
||||||
}}
|
|
||||||
itemStyle={{ color: colors.text }}
|
|
||||||
isAnimationActive={false}
|
|
||||||
/>
|
|
||||||
<Area
|
|
||||||
type={curveType}
|
|
||||||
{...commonLineProps}
|
|
||||||
dataKey="up"
|
|
||||||
name={t("Upload")}
|
|
||||||
stroke={colors.up}
|
|
||||||
fill={colors.up}
|
|
||||||
fillOpacity={0.2}
|
|
||||||
/>
|
|
||||||
<Area
|
|
||||||
type={curveType}
|
|
||||||
{...commonLineProps}
|
|
||||||
dataKey="down"
|
|
||||||
name={t("Download")}
|
|
||||||
stroke={colors.down}
|
|
||||||
fill={colors.down}
|
|
||||||
fillOpacity={0.3}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 可点击的时间范围标签 */}
|
|
||||||
<text
|
|
||||||
x="1%"
|
|
||||||
y="6%"
|
|
||||||
textAnchor="start"
|
|
||||||
fill={theme.palette.text.secondary}
|
|
||||||
fontSize={11}
|
|
||||||
fontWeight="bold"
|
|
||||||
onClick={handleTimeRangeClick}
|
|
||||||
style={{ cursor: "pointer" }}
|
|
||||||
>
|
|
||||||
{getTimeRangeText()}
|
|
||||||
</text>
|
|
||||||
|
|
||||||
{/* 上传标签 - 右上角 */}
|
|
||||||
<text
|
|
||||||
x="98%"
|
|
||||||
y="7%"
|
|
||||||
textAnchor="end"
|
|
||||||
fill={colors.up}
|
|
||||||
fontSize={12}
|
|
||||||
fontWeight="bold"
|
|
||||||
>
|
|
||||||
{t("Upload")}
|
|
||||||
</text>
|
|
||||||
|
|
||||||
{/* 下载标签 - 右上角下方 */}
|
// 根据chartStyle返回相应的图表类型
|
||||||
<text
|
if (chartStyle === "line") {
|
||||||
x="98%"
|
return (
|
||||||
y="16%"
|
<LineChart {...chartConfig}>
|
||||||
textAnchor="end"
|
{commonChartComponents}
|
||||||
fill={colors.down}
|
<Line
|
||||||
fontSize={12}
|
type={curveType}
|
||||||
fontWeight="bold"
|
{...commonLineProps}
|
||||||
>
|
dataKey="up"
|
||||||
{t("Download")}
|
name={t("Upload")}
|
||||||
</text>
|
stroke={colors.up}
|
||||||
</AreaChart>
|
/>
|
||||||
)}
|
<Line
|
||||||
|
type={curveType}
|
||||||
|
{...commonLineProps}
|
||||||
|
dataKey="down"
|
||||||
|
name={t("Download")}
|
||||||
|
stroke={colors.down}
|
||||||
|
/>
|
||||||
|
</LineChart>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<AreaChart {...chartConfig}>
|
||||||
|
{commonChartComponents}
|
||||||
|
<Area
|
||||||
|
type={curveType}
|
||||||
|
{...commonLineProps}
|
||||||
|
dataKey="up"
|
||||||
|
name={t("Upload")}
|
||||||
|
stroke={colors.up}
|
||||||
|
fill={colors.up}
|
||||||
|
fillOpacity={0.2}
|
||||||
|
/>
|
||||||
|
<Area
|
||||||
|
type={curveType}
|
||||||
|
{...commonLineProps}
|
||||||
|
dataKey="down"
|
||||||
|
name={t("Download")}
|
||||||
|
stroke={colors.down}
|
||||||
|
fill={colors.down}
|
||||||
|
fillOpacity={0.3}
|
||||||
|
/>
|
||||||
|
</AreaChart>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})()}
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
@ -28,6 +28,7 @@ import { createAuthSockette } from "@/utils/websocket";
|
|||||||
import parseTraffic from "@/utils/parse-traffic";
|
import parseTraffic from "@/utils/parse-traffic";
|
||||||
import { getConnections, isDebugEnabled, gc } from "@/services/api";
|
import { getConnections, isDebugEnabled, gc } from "@/services/api";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
|
import { useAppData } from "@/providers/app-data-provider";
|
||||||
|
|
||||||
interface MemoryUsage {
|
interface MemoryUsage {
|
||||||
inuse: number;
|
inuse: number;
|
||||||
@ -157,11 +158,13 @@ export const EnhancedTrafficStats = () => {
|
|||||||
const pageVisible = useVisibility();
|
const pageVisible = useVisibility();
|
||||||
const [isDebug, setIsDebug] = useState(false);
|
const [isDebug, setIsDebug] = useState(false);
|
||||||
|
|
||||||
|
// 使用AppDataProvider
|
||||||
|
const { connections, uptime } = useAppData();
|
||||||
|
|
||||||
// 使用单一状态对象减少状态更新次数
|
// 使用单一状态对象减少状态更新次数
|
||||||
const [stats, setStats] = useState({
|
const [stats, setStats] = useState({
|
||||||
traffic: { up: 0, down: 0 },
|
traffic: { up: 0, down: 0 },
|
||||||
memory: { inuse: 0, oslimit: undefined as number | undefined },
|
memory: { inuse: 0, oslimit: undefined as number | undefined },
|
||||||
connections: { uploadTotal: 0, downloadTotal: 0, activeConnections: 0 },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 创建一个标记来追踪最后更新时间,用于节流
|
// 创建一个标记来追踪最后更新时间,用于节流
|
||||||
@ -176,36 +179,6 @@ export const EnhancedTrafficStats = () => {
|
|||||||
memory: null as ReturnType<typeof createAuthSockette> | null,
|
memory: null as ReturnType<typeof createAuthSockette> | null,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 获取连接数据
|
|
||||||
const fetchConnections = useCallback(async () => {
|
|
||||||
if (!pageVisible) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const connections = await getConnections();
|
|
||||||
if (connections) {
|
|
||||||
setStats(prev => ({
|
|
||||||
...prev,
|
|
||||||
connections: {
|
|
||||||
uploadTotal: connections.uploadTotal || 0,
|
|
||||||
downloadTotal: connections.downloadTotal || 0,
|
|
||||||
activeConnections: connections.connections ? connections.connections.length : 0,
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to fetch connections:", err);
|
|
||||||
}
|
|
||||||
}, [pageVisible]);
|
|
||||||
|
|
||||||
// 定期更新连接数据
|
|
||||||
useEffect(() => {
|
|
||||||
if (!pageVisible) return;
|
|
||||||
|
|
||||||
fetchConnections();
|
|
||||||
const intervalId = setInterval(fetchConnections, CONNECTIONS_UPDATE_INTERVAL);
|
|
||||||
return () => clearInterval(intervalId);
|
|
||||||
}, [pageVisible, fetchConnections]);
|
|
||||||
|
|
||||||
// 检查是否支持调试
|
// 检查是否支持调试
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
isDebugEnabled().then((flag) => setIsDebug(flag));
|
isDebugEnabled().then((flag) => setIsDebug(flag));
|
||||||
@ -328,14 +301,14 @@ export const EnhancedTrafficStats = () => {
|
|||||||
const [up, upUnit] = parseTraffic(stats.traffic.up);
|
const [up, upUnit] = parseTraffic(stats.traffic.up);
|
||||||
const [down, downUnit] = parseTraffic(stats.traffic.down);
|
const [down, downUnit] = parseTraffic(stats.traffic.down);
|
||||||
const [inuse, inuseUnit] = parseTraffic(stats.memory.inuse);
|
const [inuse, inuseUnit] = parseTraffic(stats.memory.inuse);
|
||||||
const [uploadTotal, uploadTotalUnit] = parseTraffic(stats.connections.uploadTotal);
|
const [uploadTotal, uploadTotalUnit] = parseTraffic(connections.uploadTotal);
|
||||||
const [downloadTotal, downloadTotalUnit] = parseTraffic(stats.connections.downloadTotal);
|
const [downloadTotal, downloadTotalUnit] = parseTraffic(connections.downloadTotal);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
up, upUnit, down, downUnit, inuse, inuseUnit,
|
up, upUnit, down, downUnit, inuse, inuseUnit,
|
||||||
uploadTotal, uploadTotalUnit, downloadTotal, downloadTotalUnit
|
uploadTotal, uploadTotalUnit, downloadTotal, downloadTotalUnit
|
||||||
};
|
};
|
||||||
}, [stats]);
|
}, [stats, connections.uploadTotal, connections.downloadTotal]);
|
||||||
|
|
||||||
// 渲染流量图表 - 使用useMemo缓存渲染结果
|
// 渲染流量图表 - 使用useMemo缓存渲染结果
|
||||||
const trafficGraphComponent = useMemo(() => {
|
const trafficGraphComponent = useMemo(() => {
|
||||||
@ -398,7 +371,7 @@ export const EnhancedTrafficStats = () => {
|
|||||||
{
|
{
|
||||||
icon: <LinkRounded fontSize="small" />,
|
icon: <LinkRounded fontSize="small" />,
|
||||||
title: t("Active Connections"),
|
title: t("Active Connections"),
|
||||||
value: stats.connections.activeConnections,
|
value: connections.count,
|
||||||
unit: "",
|
unit: "",
|
||||||
color: "success" as const,
|
color: "success" as const,
|
||||||
},
|
},
|
||||||
@ -424,14 +397,16 @@ export const EnhancedTrafficStats = () => {
|
|||||||
color: "error" as const,
|
color: "error" as const,
|
||||||
onClick: isDebug ? handleGarbageCollection : undefined,
|
onClick: isDebug ? handleGarbageCollection : undefined,
|
||||||
},
|
},
|
||||||
], [t, parsedData, stats.connections.activeConnections, isDebug, handleGarbageCollection]);
|
], [t, parsedData, connections.count, isDebug, handleGarbageCollection]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid container spacing={1} columns={{ xs: 8, sm: 8, md: 12 }}>
|
<Grid container spacing={1} columns={{ xs: 8, sm: 8, md: 12 }}>
|
||||||
<Grid size={12}>
|
{trafficGraph && (
|
||||||
{/* 流量图表区域 */}
|
<Grid size={12}>
|
||||||
{trafficGraphComponent}
|
{/* 流量图表区域 */}
|
||||||
</Grid>
|
{trafficGraphComponent}
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
{/* 统计卡片区域 */}
|
{/* 统计卡片区域 */}
|
||||||
{statCards.map((card, index) => (
|
{statCards.map((card, index) => (
|
||||||
<Grid key={index} size={4}>
|
<Grid key={index} size={4}>
|
||||||
|
@ -27,6 +27,7 @@ import { openWebUrl, updateProfile } from "@/services/cmds";
|
|||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from "ahooks";
|
||||||
import { Notice } from "@/components/base";
|
import { Notice } from "@/components/base";
|
||||||
import { EnhancedCard } from "./enhanced-card";
|
import { EnhancedCard } from "./enhanced-card";
|
||||||
|
import { useAppData } from "@/providers/app-data-provider";
|
||||||
|
|
||||||
// 定义旋转动画
|
// 定义旋转动画
|
||||||
const round = keyframes`
|
const round = keyframes`
|
||||||
@ -270,6 +271,7 @@ const EmptyProfile = ({ onClick }: { onClick: () => void }) => {
|
|||||||
export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardProps) => {
|
export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { refreshAll } = useAppData();
|
||||||
|
|
||||||
// 更新当前订阅
|
// 更新当前订阅
|
||||||
const [updating, setUpdating] = useState(false);
|
const [updating, setUpdating] = useState(false);
|
||||||
@ -282,6 +284,9 @@ export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardPr
|
|||||||
await updateProfile(current.uid);
|
await updateProfile(current.uid);
|
||||||
Notice.success(t("Update subscription successfully"));
|
Notice.success(t("Update subscription successfully"));
|
||||||
onProfileUpdated?.();
|
onProfileUpdated?.();
|
||||||
|
|
||||||
|
// 刷新首页数据
|
||||||
|
refreshAll();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
Notice.error(err?.message || err.toString());
|
Notice.error(err?.message || err.toString());
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -18,13 +18,8 @@ import {
|
|||||||
HelpOutlineRounded,
|
HelpOutlineRounded,
|
||||||
SvgIconComponent,
|
SvgIconComponent,
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import useSWR from "swr";
|
|
||||||
import {
|
|
||||||
getSystemProxy,
|
|
||||||
getAutotemProxy,
|
|
||||||
getRunningMode,
|
|
||||||
} from "@/services/cmds";
|
|
||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
|
import { useSystemState } from "@/hooks/use-system-state";
|
||||||
|
|
||||||
const LOCAL_STORAGE_TAB_KEY = "clash-verge-proxy-active-tab";
|
const LOCAL_STORAGE_TAB_KEY = "clash-verge-proxy-active-tab";
|
||||||
|
|
||||||
@ -37,71 +32,64 @@ interface TabButtonProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 抽取Tab组件以减少重复代码
|
// 抽取Tab组件以减少重复代码
|
||||||
const TabButton: FC<TabButtonProps> = memo(({
|
const TabButton: FC<TabButtonProps> = memo(
|
||||||
isActive,
|
({ isActive, onClick, icon: Icon, label, hasIndicator = false }) => (
|
||||||
onClick,
|
<Paper
|
||||||
icon: Icon,
|
elevation={isActive ? 2 : 0}
|
||||||
label,
|
onClick={onClick}
|
||||||
hasIndicator = false
|
sx={{
|
||||||
}) => (
|
cursor: "pointer",
|
||||||
<Paper
|
px: 2,
|
||||||
elevation={isActive ? 2 : 0}
|
py: 1,
|
||||||
onClick={onClick}
|
display: "flex",
|
||||||
sx={{
|
alignItems: "center",
|
||||||
cursor: "pointer",
|
justifyContent: "center",
|
||||||
px: 2,
|
gap: 1,
|
||||||
py: 1,
|
bgcolor: isActive ? "primary.main" : "background.paper",
|
||||||
display: "flex",
|
color: isActive ? "primary.contrastText" : "text.primary",
|
||||||
alignItems: "center",
|
borderRadius: 1.5,
|
||||||
justifyContent: "center",
|
flex: 1,
|
||||||
gap: 1,
|
maxWidth: 160,
|
||||||
bgcolor: isActive ? "primary.main" : "background.paper",
|
transition: "all 0.2s ease-in-out",
|
||||||
color: isActive ? "primary.contrastText" : "text.primary",
|
position: "relative",
|
||||||
borderRadius: 1.5,
|
"&:hover": {
|
||||||
flex: 1,
|
transform: "translateY(-1px)",
|
||||||
maxWidth: 160,
|
boxShadow: 1,
|
||||||
transition: "all 0.2s ease-in-out",
|
},
|
||||||
position: "relative",
|
"&:after": isActive
|
||||||
"&:hover": {
|
? {
|
||||||
transform: "translateY(-1px)",
|
content: '""',
|
||||||
boxShadow: 1,
|
position: "absolute",
|
||||||
},
|
bottom: -9,
|
||||||
"&:after": isActive
|
left: "50%",
|
||||||
? {
|
width: 2,
|
||||||
content: '""',
|
height: 9,
|
||||||
position: "absolute",
|
bgcolor: "primary.main",
|
||||||
bottom: -9,
|
transform: "translateX(-50%)",
|
||||||
left: "50%",
|
}
|
||||||
width: 2,
|
: {},
|
||||||
height: 9,
|
}}
|
||||||
bgcolor: "primary.main",
|
|
||||||
transform: "translateX(-50%)",
|
|
||||||
}
|
|
||||||
: {},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon fontSize="small" />
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
sx={{ fontWeight: isActive ? 600 : 400 }}
|
|
||||||
>
|
>
|
||||||
{label}
|
<Icon fontSize="small" />
|
||||||
</Typography>
|
<Typography variant="body2" sx={{ fontWeight: isActive ? 600 : 400 }}>
|
||||||
{hasIndicator && (
|
{label}
|
||||||
<Box
|
</Typography>
|
||||||
sx={{
|
{hasIndicator && (
|
||||||
width: 8,
|
<Box
|
||||||
height: 8,
|
sx={{
|
||||||
borderRadius: "50%",
|
width: 8,
|
||||||
bgcolor: isActive ? "#fff" : "success.main",
|
height: 8,
|
||||||
position: "absolute",
|
borderRadius: "50%",
|
||||||
top: 8,
|
bgcolor: isActive ? "#fff" : "success.main",
|
||||||
right: 8,
|
position: "absolute",
|
||||||
}}
|
top: 8,
|
||||||
/>
|
right: 8,
|
||||||
)}
|
}}
|
||||||
</Paper>
|
/>
|
||||||
));
|
)}
|
||||||
|
</Paper>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
interface TabDescriptionProps {
|
interface TabDescriptionProps {
|
||||||
description: string;
|
description: string;
|
||||||
@ -109,56 +97,57 @@ interface TabDescriptionProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 抽取描述文本组件
|
// 抽取描述文本组件
|
||||||
const TabDescription: FC<TabDescriptionProps> = memo(({ description, tooltipTitle }) => (
|
const TabDescription: FC<TabDescriptionProps> = memo(
|
||||||
<Fade in={true} timeout={200}>
|
({ description, tooltipTitle }) => (
|
||||||
<Typography
|
<Fade in={true} timeout={200}>
|
||||||
variant="caption"
|
<Typography
|
||||||
component="div"
|
variant="caption"
|
||||||
sx={{
|
component="div"
|
||||||
width: "95%",
|
sx={{
|
||||||
textAlign: "center",
|
width: "95%",
|
||||||
color: "text.secondary",
|
textAlign: "center",
|
||||||
p: 0.8,
|
color: "text.secondary",
|
||||||
borderRadius: 1,
|
p: 0.8,
|
||||||
borderColor: "primary.main",
|
borderRadius: 1,
|
||||||
borderWidth: 1,
|
borderColor: "primary.main",
|
||||||
borderStyle: "solid",
|
borderWidth: 1,
|
||||||
backgroundColor: "background.paper",
|
borderStyle: "solid",
|
||||||
display: "flex",
|
backgroundColor: "background.paper",
|
||||||
alignItems: "center",
|
display: "flex",
|
||||||
justifyContent: "center",
|
alignItems: "center",
|
||||||
gap: 0.5,
|
justifyContent: "center",
|
||||||
wordBreak: "break-word",
|
gap: 0.5,
|
||||||
hyphens: "auto",
|
wordBreak: "break-word",
|
||||||
}}
|
hyphens: "auto",
|
||||||
>
|
}}
|
||||||
{description}
|
>
|
||||||
<Tooltip title={tooltipTitle}>
|
{description}
|
||||||
<HelpOutlineRounded
|
<Tooltip title={tooltipTitle}>
|
||||||
sx={{ fontSize: 14, opacity: 0.7, flexShrink: 0 }}
|
<HelpOutlineRounded
|
||||||
/>
|
sx={{ fontSize: 14, opacity: 0.7, flexShrink: 0 }}
|
||||||
</Tooltip>
|
/>
|
||||||
</Typography>
|
</Tooltip>
|
||||||
</Fade>
|
</Typography>
|
||||||
));
|
</Fade>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
export const ProxyTunCard: FC = () => {
|
export const ProxyTunCard: FC = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const [activeTab, setActiveTab] = useState<string>(() =>
|
const [activeTab, setActiveTab] = useState<string>(
|
||||||
localStorage.getItem(LOCAL_STORAGE_TAB_KEY) || "system"
|
() => localStorage.getItem(LOCAL_STORAGE_TAB_KEY) || "system",
|
||||||
);
|
);
|
||||||
|
|
||||||
// 获取代理状态信息
|
// 获取代理状态信息
|
||||||
const { data: sysproxy } = useSWR("getSystemProxy", getSystemProxy);
|
|
||||||
const { data: runningMode } = useSWR("getRunningMode", getRunningMode);
|
|
||||||
const { verge } = useVerge();
|
const { verge } = useVerge();
|
||||||
|
const { isSidecarMode, isAdminMode } = useSystemState();
|
||||||
|
|
||||||
// 从verge配置中获取开关状态
|
// 从verge配置中获取开关状态
|
||||||
const { enable_system_proxy, enable_tun_mode } = verge ?? {};
|
const { enable_system_proxy, enable_tun_mode } = verge ?? {};
|
||||||
|
|
||||||
// 是否以sidecar模式运行
|
// 判断Tun模式是否可用 - 当处于服务模式或管理员模式时可用
|
||||||
const isSidecarMode = runningMode === "sidecar";
|
const isTunAvailable = !isSidecarMode || isAdminMode;
|
||||||
|
|
||||||
// 处理错误
|
// 处理错误
|
||||||
const handleError = (err: Error) => {
|
const handleError = (err: Error) => {
|
||||||
@ -178,19 +167,19 @@ export const ProxyTunCard: FC = () => {
|
|||||||
text: enable_system_proxy
|
text: enable_system_proxy
|
||||||
? t("System Proxy Enabled")
|
? t("System Proxy Enabled")
|
||||||
: t("System Proxy Disabled"),
|
: t("System Proxy Disabled"),
|
||||||
tooltip: t("System Proxy Info")
|
tooltip: t("System Proxy Info"),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
text: isSidecarMode
|
text: !isTunAvailable
|
||||||
? t("TUN Mode Service Required")
|
? t("TUN Mode Service Required")
|
||||||
: enable_tun_mode
|
: enable_tun_mode
|
||||||
? t("TUN Mode Enabled")
|
? t("TUN Mode Enabled")
|
||||||
: t("TUN Mode Disabled"),
|
: t("TUN Mode Disabled"),
|
||||||
tooltip: t("TUN Mode Intercept Info")
|
tooltip: t("TUN Mode Intercept Info"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [activeTab, enable_system_proxy, enable_tun_mode, isSidecarMode, t]);
|
}, [activeTab, enable_system_proxy, enable_tun_mode, isTunAvailable, t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}>
|
<Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}>
|
||||||
@ -217,7 +206,7 @@ export const ProxyTunCard: FC = () => {
|
|||||||
onClick={() => handleTabChange("tun")}
|
onClick={() => handleTabChange("tun")}
|
||||||
icon={TroubleshootRounded}
|
icon={TroubleshootRounded}
|
||||||
label={t("Tun Mode")}
|
label={t("Tun Mode")}
|
||||||
hasIndicator={enable_tun_mode && !isSidecarMode}
|
hasIndicator={enable_tun_mode && isTunAvailable}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
|
@ -1,21 +1,30 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Typography, Stack, Divider, Chip, IconButton } from "@mui/material";
|
import { Typography, Stack, Divider, Chip, IconButton, Tooltip } from "@mui/material";
|
||||||
import { InfoOutlined, SettingsOutlined } from "@mui/icons-material";
|
import {
|
||||||
|
InfoOutlined,
|
||||||
|
SettingsOutlined,
|
||||||
|
WarningOutlined,
|
||||||
|
AdminPanelSettingsOutlined,
|
||||||
|
DnsOutlined,
|
||||||
|
ExtensionOutlined
|
||||||
|
} from "@mui/icons-material";
|
||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
import { EnhancedCard } from "./enhanced-card";
|
import { EnhancedCard } from "./enhanced-card";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { getRunningMode, getSystemInfo, installService } from "@/services/cmds";
|
import { getSystemInfo, installService } from "@/services/cmds";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { version as appVersion } from "@root/package.json";
|
import { version as appVersion } from "@root/package.json";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { check as checkUpdate } from "@tauri-apps/plugin-updater";
|
import { check as checkUpdate } from "@tauri-apps/plugin-updater";
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from "ahooks";
|
||||||
import { Notice } from "@/components/base";
|
import { Notice } from "@/components/base";
|
||||||
|
import { useSystemState } from "@/hooks/use-system-state";
|
||||||
|
|
||||||
export const SystemInfoCard = () => {
|
export const SystemInfoCard = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { verge, patchVerge } = useVerge();
|
const { verge, patchVerge } = useVerge();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { isAdminMode, isSidecarMode, mutateRunningMode } = useSystemState();
|
||||||
|
|
||||||
// 系统信息状态
|
// 系统信息状态
|
||||||
const [systemState, setSystemState] = useState({
|
const [systemState, setSystemState] = useState({
|
||||||
@ -23,16 +32,6 @@ export const SystemInfoCard = () => {
|
|||||||
lastCheckUpdate: "-",
|
lastCheckUpdate: "-",
|
||||||
});
|
});
|
||||||
|
|
||||||
// 获取运行模式
|
|
||||||
const { data: runningMode = "sidecar", mutate: mutateRunningMode } = useSWR(
|
|
||||||
"getRunningMode",
|
|
||||||
getRunningMode,
|
|
||||||
{ suspense: false, revalidateOnFocus: false }
|
|
||||||
);
|
|
||||||
|
|
||||||
// 是否以sidecar模式运行
|
|
||||||
const isSidecarMode = runningMode === "sidecar";
|
|
||||||
|
|
||||||
// 初始化系统信息
|
// 初始化系统信息
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 获取系统信息
|
// 获取系统信息
|
||||||
@ -42,7 +41,10 @@ export const SystemInfoCard = () => {
|
|||||||
if (lines.length > 0) {
|
if (lines.length > 0) {
|
||||||
const sysName = lines[0].split(": ")[1] || "";
|
const sysName = lines[0].split(": ")[1] || "";
|
||||||
const sysVersion = lines[1].split(": ")[1] || "";
|
const sysVersion = lines[1].split(": ")[1] || "";
|
||||||
setSystemState(prev => ({ ...prev, osInfo: `${sysName} ${sysVersion}` }));
|
setSystemState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
osInfo: `${sysName} ${sysVersion}`,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(console.error);
|
.catch(console.error);
|
||||||
@ -53,9 +55,9 @@ export const SystemInfoCard = () => {
|
|||||||
try {
|
try {
|
||||||
const timestamp = parseInt(lastCheck, 10);
|
const timestamp = parseInt(lastCheck, 10);
|
||||||
if (!isNaN(timestamp)) {
|
if (!isNaN(timestamp)) {
|
||||||
setSystemState(prev => ({
|
setSystemState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
lastCheckUpdate: new Date(timestamp).toLocaleString()
|
lastCheckUpdate: new Date(timestamp).toLocaleString(),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -65,11 +67,11 @@ export const SystemInfoCard = () => {
|
|||||||
// 如果启用了自动检查更新但没有记录,设置当前时间并延迟检查
|
// 如果启用了自动检查更新但没有记录,设置当前时间并延迟检查
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
localStorage.setItem("last_check_update", now.toString());
|
localStorage.setItem("last_check_update", now.toString());
|
||||||
setSystemState(prev => ({
|
setSystemState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
lastCheckUpdate: new Date(now).toLocaleString()
|
lastCheckUpdate: new Date(now).toLocaleString(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (verge?.auto_check_update) {
|
if (verge?.auto_check_update) {
|
||||||
checkUpdate().catch(console.error);
|
checkUpdate().catch(console.error);
|
||||||
@ -84,9 +86,9 @@ export const SystemInfoCard = () => {
|
|||||||
async () => {
|
async () => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
localStorage.setItem("last_check_update", now.toString());
|
localStorage.setItem("last_check_update", now.toString());
|
||||||
setSystemState(prev => ({
|
setSystemState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
lastCheckUpdate: new Date(now).toLocaleString()
|
lastCheckUpdate: new Date(now).toLocaleString(),
|
||||||
}));
|
}));
|
||||||
return await checkUpdate();
|
return await checkUpdate();
|
||||||
},
|
},
|
||||||
@ -94,7 +96,7 @@ export const SystemInfoCard = () => {
|
|||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
refreshInterval: 24 * 60 * 60 * 1000, // 每天检查一次
|
refreshInterval: 24 * 60 * 60 * 1000, // 每天检查一次
|
||||||
dedupingInterval: 60 * 60 * 1000, // 1小时内不重复检查
|
dedupingInterval: 60 * 60 * 1000, // 1小时内不重复检查
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// 导航到设置页面
|
// 导航到设置页面
|
||||||
@ -104,13 +106,13 @@ export const SystemInfoCard = () => {
|
|||||||
|
|
||||||
// 切换自启动状态
|
// 切换自启动状态
|
||||||
const toggleAutoLaunch = useCallback(async () => {
|
const toggleAutoLaunch = useCallback(async () => {
|
||||||
if (!verge) return;
|
if (!verge || isAdminMode) return;
|
||||||
try {
|
try {
|
||||||
await patchVerge({ enable_auto_launch: !verge.enable_auto_launch });
|
await patchVerge({ enable_auto_launch: !verge.enable_auto_launch });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("切换开机自启动状态失败:", err);
|
console.error("切换开机自启动状态失败:", err);
|
||||||
}
|
}
|
||||||
}, [verge, patchVerge]);
|
}, [verge, patchVerge, isAdminMode]);
|
||||||
|
|
||||||
// 安装系统服务
|
// 安装系统服务
|
||||||
const onInstallService = useLockFn(async () => {
|
const onInstallService = useLockFn(async () => {
|
||||||
@ -118,18 +120,20 @@ export const SystemInfoCard = () => {
|
|||||||
Notice.info(t("Installing Service..."), 1000);
|
Notice.info(t("Installing Service..."), 1000);
|
||||||
await installService();
|
await installService();
|
||||||
Notice.success(t("Service Installed Successfully"), 2000);
|
Notice.success(t("Service Installed Successfully"), 2000);
|
||||||
await mutateRunningMode();
|
|
||||||
|
await mutateRunningMode();
|
||||||
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
Notice.error(err.message || err.toString(), 3000);
|
Notice.error(err.message || err.toString(), 3000);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 点击运行模式处理
|
// 点击运行模式处理,Sidecar或纯管理员模式允许安装服务
|
||||||
const handleRunningModeClick = useCallback(() => {
|
const handleRunningModeClick = useCallback(() => {
|
||||||
if (isSidecarMode) {
|
if (isSidecarMode || (isAdminMode && isSidecarMode)) {
|
||||||
onInstallService();
|
onInstallService();
|
||||||
}
|
}
|
||||||
}, [isSidecarMode, onInstallService]);
|
}, [isSidecarMode, isAdminMode, onInstallService]);
|
||||||
|
|
||||||
// 检查更新
|
// 检查更新
|
||||||
const onCheckUpdate = useLockFn(async () => {
|
const onCheckUpdate = useLockFn(async () => {
|
||||||
@ -147,16 +151,82 @@ export const SystemInfoCard = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 是否启用自启动
|
// 是否启用自启动
|
||||||
const autoLaunchEnabled = useMemo(() => verge?.enable_auto_launch || false, [verge]);
|
const autoLaunchEnabled = useMemo(
|
||||||
|
() => verge?.enable_auto_launch || false,
|
||||||
|
[verge],
|
||||||
|
);
|
||||||
|
|
||||||
// 运行模式样式
|
// 运行模式样式
|
||||||
const runningModeStyle = useMemo(() => ({
|
const runningModeStyle = useMemo(
|
||||||
cursor: isSidecarMode ? "pointer" : "default",
|
() => ({
|
||||||
textDecoration: isSidecarMode ? "underline" : "none",
|
// Sidecar或纯管理员模式允许安装服务
|
||||||
"&:hover": {
|
cursor: (isSidecarMode || (isAdminMode && isSidecarMode)) ? "pointer" : "default",
|
||||||
opacity: isSidecarMode ? 0.7 : 1,
|
textDecoration: (isSidecarMode || (isAdminMode && isSidecarMode)) ? "underline" : "none",
|
||||||
},
|
display: "flex",
|
||||||
}), [isSidecarMode]);
|
alignItems: "center",
|
||||||
|
gap: 0.5,
|
||||||
|
"&:hover": {
|
||||||
|
opacity: (isSidecarMode || (isAdminMode && isSidecarMode)) ? 0.7 : 1,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[isSidecarMode, isAdminMode],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 获取模式图标和文本
|
||||||
|
const getModeIcon = () => {
|
||||||
|
if (isAdminMode) {
|
||||||
|
// 判断是否为组合模式(管理员+服务)
|
||||||
|
if (!isSidecarMode) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AdminPanelSettingsOutlined
|
||||||
|
sx={{ color: "primary.main", fontSize: 16 }}
|
||||||
|
titleAccess={t("Administrator Mode")}
|
||||||
|
/>
|
||||||
|
<DnsOutlined
|
||||||
|
sx={{ color: "success.main", fontSize: 16, ml: 0.5 }}
|
||||||
|
titleAccess={t("Service Mode")}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<AdminPanelSettingsOutlined
|
||||||
|
sx={{ color: "primary.main", fontSize: 16 }}
|
||||||
|
titleAccess={t("Administrator Mode")}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (isSidecarMode) {
|
||||||
|
return (
|
||||||
|
<ExtensionOutlined
|
||||||
|
sx={{ color: "info.main", fontSize: 16 }}
|
||||||
|
titleAccess={t("Sidecar Mode")}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<DnsOutlined
|
||||||
|
sx={{ color: "success.main", fontSize: 16 }}
|
||||||
|
titleAccess={t("Service Mode")}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取模式文本
|
||||||
|
const getModeText = () => {
|
||||||
|
if (isAdminMode) {
|
||||||
|
// 判断是否同时处于服务模式
|
||||||
|
if (!isSidecarMode) {
|
||||||
|
return t("Administrator + Service Mode");
|
||||||
|
}
|
||||||
|
return t("Administrator Mode");
|
||||||
|
} else if (isSidecarMode) {
|
||||||
|
return t("Sidecar Mode");
|
||||||
|
} else {
|
||||||
|
return t("Service Mode");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 只有当verge存在时才渲染内容
|
// 只有当verge存在时才渲染内容
|
||||||
if (!verge) return null;
|
if (!verge) return null;
|
||||||
@ -182,21 +252,29 @@ export const SystemInfoCard = () => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Stack direction="row" justifyContent="space-between">
|
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
{t("Auto Launch")}
|
{t("Auto Launch")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Chip
|
<Stack direction="row" spacing={1} alignItems="center">
|
||||||
size="small"
|
{isAdminMode && (
|
||||||
label={autoLaunchEnabled ? t("Enabled") : t("Disabled")}
|
<Tooltip title={t("Administrator mode does not support auto launch")}>
|
||||||
color={autoLaunchEnabled ? "success" : "default"}
|
<WarningOutlined sx={{ color: "warning.main", fontSize: 20 }} />
|
||||||
variant={autoLaunchEnabled ? "filled" : "outlined"}
|
</Tooltip>
|
||||||
onClick={toggleAutoLaunch}
|
)}
|
||||||
sx={{ cursor: "pointer" }}
|
<Chip
|
||||||
/>
|
size="small"
|
||||||
|
label={autoLaunchEnabled ? t("Enabled") : t("Disabled")}
|
||||||
|
color={autoLaunchEnabled ? "success" : "default"}
|
||||||
|
variant={autoLaunchEnabled ? "filled" : "outlined"}
|
||||||
|
onClick={toggleAutoLaunch}
|
||||||
|
disabled={isAdminMode}
|
||||||
|
sx={{ cursor: isAdminMode ? "not-allowed" : "pointer" }}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Stack direction="row" justifyContent="space-between">
|
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
{t("Running Mode")}
|
{t("Running Mode")}
|
||||||
</Typography>
|
</Typography>
|
||||||
@ -206,7 +284,8 @@ export const SystemInfoCard = () => {
|
|||||||
onClick={handleRunningModeClick}
|
onClick={handleRunningModeClick}
|
||||||
sx={runningModeStyle}
|
sx={runningModeStyle}
|
||||||
>
|
>
|
||||||
{isSidecarMode ? t("Sidecar Mode") : t("Service Mode")}
|
{getModeIcon()}
|
||||||
|
{getModeText()}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Divider />
|
<Divider />
|
||||||
|
@ -179,17 +179,21 @@ export const ProfileItem = (props: Props) => {
|
|||||||
/// 0 不使用任何代理
|
/// 0 不使用任何代理
|
||||||
/// 1 使用订阅好的代理
|
/// 1 使用订阅好的代理
|
||||||
/// 2 至少使用一个代理,根据订阅,如果没订阅,默认使用系统代理
|
/// 2 至少使用一个代理,根据订阅,如果没订阅,默认使用系统代理
|
||||||
const onUpdate = useLockFn(async (type: 0 | 1 | 2) => {
|
const onUpdate = useLockFn(async (type: 0 | 1 | 2): Promise<void> => {
|
||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
setLoadingCache((cache) => ({ ...cache, [itemData.uid]: true }));
|
setLoadingCache((cache) => ({ ...cache, [itemData.uid]: true }));
|
||||||
|
|
||||||
const option: Partial<IProfileOption> = {};
|
// 存储原始设置以便回退后恢复
|
||||||
|
const originalOptions = {
|
||||||
|
with_proxy: itemData.option?.with_proxy,
|
||||||
|
self_proxy: itemData.option?.self_proxy
|
||||||
|
};
|
||||||
|
|
||||||
|
// 根据类型设置初始更新选项
|
||||||
|
const option: Partial<IProfileOption> = {};
|
||||||
if (type === 0) {
|
if (type === 0) {
|
||||||
option.with_proxy = false;
|
option.with_proxy = false;
|
||||||
option.self_proxy = false;
|
option.self_proxy = false;
|
||||||
} else if (type === 1) {
|
|
||||||
// nothing
|
|
||||||
} else if (type === 2) {
|
} else if (type === 2) {
|
||||||
if (itemData.option?.self_proxy) {
|
if (itemData.option?.self_proxy) {
|
||||||
option.with_proxy = false;
|
option.with_proxy = false;
|
||||||
@ -201,14 +205,31 @@ export const ProfileItem = (props: Props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 尝试正常更新
|
||||||
await updateProfile(itemData.uid, option);
|
await updateProfile(itemData.uid, option);
|
||||||
Notice.success(t("Update subscription successfully"));
|
Notice.success(t("Update subscription successfully"));
|
||||||
mutate("getProfiles");
|
mutate("getProfiles");
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
// 更新失败,尝试使用自身代理
|
||||||
const errmsg = err?.message || err.toString();
|
const errmsg = err?.message || err.toString();
|
||||||
Notice.error(
|
Notice.info(t("Update failed, retrying with Clash proxy..."));
|
||||||
errmsg.replace(/error sending request for url (\S+?): /, ""),
|
|
||||||
);
|
try {
|
||||||
|
await updateProfile(itemData.uid, {
|
||||||
|
with_proxy: false,
|
||||||
|
self_proxy: true
|
||||||
|
});
|
||||||
|
|
||||||
|
Notice.success(t("Update with Clash proxy successfully"));
|
||||||
|
|
||||||
|
await updateProfile(itemData.uid, originalOptions);
|
||||||
|
mutate("getProfiles");
|
||||||
|
} catch (retryErr: any) {
|
||||||
|
const retryErrmsg = retryErr?.message || retryErr.toString();
|
||||||
|
Notice.error(
|
||||||
|
`${t("Update failed even with Clash proxy")}: ${retryErrmsg.replace(/error sending request for url (\S+?): /, "")}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingCache((cache) => ({ ...cache, [itemData.uid]: false }));
|
setLoadingCache((cache) => ({ ...cache, [itemData.uid]: false }));
|
||||||
}
|
}
|
||||||
|
@ -88,11 +88,13 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
|
|||||||
formIns.handleSubmit(async (form) => {
|
formIns.handleSubmit(async (form) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
|
// 基本验证
|
||||||
if (!form.type) throw new Error("`Type` should not be null");
|
if (!form.type) throw new Error("`Type` should not be null");
|
||||||
if (form.type === "remote" && !form.url) {
|
if (form.type === "remote" && !form.url) {
|
||||||
throw new Error("The URL should not be null");
|
throw new Error("The URL should not be null");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理表单数据
|
||||||
if (form.option?.update_interval) {
|
if (form.option?.update_interval) {
|
||||||
form.option.update_interval = +form.option.update_interval;
|
form.option.update_interval = +form.option.update_interval;
|
||||||
} else {
|
} else {
|
||||||
@ -101,25 +103,72 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
|
|||||||
if (form.option?.user_agent === "") {
|
if (form.option?.user_agent === "") {
|
||||||
delete form.option.user_agent;
|
delete form.option.user_agent;
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = form.name || `${form.type} file`;
|
const name = form.name || `${form.type} file`;
|
||||||
const item = { ...form, name };
|
const item = { ...form, name };
|
||||||
|
const isRemote = form.type === "remote";
|
||||||
// 创建
|
|
||||||
if (openType === "new") {
|
// 保存原始代理设置以便回退成功后恢复
|
||||||
await createProfile(item, fileDataRef.current);
|
const originalOptions = {
|
||||||
}
|
with_proxy: form.option?.with_proxy,
|
||||||
// 编辑
|
self_proxy: form.option?.self_proxy
|
||||||
else {
|
};
|
||||||
if (!form.uid) throw new Error("UID not found");
|
|
||||||
await patchProfile(form.uid, item);
|
// 执行创建或更新操作,本地配置不需要回退机制
|
||||||
|
if (!isRemote) {
|
||||||
|
if (openType === "new") {
|
||||||
|
await createProfile(item, fileDataRef.current);
|
||||||
|
} else {
|
||||||
|
if (!form.uid) throw new Error("UID not found");
|
||||||
|
await patchProfile(form.uid, item);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 远程配置使用回退机制
|
||||||
|
try {
|
||||||
|
// 尝试正常操作
|
||||||
|
if (openType === "new") {
|
||||||
|
await createProfile(item, fileDataRef.current);
|
||||||
|
} else {
|
||||||
|
if (!form.uid) throw new Error("UID not found");
|
||||||
|
await patchProfile(form.uid, item);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// 首次创建/更新失败,尝试使用自身代理
|
||||||
|
Notice.info(t("Profile creation failed, retrying with Clash proxy..."));
|
||||||
|
|
||||||
|
// 使用自身代理的配置
|
||||||
|
const retryItem = {
|
||||||
|
...item,
|
||||||
|
option: {
|
||||||
|
...item.option,
|
||||||
|
with_proxy: false,
|
||||||
|
self_proxy: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 使用自身代理再次尝试
|
||||||
|
if (openType === "new") {
|
||||||
|
await createProfile(retryItem, fileDataRef.current);
|
||||||
|
} else {
|
||||||
|
if (!form.uid) throw new Error("UID not found");
|
||||||
|
await patchProfile(form.uid, retryItem);
|
||||||
|
|
||||||
|
// 编辑模式下恢复原始代理设置
|
||||||
|
await patchProfile(form.uid, { option: originalOptions });
|
||||||
|
}
|
||||||
|
|
||||||
|
Notice.success(t("Profile creation succeeded with Clash proxy"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 成功后的操作
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
setLoading(false);
|
|
||||||
setTimeout(() => formIns.reset(), 500);
|
setTimeout(() => formIns.reset(), 500);
|
||||||
fileDataRef.current = null;
|
fileDataRef.current = null;
|
||||||
props.onChange();
|
props.onChange();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
Notice.error(err.message || err.toString());
|
Notice.error(err.message || err.toString());
|
||||||
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1,195 +1,46 @@
|
|||||||
import dayjs from "dayjs";
|
|
||||||
import useSWR, { mutate } from "swr";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
Box,
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogActions,
|
||||||
IconButton,
|
IconButton,
|
||||||
List,
|
List,
|
||||||
ListItem,
|
ListItem,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
styled,
|
|
||||||
Box,
|
|
||||||
alpha,
|
|
||||||
Typography,
|
Typography,
|
||||||
Divider,
|
Divider,
|
||||||
LinearProgress,
|
LinearProgress,
|
||||||
keyframes,
|
alpha,
|
||||||
|
styled,
|
||||||
|
useTheme
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { RefreshRounded } from "@mui/icons-material";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { getProxyProviders, proxyProviderUpdate } from "@/services/api";
|
import { useLockFn } from "ahooks";
|
||||||
import { BaseDialog } from "../base";
|
import { proxyProviderUpdate } from "@/services/api";
|
||||||
|
import { useAppData } from "@/providers/app-data-provider";
|
||||||
|
import { Notice } from "@/components/base";
|
||||||
|
import { StorageOutlined, RefreshRounded } from "@mui/icons-material";
|
||||||
|
import dayjs from "dayjs";
|
||||||
import parseTraffic from "@/utils/parse-traffic";
|
import parseTraffic from "@/utils/parse-traffic";
|
||||||
|
|
||||||
const round = keyframes`
|
// 定义代理提供者类型
|
||||||
from { transform: rotate(0deg); }
|
interface ProxyProviderItem {
|
||||||
to { transform: rotate(360deg); }
|
name?: string;
|
||||||
`;
|
proxies: any[];
|
||||||
|
updatedAt: number;
|
||||||
export const ProviderButton = () => {
|
vehicleType: string;
|
||||||
const { t } = useTranslation();
|
subscriptionInfo?: {
|
||||||
const { data } = useSWR("getProxyProviders", getProxyProviders);
|
Upload: number;
|
||||||
|
Download: number;
|
||||||
const [open, setOpen] = useState(false);
|
Total: number;
|
||||||
|
Expire: number;
|
||||||
const hasProvider = Object.keys(data || {}).length > 0;
|
|
||||||
const [updating, setUpdating] = useState(
|
|
||||||
Object.keys(data || {}).map(() => false),
|
|
||||||
);
|
|
||||||
|
|
||||||
const setUpdatingAt = (status: boolean, index: number) => {
|
|
||||||
setUpdating((prev) => {
|
|
||||||
const next = [...prev];
|
|
||||||
next[index] = status;
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const handleUpdate = async (key: string, index: number) => {
|
|
||||||
setUpdatingAt(true, index);
|
|
||||||
proxyProviderUpdate(key)
|
|
||||||
.then(async () => {
|
|
||||||
setUpdatingAt(false, index);
|
|
||||||
await mutate("getProxies");
|
|
||||||
await mutate("getProxyProviders");
|
|
||||||
})
|
|
||||||
.catch(async () => {
|
|
||||||
setUpdatingAt(false, index);
|
|
||||||
await mutate("getProxies");
|
|
||||||
await mutate("getProxyProviders");
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (!hasProvider) return null;
|
// 样式化组件 - 类型框
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
variant="outlined"
|
|
||||||
sx={{ textTransform: "capitalize" }}
|
|
||||||
onClick={() => setOpen(true)}
|
|
||||||
>
|
|
||||||
{t("Proxy Provider")}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<BaseDialog
|
|
||||||
open={open}
|
|
||||||
title={
|
|
||||||
<Box display="flex" justifyContent="space-between" gap={1}>
|
|
||||||
<Typography variant="h6">{t("Proxy Provider")}</Typography>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
size="small"
|
|
||||||
onClick={async () => {
|
|
||||||
Object.entries(data || {}).forEach(async ([key], index) => {
|
|
||||||
await handleUpdate(key, index);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("Update All")}
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
}
|
|
||||||
contentSx={{ width: 400 }}
|
|
||||||
disableOk
|
|
||||||
cancelBtn={t("Close")}
|
|
||||||
onClose={() => setOpen(false)}
|
|
||||||
onCancel={() => setOpen(false)}
|
|
||||||
>
|
|
||||||
<List sx={{ py: 0, minHeight: 250 }}>
|
|
||||||
{Object.entries(data || {}).map(([key, item], index) => {
|
|
||||||
const time = dayjs(item.updatedAt);
|
|
||||||
const sub = item.subscriptionInfo;
|
|
||||||
const hasSubInfo = !!sub;
|
|
||||||
const upload = sub?.Upload || 0;
|
|
||||||
const download = sub?.Download || 0;
|
|
||||||
const total = sub?.Total || 0;
|
|
||||||
const expire = sub?.Expire || 0;
|
|
||||||
const progress = Math.min(
|
|
||||||
Math.round(((download + upload) * 100) / (total + 0.01)) + 1,
|
|
||||||
100,
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ListItem
|
|
||||||
sx={{
|
|
||||||
p: 0,
|
|
||||||
borderRadius: "10px",
|
|
||||||
border: "solid 2px var(--divider-color)",
|
|
||||||
mb: 1,
|
|
||||||
}}
|
|
||||||
key={key}
|
|
||||||
>
|
|
||||||
<ListItemText
|
|
||||||
sx={{ px: 1 }}
|
|
||||||
primary={
|
|
||||||
<>
|
|
||||||
<Typography
|
|
||||||
variant="h6"
|
|
||||||
component="span"
|
|
||||||
noWrap
|
|
||||||
title={key}
|
|
||||||
>
|
|
||||||
{key}
|
|
||||||
</Typography>
|
|
||||||
<TypeBox component="span" sx={{ marginLeft: "8px" }}>
|
|
||||||
{item.proxies.length}
|
|
||||||
</TypeBox>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
secondary={
|
|
||||||
<>
|
|
||||||
<StyledTypeBox component="span">
|
|
||||||
{item.vehicleType}
|
|
||||||
</StyledTypeBox>
|
|
||||||
<StyledTypeBox component="span">
|
|
||||||
{t("Update At")} {time.fromNow()}
|
|
||||||
</StyledTypeBox>
|
|
||||||
{hasSubInfo && (
|
|
||||||
<>
|
|
||||||
<Box sx={{ ...boxStyle, fontSize: 14 }}>
|
|
||||||
<span title="Used / Total">
|
|
||||||
{parseTraffic(upload + download)} /{" "}
|
|
||||||
{parseTraffic(total)}
|
|
||||||
</span>
|
|
||||||
<span title="Expire Time">
|
|
||||||
{parseExpire(expire)}
|
|
||||||
</span>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<LinearProgress
|
|
||||||
variant="determinate"
|
|
||||||
value={progress}
|
|
||||||
style={{ opacity: total > 0 ? 1 : 0 }}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Divider orientation="vertical" flexItem />
|
|
||||||
<IconButton
|
|
||||||
size="small"
|
|
||||||
color="inherit"
|
|
||||||
title={`${t("Update")}${t("Proxy Provider")}`}
|
|
||||||
onClick={() => handleUpdate(key, index)}
|
|
||||||
sx={{
|
|
||||||
animation: updating[index]
|
|
||||||
? `1s linear infinite ${round}`
|
|
||||||
: "none",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<RefreshRounded />
|
|
||||||
</IconButton>
|
|
||||||
</ListItem>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</List>
|
|
||||||
</BaseDialog>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
const TypeBox = styled(Box)<{ component?: React.ElementType }>(({ theme }) => ({
|
const TypeBox = styled(Box)<{ component?: React.ElementType }>(({ theme }) => ({
|
||||||
display: "inline-block",
|
display: "inline-block",
|
||||||
border: "1px solid #ccc",
|
border: "1px solid #ccc",
|
||||||
@ -202,28 +53,271 @@ const TypeBox = styled(Box)<{ component?: React.ElementType }>(({ theme }) => ({
|
|||||||
lineHeight: 1.25,
|
lineHeight: 1.25,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledTypeBox = styled(Box)<{ component?: React.ElementType }>(
|
// 解析过期时间
|
||||||
({ theme }) => ({
|
const parseExpire = (expire?: number) => {
|
||||||
display: "inline-block",
|
|
||||||
border: "1px solid #ccc",
|
|
||||||
borderColor: alpha(theme.palette.primary.main, 0.5),
|
|
||||||
color: alpha(theme.palette.primary.main, 0.8),
|
|
||||||
borderRadius: 4,
|
|
||||||
fontSize: 10,
|
|
||||||
marginRight: "4px",
|
|
||||||
padding: "0 2px",
|
|
||||||
lineHeight: 1.25,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const boxStyle = {
|
|
||||||
height: 26,
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
};
|
|
||||||
|
|
||||||
function parseExpire(expire?: number) {
|
|
||||||
if (!expire) return "-";
|
if (!expire) return "-";
|
||||||
return dayjs(expire * 1000).format("YYYY-MM-DD");
|
return dayjs(expire * 1000).format("YYYY-MM-DD");
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export const ProviderButton = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const theme = useTheme();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const { proxyProviders, refreshProxy, refreshProxyProviders } = useAppData();
|
||||||
|
const [updating, setUpdating] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
|
// 检查是否有提供者
|
||||||
|
const hasProviders = Object.keys(proxyProviders || {}).length > 0;
|
||||||
|
|
||||||
|
// 更新单个代理提供者
|
||||||
|
const updateProvider = useLockFn(async (name: string) => {
|
||||||
|
try {
|
||||||
|
// 设置更新状态
|
||||||
|
setUpdating(prev => ({ ...prev, [name]: true }));
|
||||||
|
|
||||||
|
await proxyProviderUpdate(name);
|
||||||
|
|
||||||
|
// 刷新数据
|
||||||
|
await refreshProxy();
|
||||||
|
await refreshProxyProviders();
|
||||||
|
|
||||||
|
Notice.success(`${name} 更新成功`);
|
||||||
|
} catch (err: any) {
|
||||||
|
Notice.error(`${name} 更新失败: ${err?.message || err.toString()}`);
|
||||||
|
} finally {
|
||||||
|
// 清除更新状态
|
||||||
|
setUpdating(prev => ({ ...prev, [name]: false }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新所有代理提供者
|
||||||
|
const updateAllProviders = useLockFn(async () => {
|
||||||
|
try {
|
||||||
|
// 获取所有provider的名称
|
||||||
|
const allProviders = Object.keys(proxyProviders || {});
|
||||||
|
if (allProviders.length === 0) {
|
||||||
|
Notice.info("没有可更新的代理提供者");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置所有provider为更新中状态
|
||||||
|
const newUpdating = allProviders.reduce((acc, key) => {
|
||||||
|
acc[key] = true;
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, boolean>);
|
||||||
|
setUpdating(newUpdating);
|
||||||
|
|
||||||
|
// 改为串行逐个更新所有provider
|
||||||
|
for (const name of allProviders) {
|
||||||
|
try {
|
||||||
|
await proxyProviderUpdate(name);
|
||||||
|
// 每个更新完成后更新状态
|
||||||
|
setUpdating(prev => ({ ...prev, [name]: false }));
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`更新 ${name} 失败`, err);
|
||||||
|
// 继续执行下一个,不中断整体流程
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新数据
|
||||||
|
await refreshProxy();
|
||||||
|
await refreshProxyProviders();
|
||||||
|
|
||||||
|
Notice.success("全部代理提供者更新成功");
|
||||||
|
} catch (err: any) {
|
||||||
|
Notice.error(`更新失败: ${err?.message || err.toString()}`);
|
||||||
|
} finally {
|
||||||
|
// 清除所有更新状态
|
||||||
|
setUpdating({});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!hasProviders) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
startIcon={<StorageOutlined />}
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
sx={{ mr: 1 }}
|
||||||
|
>
|
||||||
|
{t("Proxy Provider")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
onClose={handleClose}
|
||||||
|
maxWidth="sm"
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
<DialogTitle>
|
||||||
|
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||||
|
<Typography variant="h6">{t("Proxy Provider")}</Typography>
|
||||||
|
<Box>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
size="small"
|
||||||
|
onClick={updateAllProviders}
|
||||||
|
>
|
||||||
|
{t("Update All")}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</DialogTitle>
|
||||||
|
|
||||||
|
<DialogContent>
|
||||||
|
<List sx={{ py: 0, minHeight: 250 }}>
|
||||||
|
{Object.entries(proxyProviders || {}).map(([key, item]) => {
|
||||||
|
const provider = item as ProxyProviderItem;
|
||||||
|
const time = dayjs(provider.updatedAt);
|
||||||
|
const isUpdating = updating[key];
|
||||||
|
|
||||||
|
// 订阅信息
|
||||||
|
const sub = provider.subscriptionInfo;
|
||||||
|
const hasSubInfo = !!sub;
|
||||||
|
const upload = sub?.Upload || 0;
|
||||||
|
const download = sub?.Download || 0;
|
||||||
|
const total = sub?.Total || 0;
|
||||||
|
const expire = sub?.Expire || 0;
|
||||||
|
|
||||||
|
// 流量使用进度
|
||||||
|
const progress = total > 0
|
||||||
|
? Math.min(Math.round(((download + upload) * 100) / total) + 1, 100)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItem
|
||||||
|
key={key}
|
||||||
|
sx={[
|
||||||
|
{
|
||||||
|
p: 0,
|
||||||
|
mb: "8px",
|
||||||
|
borderRadius: 2,
|
||||||
|
overflow: "hidden",
|
||||||
|
transition: "all 0.2s"
|
||||||
|
},
|
||||||
|
({ palette: { mode, primary } }) => {
|
||||||
|
const bgcolor = mode === "light" ? "#ffffff" : "#24252f";
|
||||||
|
const hoverColor = mode === "light"
|
||||||
|
? alpha(primary.main, 0.1)
|
||||||
|
: alpha(primary.main, 0.2);
|
||||||
|
|
||||||
|
return {
|
||||||
|
backgroundColor: bgcolor,
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: hoverColor,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<ListItemText
|
||||||
|
sx={{ px: 2, py: 1 }}
|
||||||
|
primary={
|
||||||
|
<Box sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
}}>
|
||||||
|
<Typography
|
||||||
|
variant="subtitle1"
|
||||||
|
component="div"
|
||||||
|
noWrap
|
||||||
|
title={key}
|
||||||
|
sx={{ display: "flex", alignItems: "center" }}
|
||||||
|
>
|
||||||
|
<span style={{ marginRight: "8px" }}>{key}</span>
|
||||||
|
<TypeBox component="span">
|
||||||
|
{provider.proxies.length}
|
||||||
|
</TypeBox>
|
||||||
|
<TypeBox component="span">
|
||||||
|
{provider.vehicleType}
|
||||||
|
</TypeBox>
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography variant="body2" color="text.secondary" noWrap>
|
||||||
|
<small>{t("Update At")}: </small>{time.fromNow()}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
secondary={
|
||||||
|
<>
|
||||||
|
{/* 订阅信息 */}
|
||||||
|
{hasSubInfo && (
|
||||||
|
<>
|
||||||
|
<Box sx={{
|
||||||
|
mb: 1,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
}}>
|
||||||
|
<span title={t("Used / Total") as string}>
|
||||||
|
{parseTraffic(upload + download)} / {parseTraffic(total)}
|
||||||
|
</span>
|
||||||
|
<span title={t("Expire Time") as string}>
|
||||||
|
{parseExpire(expire)}
|
||||||
|
</span>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 进度条 */}
|
||||||
|
<LinearProgress
|
||||||
|
variant="determinate"
|
||||||
|
value={progress}
|
||||||
|
sx={{
|
||||||
|
height: 6,
|
||||||
|
borderRadius: 3,
|
||||||
|
opacity: total > 0 ? 1 : 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Divider orientation="vertical" flexItem />
|
||||||
|
<Box sx={{
|
||||||
|
width: 40,
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center"
|
||||||
|
}}>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
color="primary"
|
||||||
|
onClick={(e) => {
|
||||||
|
updateProvider(key);
|
||||||
|
}}
|
||||||
|
disabled={isUpdating}
|
||||||
|
sx={{
|
||||||
|
animation: isUpdating ? "spin 1s linear infinite" : "none",
|
||||||
|
"@keyframes spin": {
|
||||||
|
"0%": { transform: "rotate(0deg)" },
|
||||||
|
"100%": { transform: "rotate(360deg)" }
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title={t("Update Provider") as string}
|
||||||
|
>
|
||||||
|
<RefreshRounded />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</List>
|
||||||
|
</DialogContent>
|
||||||
|
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={handleClose} variant="outlined">
|
||||||
|
{t("Close")}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -5,6 +5,8 @@ import {
|
|||||||
ListItemButton,
|
ListItemButton,
|
||||||
Typography,
|
Typography,
|
||||||
styled,
|
styled,
|
||||||
|
Chip,
|
||||||
|
Tooltip,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import {
|
import {
|
||||||
ExpandLessRounded,
|
ExpandLessRounded,
|
||||||
@ -21,17 +23,19 @@ import { useThemeMode } from "@/services/states";
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { convertFileSrc } from "@tauri-apps/api/core";
|
import { convertFileSrc } from "@tauri-apps/api/core";
|
||||||
import { downloadIconCache } from "@/services/cmds";
|
import { downloadIconCache } from "@/services/cmds";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface RenderProps {
|
interface RenderProps {
|
||||||
item: IRenderItem;
|
item: IRenderItem;
|
||||||
indent: boolean;
|
indent: boolean;
|
||||||
onLocation: (group: IProxyGroupItem) => void;
|
onLocation: (group: IRenderItem["group"]) => void;
|
||||||
onCheckAll: (groupName: string) => void;
|
onCheckAll: (groupName: string) => void;
|
||||||
onHeadState: (groupName: string, patch: Partial<HeadState>) => void;
|
onHeadState: (groupName: string, patch: Partial<HeadState>) => void;
|
||||||
onChangeProxy: (group: IProxyGroupItem, proxy: IProxyItem) => void;
|
onChangeProxy: (group: IRenderItem["group"], proxy: IRenderItem["proxy"] & { name: string }) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProxyRender = (props: RenderProps) => {
|
export const ProxyRender = (props: RenderProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { indent, item, onLocation, onCheckAll, onHeadState, onChangeProxy } =
|
const { indent, item, onLocation, onCheckAll, onHeadState, onChangeProxy } =
|
||||||
props;
|
props;
|
||||||
const { type, group, headState, proxy, proxyCol } = item;
|
const { type, group, headState, proxy, proxyCol } = item;
|
||||||
@ -123,7 +127,20 @@ export const ProxyRender = (props: RenderProps) => {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{headState?.open ? <ExpandLessRounded /> : <ExpandMoreRounded />}
|
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||||
|
<Tooltip title={t("Proxy Count")} arrow>
|
||||||
|
<Chip
|
||||||
|
size="small"
|
||||||
|
label={`${group.all.length}`}
|
||||||
|
sx={{
|
||||||
|
mr: 1,
|
||||||
|
backgroundColor: (theme) => alpha(theme.palette.primary.main, 0.1),
|
||||||
|
color: (theme) => theme.palette.primary.main,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
{headState?.open ? <ExpandLessRounded /> : <ExpandMoreRounded />}
|
||||||
|
</Box>
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
import useSWR from "swr";
|
|
||||||
import { useEffect, useMemo, useCallback } from "react";
|
import { useEffect, useMemo, useCallback } from "react";
|
||||||
import { getProxies } from "@/services/api";
|
|
||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
import { filterSort } from "./use-filter-sort";
|
import { filterSort } from "./use-filter-sort";
|
||||||
import { useWindowWidth } from "./use-window-width";
|
import { useWindowWidth } from "./use-window-width";
|
||||||
@ -9,12 +7,52 @@ import {
|
|||||||
DEFAULT_STATE,
|
DEFAULT_STATE,
|
||||||
type HeadState,
|
type HeadState,
|
||||||
} from "./use-head-state";
|
} from "./use-head-state";
|
||||||
|
import { useAppData } from "@/providers/app-data-provider";
|
||||||
|
|
||||||
|
// 定义代理项接口
|
||||||
|
interface IProxyItem {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
udp: boolean;
|
||||||
|
xudp: boolean;
|
||||||
|
tfo: boolean;
|
||||||
|
mptcp: boolean;
|
||||||
|
smux: boolean;
|
||||||
|
history: {
|
||||||
|
time: string;
|
||||||
|
delay: number;
|
||||||
|
}[];
|
||||||
|
provider?: string;
|
||||||
|
testUrl?: string;
|
||||||
|
[key: string]: any; // 添加索引签名以适应其他可能的属性
|
||||||
|
}
|
||||||
|
|
||||||
|
// 代理组类型
|
||||||
|
type ProxyGroup = {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
udp: boolean;
|
||||||
|
xudp: boolean;
|
||||||
|
tfo: boolean;
|
||||||
|
mptcp: boolean;
|
||||||
|
smux: boolean;
|
||||||
|
history: {
|
||||||
|
time: string;
|
||||||
|
delay: number;
|
||||||
|
}[];
|
||||||
|
now: string;
|
||||||
|
all: IProxyItem[];
|
||||||
|
hidden?: boolean;
|
||||||
|
icon?: string;
|
||||||
|
testUrl?: string;
|
||||||
|
provider?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export interface IRenderItem {
|
export interface IRenderItem {
|
||||||
// 组 | head | item | empty | item col
|
// 组 | head | item | empty | item col
|
||||||
type: 0 | 1 | 2 | 3 | 4;
|
type: 0 | 1 | 2 | 3 | 4;
|
||||||
key: string;
|
key: string;
|
||||||
group: IProxyGroupItem;
|
group: ProxyGroup;
|
||||||
proxy?: IProxyItem;
|
proxy?: IProxyItem;
|
||||||
col?: number;
|
col?: number;
|
||||||
proxyCol?: IProxyItem[];
|
proxyCol?: IProxyItem[];
|
||||||
@ -51,16 +89,8 @@ const groupProxies = <T = any>(list: T[], size: number): T[][] => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useRenderList = (mode: string) => {
|
export const useRenderList = (mode: string) => {
|
||||||
const { data: proxiesData, mutate: mutateProxies } = useSWR(
|
// 使用全局数据提供者
|
||||||
"getProxies",
|
const { proxies: proxiesData, refreshProxy } = useAppData();
|
||||||
getProxies,
|
|
||||||
{
|
|
||||||
refreshInterval: 2000,
|
|
||||||
revalidateOnFocus: false,
|
|
||||||
revalidateOnReconnect: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const { verge } = useVerge();
|
const { verge } = useVerge();
|
||||||
const { width } = useWindowWidth();
|
const { width } = useWindowWidth();
|
||||||
const [headStates, setHeadState] = useHeadStateNew();
|
const [headStates, setHeadState] = useHeadStateNew();
|
||||||
@ -80,9 +110,9 @@ export const useRenderList = (mode: string) => {
|
|||||||
(mode === "rule" && !groups.length) ||
|
(mode === "rule" && !groups.length) ||
|
||||||
(mode === "global" && proxies.length < 2)
|
(mode === "global" && proxies.length < 2)
|
||||||
) {
|
) {
|
||||||
setTimeout(() => mutateProxies(), 500);
|
setTimeout(() => refreshProxy(), 500);
|
||||||
}
|
}
|
||||||
}, [proxiesData, mode, mutateProxies]);
|
}, [proxiesData, mode, refreshProxy]);
|
||||||
|
|
||||||
// 处理渲染列表
|
// 处理渲染列表
|
||||||
const renderList: IRenderItem[] = useMemo(() => {
|
const renderList: IRenderItem[] = useMemo(() => {
|
||||||
@ -94,7 +124,7 @@ export const useRenderList = (mode: string) => {
|
|||||||
? proxiesData.groups
|
? proxiesData.groups
|
||||||
: [proxiesData.global!];
|
: [proxiesData.global!];
|
||||||
|
|
||||||
const retList = renderGroups.flatMap((group) => {
|
const retList = renderGroups.flatMap((group: ProxyGroup) => {
|
||||||
const headState = headStates[group.name] || DEFAULT_STATE;
|
const headState = headStates[group.name] || DEFAULT_STATE;
|
||||||
const ret: IRenderItem[] = [
|
const ret: IRenderItem[] = [
|
||||||
{
|
{
|
||||||
@ -131,9 +161,9 @@ export const useRenderList = (mode: string) => {
|
|||||||
});
|
});
|
||||||
} else if (col > 1) {
|
} else if (col > 1) {
|
||||||
return ret.concat(
|
return ret.concat(
|
||||||
groupProxies(proxies, col).map((proxyCol) => ({
|
groupProxies(proxies, col).map((proxyCol, colIndex) => ({
|
||||||
type: 4,
|
type: 4,
|
||||||
key: `col-${group.name}-${proxyCol[0].name}`,
|
key: `col-${group.name}-${proxyCol[0].name}-${colIndex}`,
|
||||||
group,
|
group,
|
||||||
headState,
|
headState,
|
||||||
col,
|
col,
|
||||||
@ -158,12 +188,12 @@ export const useRenderList = (mode: string) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!useRule) return retList.slice(1);
|
if (!useRule) return retList.slice(1);
|
||||||
return retList.filter((item) => !item.group.hidden);
|
return retList.filter((item: IRenderItem) => !item.group.hidden);
|
||||||
}, [headStates, proxiesData, mode, col]);
|
}, [headStates, proxiesData, mode, col]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
renderList,
|
renderList,
|
||||||
onProxies: mutateProxies,
|
onProxies: refreshProxy,
|
||||||
onHeadState: setHeadState,
|
onHeadState: setHeadState,
|
||||||
currentColumns: col,
|
currentColumns: col,
|
||||||
};
|
};
|
||||||
|
@ -1,170 +1,39 @@
|
|||||||
import dayjs from "dayjs";
|
|
||||||
import useSWR, { mutate } from "swr";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
Box,
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogActions,
|
||||||
IconButton,
|
IconButton,
|
||||||
List,
|
List,
|
||||||
ListItem,
|
ListItem,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
Typography,
|
Typography,
|
||||||
styled,
|
|
||||||
Box,
|
|
||||||
alpha,
|
|
||||||
Divider,
|
Divider,
|
||||||
keyframes,
|
alpha,
|
||||||
|
styled,
|
||||||
|
useTheme
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { RefreshRounded } from "@mui/icons-material";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { getRuleProviders, ruleProviderUpdate } from "@/services/api";
|
import { useLockFn } from "ahooks";
|
||||||
import { BaseDialog } from "../base";
|
import { ruleProviderUpdate } from "@/services/api";
|
||||||
|
import { Notice } from "@/components/base";
|
||||||
|
import { StorageOutlined, RefreshRounded } from "@mui/icons-material";
|
||||||
|
import { useAppData } from "@/providers/app-data-provider";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
const round = keyframes`
|
// 定义规则提供者类型
|
||||||
from { transform: rotate(0deg); }
|
interface RuleProviderItem {
|
||||||
to { transform: rotate(360deg); }
|
behavior: string;
|
||||||
`;
|
ruleCount: number;
|
||||||
|
updatedAt: number;
|
||||||
|
vehicleType: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const ProviderButton = () => {
|
// 辅助组件 - 类型框
|
||||||
const { t } = useTranslation();
|
const TypeBox = styled(Box)<{ component?: React.ElementType }>(({ theme }) => ({
|
||||||
const { data } = useSWR("getRuleProviders", getRuleProviders);
|
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
|
|
||||||
const hasProvider = Object.keys(data || {}).length > 0;
|
|
||||||
const [updating, setUpdating] = useState(
|
|
||||||
Object.keys(data || {}).map(() => false),
|
|
||||||
);
|
|
||||||
|
|
||||||
const setUpdatingAt = (status: boolean, index: number) => {
|
|
||||||
setUpdating((prev) => {
|
|
||||||
const next = [...prev];
|
|
||||||
next[index] = status;
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const handleUpdate = async (key: string, index: number) => {
|
|
||||||
setUpdatingAt(true, index);
|
|
||||||
ruleProviderUpdate(key)
|
|
||||||
.then(async () => {
|
|
||||||
setUpdatingAt(false, index);
|
|
||||||
await mutate("getRules");
|
|
||||||
await mutate("getRuleProviders");
|
|
||||||
})
|
|
||||||
.catch(async () => {
|
|
||||||
setUpdatingAt(false, index);
|
|
||||||
await mutate("getRules");
|
|
||||||
await mutate("getRuleProviders");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!hasProvider) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
variant="outlined"
|
|
||||||
sx={{ textTransform: "capitalize" }}
|
|
||||||
onClick={() => setOpen(true)}
|
|
||||||
>
|
|
||||||
{t("Rule Provider")}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<BaseDialog
|
|
||||||
open={open}
|
|
||||||
title={
|
|
||||||
<Box display="flex" justifyContent="space-between" gap={1}>
|
|
||||||
<Typography variant="h6">{t("Rule Provider")}</Typography>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
size="small"
|
|
||||||
onClick={async () => {
|
|
||||||
Object.entries(data || {}).forEach(async ([key], index) => {
|
|
||||||
await handleUpdate(key, index);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("Update All")}
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
}
|
|
||||||
contentSx={{ width: 400 }}
|
|
||||||
disableOk
|
|
||||||
cancelBtn={t("Close")}
|
|
||||||
onClose={() => setOpen(false)}
|
|
||||||
onCancel={() => setOpen(false)}
|
|
||||||
>
|
|
||||||
<List sx={{ py: 0, minHeight: 250 }}>
|
|
||||||
{Object.entries(data || {}).map(([key, item], index) => {
|
|
||||||
const time = dayjs(item.updatedAt);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ListItem
|
|
||||||
sx={{
|
|
||||||
p: 0,
|
|
||||||
borderRadius: "10px",
|
|
||||||
border: "solid 2px var(--divider-color)",
|
|
||||||
mb: 1,
|
|
||||||
}}
|
|
||||||
key={key}
|
|
||||||
>
|
|
||||||
<ListItemText
|
|
||||||
sx={{ px: 1 }}
|
|
||||||
primary={
|
|
||||||
<>
|
|
||||||
<Typography
|
|
||||||
variant="h6"
|
|
||||||
component="span"
|
|
||||||
noWrap
|
|
||||||
title={key}
|
|
||||||
>
|
|
||||||
{key}
|
|
||||||
</Typography>
|
|
||||||
<TypeBox component="span" sx={{ marginLeft: "8px" }}>
|
|
||||||
{item.ruleCount}
|
|
||||||
</TypeBox>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
secondary={
|
|
||||||
<>
|
|
||||||
<StyledTypeBox component="span">
|
|
||||||
{item.vehicleType}
|
|
||||||
</StyledTypeBox>
|
|
||||||
<StyledTypeBox component="span">
|
|
||||||
{item.behavior}
|
|
||||||
</StyledTypeBox>
|
|
||||||
<StyledTypeBox component="span">
|
|
||||||
{t("Update At")} {time.fromNow()}
|
|
||||||
</StyledTypeBox>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Divider orientation="vertical" flexItem />
|
|
||||||
<IconButton
|
|
||||||
size="small"
|
|
||||||
color="inherit"
|
|
||||||
title={`${t("Update")}${t("Rule Provider")}`}
|
|
||||||
onClick={() => handleUpdate(key, index)}
|
|
||||||
sx={{
|
|
||||||
animation: updating[index]
|
|
||||||
? `1s linear infinite ${round}`
|
|
||||||
: "none",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<RefreshRounded />
|
|
||||||
</IconButton>
|
|
||||||
</ListItem>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</List>
|
|
||||||
</BaseDialog>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
const TypeBox = styled(Box, {
|
|
||||||
shouldForwardProp: (prop) => prop !== "component",
|
|
||||||
})<{ component?: React.ElementType }>(({ theme }) => ({
|
|
||||||
display: "inline-block",
|
display: "inline-block",
|
||||||
border: "1px solid #ccc",
|
border: "1px solid #ccc",
|
||||||
borderColor: alpha(theme.palette.secondary.main, 0.5),
|
borderColor: alpha(theme.palette.secondary.main, 0.5),
|
||||||
@ -176,16 +45,222 @@ const TypeBox = styled(Box, {
|
|||||||
lineHeight: 1.25,
|
lineHeight: 1.25,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledTypeBox = styled(Box, {
|
export const ProviderButton = () => {
|
||||||
shouldForwardProp: (prop) => prop !== "component",
|
const { t } = useTranslation();
|
||||||
})<{ component?: React.ElementType }>(({ theme }) => ({
|
const theme = useTheme();
|
||||||
display: "inline-block",
|
const [open, setOpen] = useState(false);
|
||||||
border: "1px solid #ccc",
|
const { ruleProviders, refreshRules, refreshRuleProviders } = useAppData();
|
||||||
borderColor: alpha(theme.palette.primary.main, 0.5),
|
const [updating, setUpdating] = useState<Record<string, boolean>>({});
|
||||||
color: alpha(theme.palette.primary.main, 0.8),
|
|
||||||
borderRadius: 4,
|
// 检查是否有提供者
|
||||||
fontSize: 10,
|
const hasProviders = Object.keys(ruleProviders || {}).length > 0;
|
||||||
marginRight: "4px",
|
|
||||||
padding: "0 2px",
|
// 更新单个规则提供者
|
||||||
lineHeight: 1.25,
|
const updateProvider = useLockFn(async (name: string) => {
|
||||||
}));
|
try {
|
||||||
|
// 设置更新状态
|
||||||
|
setUpdating(prev => ({ ...prev, [name]: true }));
|
||||||
|
|
||||||
|
await ruleProviderUpdate(name);
|
||||||
|
|
||||||
|
// 刷新数据
|
||||||
|
await refreshRules();
|
||||||
|
await refreshRuleProviders();
|
||||||
|
|
||||||
|
Notice.success(`${name} 更新成功`);
|
||||||
|
} catch (err: any) {
|
||||||
|
Notice.error(`${name} 更新失败: ${err?.message || err.toString()}`);
|
||||||
|
} finally {
|
||||||
|
// 清除更新状态
|
||||||
|
setUpdating(prev => ({ ...prev, [name]: false }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新所有规则提供者
|
||||||
|
const updateAllProviders = useLockFn(async () => {
|
||||||
|
try {
|
||||||
|
// 获取所有provider的名称
|
||||||
|
const allProviders = Object.keys(ruleProviders || {});
|
||||||
|
if (allProviders.length === 0) {
|
||||||
|
Notice.info("没有可更新的规则提供者");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置所有provider为更新中状态
|
||||||
|
const newUpdating = allProviders.reduce((acc, key) => {
|
||||||
|
acc[key] = true;
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, boolean>);
|
||||||
|
setUpdating(newUpdating);
|
||||||
|
|
||||||
|
// 改为串行逐个更新所有provider
|
||||||
|
for (const name of allProviders) {
|
||||||
|
try {
|
||||||
|
await ruleProviderUpdate(name);
|
||||||
|
// 每个更新完成后更新状态
|
||||||
|
setUpdating(prev => ({ ...prev, [name]: false }));
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`更新 ${name} 失败`, err);
|
||||||
|
// 继续执行下一个,不中断整体流程
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新数据
|
||||||
|
await refreshRules();
|
||||||
|
await refreshRuleProviders();
|
||||||
|
|
||||||
|
Notice.success("全部规则提供者更新成功");
|
||||||
|
} catch (err: any) {
|
||||||
|
Notice.error(`更新失败: ${err?.message || err.toString()}`);
|
||||||
|
} finally {
|
||||||
|
// 清除所有更新状态
|
||||||
|
setUpdating({});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!hasProviders) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
startIcon={<StorageOutlined />}
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
>
|
||||||
|
{t("Rule Provider")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
onClose={handleClose}
|
||||||
|
maxWidth="sm"
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
<DialogTitle>
|
||||||
|
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||||
|
<Typography variant="h6">{t("Rule Providers")}</Typography>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
size="small"
|
||||||
|
onClick={updateAllProviders}
|
||||||
|
>
|
||||||
|
{t("Update All")}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</DialogTitle>
|
||||||
|
|
||||||
|
<DialogContent>
|
||||||
|
<List sx={{ py: 0, minHeight: 250 }}>
|
||||||
|
{Object.entries(ruleProviders || {}).map(([key, item]) => {
|
||||||
|
const provider = item as RuleProviderItem;
|
||||||
|
const time = dayjs(provider.updatedAt);
|
||||||
|
const isUpdating = updating[key];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItem
|
||||||
|
key={key}
|
||||||
|
sx={[
|
||||||
|
{
|
||||||
|
p: 0,
|
||||||
|
mb: "8px",
|
||||||
|
borderRadius: 2,
|
||||||
|
overflow: "hidden",
|
||||||
|
transition: "all 0.2s"
|
||||||
|
},
|
||||||
|
({ palette: { mode, primary } }) => {
|
||||||
|
const bgcolor = mode === "light" ? "#ffffff" : "#24252f";
|
||||||
|
const hoverColor = mode === "light"
|
||||||
|
? alpha(primary.main, 0.1)
|
||||||
|
: alpha(primary.main, 0.2);
|
||||||
|
|
||||||
|
return {
|
||||||
|
backgroundColor: bgcolor,
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: hoverColor,
|
||||||
|
borderColor: alpha(primary.main, 0.3)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<ListItemText
|
||||||
|
sx={{ px: 2, py: 1 }}
|
||||||
|
primary={
|
||||||
|
<Box sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
}}>
|
||||||
|
<Typography
|
||||||
|
variant="subtitle1"
|
||||||
|
component="div"
|
||||||
|
noWrap
|
||||||
|
title={key}
|
||||||
|
sx={{ display: "flex", alignItems: "center" }}
|
||||||
|
>
|
||||||
|
<span style={{ marginRight: "8px" }}>{key}</span>
|
||||||
|
<TypeBox component="span">
|
||||||
|
{provider.ruleCount}
|
||||||
|
</TypeBox>
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography variant="body2" color="text.secondary" noWrap>
|
||||||
|
<small>{t("Update At")}: </small>{time.fromNow()}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
secondary={
|
||||||
|
<Box sx={{ display: "flex" }}>
|
||||||
|
<TypeBox component="span">
|
||||||
|
{provider.vehicleType}
|
||||||
|
</TypeBox>
|
||||||
|
<TypeBox component="span">
|
||||||
|
{provider.behavior}
|
||||||
|
</TypeBox>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Divider orientation="vertical" flexItem />
|
||||||
|
<Box sx={{
|
||||||
|
width: 40,
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center"
|
||||||
|
}}>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => updateProvider(key)}
|
||||||
|
disabled={isUpdating}
|
||||||
|
sx={{
|
||||||
|
animation: isUpdating ? "spin 1s linear infinite" : "none",
|
||||||
|
"@keyframes spin": {
|
||||||
|
"0%": { transform: "rotate(0deg)" },
|
||||||
|
"100%": { transform: "rotate(360deg)" }
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title={t("Update Provider") as string}
|
||||||
|
>
|
||||||
|
<RefreshRounded />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</List>
|
||||||
|
</DialogContent>
|
||||||
|
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={handleClose} variant="outlined">
|
||||||
|
{t("Close")}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -57,21 +57,18 @@ const DEFAULT_DNS_CONFIG = {
|
|||||||
"*.msftncsi.com",
|
"*.msftncsi.com",
|
||||||
"www.msftconnecttest.com",
|
"www.msftconnecttest.com",
|
||||||
],
|
],
|
||||||
"default-nameserver": ["223.6.6.6", "8.8.8.8"],
|
"default-nameserver": ["system", "223.6.6.6", "8.8.8.8"],
|
||||||
nameserver: [
|
nameserver: [
|
||||||
"8.8.8.8",
|
"8.8.8.8",
|
||||||
"https://doh.pub/dns-query",
|
"https://doh.pub/dns-query",
|
||||||
"https://dns.alidns.com/dns-query",
|
"https://dns.alidns.com/dns-query",
|
||||||
],
|
],
|
||||||
fallback: [
|
fallback: [],
|
||||||
"https://dns.alidns.com/dns-query",
|
|
||||||
"https://dns.google/dns-query",
|
|
||||||
"https://cloudflare-dns.com/dns-query",
|
|
||||||
],
|
|
||||||
"nameserver-policy": {},
|
"nameserver-policy": {},
|
||||||
"proxy-server-nameserver": [
|
"proxy-server-nameserver": [
|
||||||
"https://doh.pub/dns-query",
|
"https://doh.pub/dns-query",
|
||||||
"https://dns.alidns.com/dns-query",
|
"https://dns.alidns.com/dns-query",
|
||||||
|
"tls://223.5.5.5"
|
||||||
],
|
],
|
||||||
"direct-nameserver": [],
|
"direct-nameserver": [],
|
||||||
"direct-nameserver-follow-policy": false,
|
"direct-nameserver-follow-policy": false,
|
||||||
@ -98,12 +95,12 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
fakeIpFilterMode: "blacklist" | "whitelist";
|
fakeIpFilterMode: "blacklist" | "whitelist";
|
||||||
preferH3: boolean;
|
preferH3: boolean;
|
||||||
respectRules: boolean;
|
respectRules: boolean;
|
||||||
|
useHosts: boolean;
|
||||||
|
useSystemHosts: boolean;
|
||||||
fakeIpFilter: string;
|
fakeIpFilter: string;
|
||||||
nameserver: string;
|
nameserver: string;
|
||||||
fallback: string;
|
fallback: string;
|
||||||
defaultNameserver: string;
|
defaultNameserver: string;
|
||||||
useHosts: boolean;
|
|
||||||
useSystemHosts: boolean;
|
|
||||||
proxyServerNameserver: string;
|
proxyServerNameserver: string;
|
||||||
directNameserver: string;
|
directNameserver: string;
|
||||||
directNameserverFollowPolicy: boolean;
|
directNameserverFollowPolicy: boolean;
|
||||||
@ -120,12 +117,12 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
fakeIpFilterMode: DEFAULT_DNS_CONFIG["fake-ip-filter-mode"],
|
fakeIpFilterMode: DEFAULT_DNS_CONFIG["fake-ip-filter-mode"],
|
||||||
preferH3: DEFAULT_DNS_CONFIG["prefer-h3"],
|
preferH3: DEFAULT_DNS_CONFIG["prefer-h3"],
|
||||||
respectRules: DEFAULT_DNS_CONFIG["respect-rules"],
|
respectRules: DEFAULT_DNS_CONFIG["respect-rules"],
|
||||||
|
useHosts: DEFAULT_DNS_CONFIG["use-hosts"],
|
||||||
|
useSystemHosts: DEFAULT_DNS_CONFIG["use-system-hosts"],
|
||||||
fakeIpFilter: DEFAULT_DNS_CONFIG["fake-ip-filter"].join(", "),
|
fakeIpFilter: DEFAULT_DNS_CONFIG["fake-ip-filter"].join(", "),
|
||||||
defaultNameserver: DEFAULT_DNS_CONFIG["default-nameserver"].join(", "),
|
defaultNameserver: DEFAULT_DNS_CONFIG["default-nameserver"].join(", "),
|
||||||
nameserver: DEFAULT_DNS_CONFIG.nameserver.join(", "),
|
nameserver: DEFAULT_DNS_CONFIG.nameserver.join(", "),
|
||||||
fallback: DEFAULT_DNS_CONFIG.fallback.join(", "),
|
fallback: DEFAULT_DNS_CONFIG.fallback.join(", "),
|
||||||
useHosts: DEFAULT_DNS_CONFIG["use-hosts"],
|
|
||||||
useSystemHosts: DEFAULT_DNS_CONFIG["use-system-hosts"],
|
|
||||||
proxyServerNameserver:
|
proxyServerNameserver:
|
||||||
DEFAULT_DNS_CONFIG["proxy-server-nameserver"]?.join(", ") || "",
|
DEFAULT_DNS_CONFIG["proxy-server-nameserver"]?.join(", ") || "",
|
||||||
directNameserver: DEFAULT_DNS_CONFIG["direct-nameserver"]?.join(", ") || "",
|
directNameserver: DEFAULT_DNS_CONFIG["direct-nameserver"]?.join(", ") || "",
|
||||||
@ -209,6 +206,9 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
preferH3: config["prefer-h3"] ?? DEFAULT_DNS_CONFIG["prefer-h3"],
|
preferH3: config["prefer-h3"] ?? DEFAULT_DNS_CONFIG["prefer-h3"],
|
||||||
respectRules:
|
respectRules:
|
||||||
config["respect-rules"] ?? DEFAULT_DNS_CONFIG["respect-rules"],
|
config["respect-rules"] ?? DEFAULT_DNS_CONFIG["respect-rules"],
|
||||||
|
useHosts: config["use-hosts"] ?? DEFAULT_DNS_CONFIG["use-hosts"],
|
||||||
|
useSystemHosts:
|
||||||
|
config["use-system-hosts"] ?? DEFAULT_DNS_CONFIG["use-system-hosts"],
|
||||||
fakeIpFilter:
|
fakeIpFilter:
|
||||||
config["fake-ip-filter"]?.join(", ") ??
|
config["fake-ip-filter"]?.join(", ") ??
|
||||||
DEFAULT_DNS_CONFIG["fake-ip-filter"].join(", "),
|
DEFAULT_DNS_CONFIG["fake-ip-filter"].join(", "),
|
||||||
@ -220,9 +220,6 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
defaultNameserver:
|
defaultNameserver:
|
||||||
config["default-nameserver"]?.join(", ") ??
|
config["default-nameserver"]?.join(", ") ??
|
||||||
DEFAULT_DNS_CONFIG["default-nameserver"].join(", "),
|
DEFAULT_DNS_CONFIG["default-nameserver"].join(", "),
|
||||||
useHosts: config["use-hosts"] ?? DEFAULT_DNS_CONFIG["use-hosts"],
|
|
||||||
useSystemHosts:
|
|
||||||
config["use-system-hosts"] ?? DEFAULT_DNS_CONFIG["use-system-hosts"],
|
|
||||||
proxyServerNameserver:
|
proxyServerNameserver:
|
||||||
config["proxy-server-nameserver"]?.join(", ") ??
|
config["proxy-server-nameserver"]?.join(", ") ??
|
||||||
(DEFAULT_DNS_CONFIG["proxy-server-nameserver"]?.join(", ") || ""),
|
(DEFAULT_DNS_CONFIG["proxy-server-nameserver"]?.join(", ") || ""),
|
||||||
@ -259,12 +256,12 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
fakeIpFilterMode: DEFAULT_DNS_CONFIG["fake-ip-filter-mode"],
|
fakeIpFilterMode: DEFAULT_DNS_CONFIG["fake-ip-filter-mode"],
|
||||||
preferH3: DEFAULT_DNS_CONFIG["prefer-h3"],
|
preferH3: DEFAULT_DNS_CONFIG["prefer-h3"],
|
||||||
respectRules: DEFAULT_DNS_CONFIG["respect-rules"],
|
respectRules: DEFAULT_DNS_CONFIG["respect-rules"],
|
||||||
|
useHosts: DEFAULT_DNS_CONFIG["use-hosts"],
|
||||||
|
useSystemHosts: DEFAULT_DNS_CONFIG["use-system-hosts"],
|
||||||
fakeIpFilter: DEFAULT_DNS_CONFIG["fake-ip-filter"].join(", "),
|
fakeIpFilter: DEFAULT_DNS_CONFIG["fake-ip-filter"].join(", "),
|
||||||
defaultNameserver: DEFAULT_DNS_CONFIG["default-nameserver"].join(", "),
|
defaultNameserver: DEFAULT_DNS_CONFIG["default-nameserver"].join(", "),
|
||||||
nameserver: DEFAULT_DNS_CONFIG.nameserver.join(", "),
|
nameserver: DEFAULT_DNS_CONFIG.nameserver.join(", "),
|
||||||
fallback: DEFAULT_DNS_CONFIG.fallback.join(", "),
|
fallback: DEFAULT_DNS_CONFIG.fallback.join(", "),
|
||||||
useHosts: DEFAULT_DNS_CONFIG["use-hosts"],
|
|
||||||
useSystemHosts: DEFAULT_DNS_CONFIG["use-system-hosts"],
|
|
||||||
proxyServerNameserver:
|
proxyServerNameserver:
|
||||||
DEFAULT_DNS_CONFIG["proxy-server-nameserver"]?.join(", ") || "",
|
DEFAULT_DNS_CONFIG["proxy-server-nameserver"]?.join(", ") || "",
|
||||||
directNameserver:
|
directNameserver:
|
||||||
@ -330,6 +327,10 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
preferH3: dnsConfig["prefer-h3"] ?? DEFAULT_DNS_CONFIG["prefer-h3"],
|
preferH3: dnsConfig["prefer-h3"] ?? DEFAULT_DNS_CONFIG["prefer-h3"],
|
||||||
respectRules:
|
respectRules:
|
||||||
dnsConfig["respect-rules"] ?? DEFAULT_DNS_CONFIG["respect-rules"],
|
dnsConfig["respect-rules"] ?? DEFAULT_DNS_CONFIG["respect-rules"],
|
||||||
|
useHosts: dnsConfig["use-hosts"] ?? DEFAULT_DNS_CONFIG["use-hosts"],
|
||||||
|
useSystemHosts:
|
||||||
|
dnsConfig["use-system-hosts"] ??
|
||||||
|
DEFAULT_DNS_CONFIG["use-system-hosts"],
|
||||||
fakeIpFilter:
|
fakeIpFilter:
|
||||||
dnsConfig["fake-ip-filter"]?.join(", ") ??
|
dnsConfig["fake-ip-filter"]?.join(", ") ??
|
||||||
DEFAULT_DNS_CONFIG["fake-ip-filter"].join(", "),
|
DEFAULT_DNS_CONFIG["fake-ip-filter"].join(", "),
|
||||||
@ -342,10 +343,6 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
fallback:
|
fallback:
|
||||||
dnsConfig.fallback?.join(", ") ??
|
dnsConfig.fallback?.join(", ") ??
|
||||||
DEFAULT_DNS_CONFIG.fallback.join(", "),
|
DEFAULT_DNS_CONFIG.fallback.join(", "),
|
||||||
useHosts: dnsConfig["use-hosts"] ?? DEFAULT_DNS_CONFIG["use-hosts"],
|
|
||||||
useSystemHosts:
|
|
||||||
dnsConfig["use-system-hosts"] ??
|
|
||||||
DEFAULT_DNS_CONFIG["use-system-hosts"],
|
|
||||||
proxyServerNameserver:
|
proxyServerNameserver:
|
||||||
dnsConfig["proxy-server-nameserver"]?.join(", ") ??
|
dnsConfig["proxy-server-nameserver"]?.join(", ") ??
|
||||||
(DEFAULT_DNS_CONFIG["proxy-server-nameserver"]?.join(", ") || ""),
|
(DEFAULT_DNS_CONFIG["proxy-server-nameserver"]?.join(", ") || ""),
|
||||||
@ -458,12 +455,12 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
"fake-ip-filter-mode": values.fakeIpFilterMode,
|
"fake-ip-filter-mode": values.fakeIpFilterMode,
|
||||||
"prefer-h3": values.preferH3,
|
"prefer-h3": values.preferH3,
|
||||||
"respect-rules": values.respectRules,
|
"respect-rules": values.respectRules,
|
||||||
|
"use-hosts": values.useHosts,
|
||||||
|
"use-system-hosts": values.useSystemHosts,
|
||||||
"fake-ip-filter": parseList(values.fakeIpFilter),
|
"fake-ip-filter": parseList(values.fakeIpFilter),
|
||||||
"default-nameserver": parseList(values.defaultNameserver),
|
"default-nameserver": parseList(values.defaultNameserver),
|
||||||
nameserver: parseList(values.nameserver),
|
nameserver: parseList(values.nameserver),
|
||||||
fallback: parseList(values.fallback),
|
"direct-nameserver-follow-policy": values.directNameserverFollowPolicy,
|
||||||
"use-hosts": values.useHosts,
|
|
||||||
"use-system-hosts": values.useSystemHosts,
|
|
||||||
"fallback-filter": {
|
"fallback-filter": {
|
||||||
geoip: values.fallbackGeoip,
|
geoip: values.fallbackGeoip,
|
||||||
"geoip-code": values.fallbackGeoipCode,
|
"geoip-code": values.fallbackGeoipCode,
|
||||||
@ -472,13 +469,16 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// 只在有nameserverPolicy时添加
|
// 只在有值时添加其他可选字段
|
||||||
|
if (values.fallback) {
|
||||||
|
dnsConfig["fallback"] = parseList(values.fallback);
|
||||||
|
}
|
||||||
|
|
||||||
const policy = parseNameserverPolicy(values.nameserverPolicy);
|
const policy = parseNameserverPolicy(values.nameserverPolicy);
|
||||||
if (Object.keys(policy).length > 0) {
|
if (Object.keys(policy).length > 0) {
|
||||||
dnsConfig["nameserver-policy"] = policy;
|
dnsConfig["nameserver-policy"] = policy;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 只在有值时添加其他可选字段
|
|
||||||
if (values.proxyServerNameserver) {
|
if (values.proxyServerNameserver) {
|
||||||
dnsConfig["proxy-server-nameserver"] = parseList(
|
dnsConfig["proxy-server-nameserver"] = parseList(
|
||||||
values.proxyServerNameserver,
|
values.proxyServerNameserver,
|
||||||
@ -489,9 +489,6 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
dnsConfig["direct-nameserver"] = parseList(values.directNameserver);
|
dnsConfig["direct-nameserver"] = parseList(values.directNameserver);
|
||||||
}
|
}
|
||||||
|
|
||||||
dnsConfig["direct-nameserver-follow-policy"] =
|
|
||||||
values.directNameserverFollowPolicy;
|
|
||||||
|
|
||||||
return dnsConfig;
|
return dnsConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -576,7 +573,7 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
open={open}
|
open={open}
|
||||||
title={
|
title={
|
||||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||||
{t("DNS Settings")}
|
{t("DNS Overwrite")}
|
||||||
<Box display="flex" alignItems="center" gap={1}>
|
<Box display="flex" alignItems="center" gap={1}>
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@ -755,7 +752,7 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
size="small"
|
size="small"
|
||||||
value={values.defaultNameserver}
|
value={values.defaultNameserver}
|
||||||
onChange={handleChange("defaultNameserver")}
|
onChange={handleChange("defaultNameserver")}
|
||||||
placeholder="223.6.6.6, 8.8.8.8"
|
placeholder="system,223.6.6.6, 8.8.8.8"
|
||||||
/>
|
/>
|
||||||
</Item>
|
</Item>
|
||||||
|
|
||||||
|
@ -5,7 +5,6 @@ import {
|
|||||||
SettingsRounded,
|
SettingsRounded,
|
||||||
ShuffleRounded,
|
ShuffleRounded,
|
||||||
LanRounded,
|
LanRounded,
|
||||||
DnsRounded,
|
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import { DialogRef, Notice, Switch } from "@/components/base";
|
import { DialogRef, Notice, Switch } from "@/components/base";
|
||||||
import { useClash } from "@/hooks/use-clash";
|
import { useClash } from "@/hooks/use-clash";
|
||||||
@ -49,7 +48,16 @@ const SettingClash = ({ onError }: Props) => {
|
|||||||
const { enable_random_port = false, verge_mixed_port } = verge ?? {};
|
const { enable_random_port = false, verge_mixed_port } = verge ?? {};
|
||||||
|
|
||||||
// 独立跟踪DNS设置开关状态
|
// 独立跟踪DNS设置开关状态
|
||||||
const [dnsSettingsEnabled, setDnsSettingsEnabled] = useState(false);
|
const [dnsSettingsEnabled, setDnsSettingsEnabled] = useState(() => {
|
||||||
|
// 尝试从localStorage获取之前保存的状态
|
||||||
|
const savedState = localStorage.getItem("dns_settings_enabled");
|
||||||
|
if (savedState !== null) {
|
||||||
|
return savedState === "true";
|
||||||
|
}
|
||||||
|
// 如果没有保存的状态,则从verge配置中获取
|
||||||
|
return verge?.enable_dns_settings ?? false;
|
||||||
|
});
|
||||||
|
|
||||||
const { addListener } = useListen();
|
const { addListener } = useListen();
|
||||||
|
|
||||||
const webRef = useRef<DialogRef>(null);
|
const webRef = useRef<DialogRef>(null);
|
||||||
@ -59,12 +67,6 @@ const SettingClash = ({ onError }: Props) => {
|
|||||||
const networkRef = useRef<DialogRef>(null);
|
const networkRef = useRef<DialogRef>(null);
|
||||||
const dnsRef = useRef<DialogRef>(null);
|
const dnsRef = useRef<DialogRef>(null);
|
||||||
|
|
||||||
// 初始化时从verge配置中加载DNS设置开关状态
|
|
||||||
useEffect(() => {
|
|
||||||
const dnsSettingsState = verge?.enable_dns_settings ?? false;
|
|
||||||
setDnsSettingsEnabled(dnsSettingsState);
|
|
||||||
}, [verge]);
|
|
||||||
|
|
||||||
const onSwitchFormat = (_e: any, value: boolean) => value;
|
const onSwitchFormat = (_e: any, value: boolean) => value;
|
||||||
const onChangeData = (patch: Partial<IConfigData>) => {
|
const onChangeData = (patch: Partial<IConfigData>) => {
|
||||||
mutateClash((old) => ({ ...(old! || {}), ...patch }), false);
|
mutateClash((old) => ({ ...(old! || {}), ...patch }), false);
|
||||||
@ -84,15 +86,21 @@ const SettingClash = ({ onError }: Props) => {
|
|||||||
// 实现DNS设置开关处理函数
|
// 实现DNS设置开关处理函数
|
||||||
const handleDnsToggle = useLockFn(async (enable: boolean) => {
|
const handleDnsToggle = useLockFn(async (enable: boolean) => {
|
||||||
try {
|
try {
|
||||||
|
// 立即更新UI状态
|
||||||
setDnsSettingsEnabled(enable);
|
setDnsSettingsEnabled(enable);
|
||||||
|
// 保存到localStorage,用于记住用户的选择
|
||||||
|
localStorage.setItem("dns_settings_enabled", String(enable));
|
||||||
|
// 更新verge配置
|
||||||
await patchVerge({ enable_dns_settings: enable });
|
await patchVerge({ enable_dns_settings: enable });
|
||||||
await invoke("apply_dns_config", { apply: enable });
|
await invoke("apply_dns_config", { apply: enable });
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
mutateClash();
|
mutateClash();
|
||||||
}, 500); // 延迟500ms确保后端完成处理
|
}, 500); // 延迟500ms确保后端完成处理
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
Notice.error(err.message || err.toString());
|
// 如果出错,恢复原始状态
|
||||||
setDnsSettingsEnabled(!enable);
|
setDnsSettingsEnabled(!enable);
|
||||||
|
localStorage.setItem("dns_settings_enabled", String(!enable));
|
||||||
|
Notice.error(err.message || err.toString());
|
||||||
await patchVerge({ enable_dns_settings: !enable }).catch(() => {
|
await patchVerge({ enable_dns_settings: !enable }).catch(() => {
|
||||||
// 忽略恢复状态时的错误
|
// 忽略恢复状态时的错误
|
||||||
});
|
});
|
||||||
@ -143,7 +151,6 @@ const SettingClash = ({ onError }: Props) => {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{/* 使用独立状态,不再依赖dns?.enable */}
|
|
||||||
<Switch
|
<Switch
|
||||||
edge="end"
|
edge="end"
|
||||||
checked={dnsSettingsEnabled}
|
checked={dnsSettingsEnabled}
|
||||||
|
@ -18,12 +18,12 @@ import { TooltipIcon } from "@/components/base/base-tooltip-icon";
|
|||||||
import {
|
import {
|
||||||
getSystemProxy,
|
getSystemProxy,
|
||||||
getAutotemProxy,
|
getAutotemProxy,
|
||||||
getRunningMode,
|
|
||||||
installService,
|
installService,
|
||||||
getAutoLaunchStatus,
|
getAutoLaunchStatus,
|
||||||
} from "@/services/cmds";
|
} from "@/services/cmds";
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from "ahooks";
|
||||||
import { Box, Button, Tooltip } from "@mui/material";
|
import { Button, Tooltip } from "@mui/material";
|
||||||
|
import { useSystemState } from "@/hooks/use-system-state";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onError?: (err: Error) => void;
|
onError?: (err: Error) => void;
|
||||||
@ -36,23 +36,29 @@ const SettingSystem = ({ onError }: Props) => {
|
|||||||
|
|
||||||
const { data: sysproxy } = useSWR("getSystemProxy", getSystemProxy);
|
const { data: sysproxy } = useSWR("getSystemProxy", getSystemProxy);
|
||||||
const { data: autoproxy } = useSWR("getAutotemProxy", getAutotemProxy);
|
const { data: autoproxy } = useSWR("getAutotemProxy", getAutotemProxy);
|
||||||
const { data: runningMode, mutate: mutateRunningMode } = useSWR(
|
const { data: autoLaunchEnabled } = useSWR(
|
||||||
"getRunningMode",
|
"getAutoLaunchStatus",
|
||||||
getRunningMode,
|
getAutoLaunchStatus,
|
||||||
|
{ revalidateOnFocus: false }
|
||||||
);
|
);
|
||||||
const { data: autoLaunchEnabled } = useSWR("getAutoLaunchStatus", getAutoLaunchStatus);
|
|
||||||
|
const { isAdminMode, isSidecarMode, mutateRunningMode } = useSystemState();
|
||||||
|
|
||||||
|
// 判断Tun模式是否可用 - 当处于服务模式或管理员模式时可用
|
||||||
|
const isTunAvailable = !isSidecarMode || isAdminMode;
|
||||||
|
|
||||||
// 当实际自启动状态与配置不同步时更新配置
|
// 当实际自启动状态与配置不同步时更新配置
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (autoLaunchEnabled !== undefined && verge && verge.enable_auto_launch !== autoLaunchEnabled) {
|
if (
|
||||||
|
autoLaunchEnabled !== undefined &&
|
||||||
|
verge &&
|
||||||
|
verge.enable_auto_launch !== autoLaunchEnabled
|
||||||
|
) {
|
||||||
// 静默更新配置,不触发UI刷新
|
// 静默更新配置,不触发UI刷新
|
||||||
mutateVerge({ ...verge, enable_auto_launch: autoLaunchEnabled }, false);
|
mutateVerge({ ...verge, enable_auto_launch: autoLaunchEnabled }, false);
|
||||||
}
|
}
|
||||||
}, [autoLaunchEnabled]);
|
}, [autoLaunchEnabled]);
|
||||||
|
|
||||||
// 是否以sidecar模式运行
|
|
||||||
const isSidecarMode = runningMode === "sidecar";
|
|
||||||
|
|
||||||
const sysproxyRef = useRef<DialogRef>(null);
|
const sysproxyRef = useRef<DialogRef>(null);
|
||||||
const tunRef = useRef<DialogRef>(null);
|
const tunRef = useRef<DialogRef>(null);
|
||||||
|
|
||||||
@ -103,12 +109,12 @@ const SettingSystem = ({ onError }: Props) => {
|
|||||||
icon={SettingsRounded}
|
icon={SettingsRounded}
|
||||||
onClick={() => tunRef.current?.open()}
|
onClick={() => tunRef.current?.open()}
|
||||||
/>
|
/>
|
||||||
{isSidecarMode && (
|
{isSidecarMode && !isAdminMode && (
|
||||||
<Tooltip title={t("TUN requires Service Mode")}>
|
<Tooltip title={t("TUN requires Service Mode")}>
|
||||||
<WarningRounded sx={{ color: "warning.main", mr: 1 }} />
|
<WarningRounded sx={{ color: "warning.main", mr: 1 }} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{isSidecarMode && (
|
{isSidecarMode && !isAdminMode && (
|
||||||
<Tooltip title={t("Install Service")}>
|
<Tooltip title={t("Install Service")}>
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@ -130,20 +136,20 @@ const SettingSystem = ({ onError }: Props) => {
|
|||||||
onCatch={onError}
|
onCatch={onError}
|
||||||
onFormat={onSwitchFormat}
|
onFormat={onSwitchFormat}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
// 当在sidecar模式下禁用切换
|
// 当在sidecar模式下且非管理员模式时禁用切换
|
||||||
if (isSidecarMode) return;
|
if (isSidecarMode && !isAdminMode) return;
|
||||||
onChangeData({ enable_tun_mode: e });
|
onChangeData({ enable_tun_mode: e });
|
||||||
}}
|
}}
|
||||||
onGuard={(e) => {
|
onGuard={(e) => {
|
||||||
// 当在sidecar模式下禁用切换
|
// 当在sidecar模式下且非管理员模式时禁用切换
|
||||||
if (isSidecarMode) {
|
if (isSidecarMode && !isAdminMode) {
|
||||||
Notice.error(t("TUN requires Service Mode"), 2000);
|
Notice.error(t("TUN requires Service Mode"), 2000);
|
||||||
return Promise.reject(new Error(t("TUN requires Service Mode")));
|
return Promise.reject(new Error(t("TUN requires Service Mode")));
|
||||||
}
|
}
|
||||||
return patchVerge({ enable_tun_mode: e });
|
return patchVerge({ enable_tun_mode: e });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Switch edge="end" disabled={isSidecarMode} />
|
<Switch edge="end" disabled={isSidecarMode && !isAdminMode} />
|
||||||
</GuardState>
|
</GuardState>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem
|
<SettingItem
|
||||||
@ -184,14 +190,32 @@ const SettingSystem = ({ onError }: Props) => {
|
|||||||
</GuardState>
|
</GuardState>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
|
|
||||||
<SettingItem label={t("Auto Launch")}>
|
<SettingItem
|
||||||
|
label={t("Auto Launch")}
|
||||||
|
extra={
|
||||||
|
isAdminMode && (
|
||||||
|
<Tooltip title={t("Administrator mode does not support auto launch")}>
|
||||||
|
<WarningRounded sx={{ color: "warning.main", mr: 1 }} />
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
<GuardState
|
<GuardState
|
||||||
value={enable_auto_launch ?? false}
|
value={enable_auto_launch ?? false}
|
||||||
valueProps="checked"
|
valueProps="checked"
|
||||||
onCatch={onError}
|
onCatch={onError}
|
||||||
onFormat={onSwitchFormat}
|
onFormat={onSwitchFormat}
|
||||||
onChange={(e) => onChangeData({ enable_auto_launch: e })}
|
onChange={(e) => {
|
||||||
|
// 在管理员模式下禁用更改
|
||||||
|
if (isAdminMode) return;
|
||||||
|
onChangeData({ enable_auto_launch: e });
|
||||||
|
}}
|
||||||
onGuard={async (e) => {
|
onGuard={async (e) => {
|
||||||
|
if (isAdminMode) {
|
||||||
|
Notice.error(t("Administrator mode does not support auto launch"), 2000);
|
||||||
|
return Promise.reject(new Error(t("Administrator mode does not support auto launch")));
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 在应用更改之前先触发UI更新,让用户立即看到反馈
|
// 在应用更改之前先触发UI更新,让用户立即看到反馈
|
||||||
onChangeData({ enable_auto_launch: e });
|
onChangeData({ enable_auto_launch: e });
|
||||||
@ -206,7 +230,7 @@ const SettingSystem = ({ onError }: Props) => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Switch edge="end" />
|
<Switch edge="end" disabled={isAdminMode} />
|
||||||
</GuardState>
|
</GuardState>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
|
|
||||||
|
@ -52,7 +52,7 @@ const ProxyControlSwitches = ({ label, onError }: ProxySwitchProps) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 是否以sidecar模式运行
|
// 是否以sidecar模式运行
|
||||||
const isSidecarMode = runningMode === "sidecar";
|
const isSidecarMode = runningMode === "Sidecar";
|
||||||
|
|
||||||
const sysproxyRef = useRef<DialogRef>(null);
|
const sysproxyRef = useRef<DialogRef>(null);
|
||||||
const tunRef = useRef<DialogRef>(null);
|
const tunRef = useRef<DialogRef>(null);
|
||||||
@ -138,7 +138,7 @@ const ProxyControlSwitches = ({ label, onError }: ProxySwitchProps) => {
|
|||||||
{t("System Proxy")}
|
{t("System Proxy")}
|
||||||
</Typography>
|
</Typography>
|
||||||
{/* <Typography variant="caption" color="text.secondary">
|
{/* <Typography variant="caption" color="text.secondary">
|
||||||
{sysproxy?.enable
|
{sysproxy?.enable
|
||||||
? t("Proxy is active")
|
? t("Proxy is active")
|
||||||
: t("Enable this for most users")
|
: t("Enable this for most users")
|
||||||
}
|
}
|
||||||
|
@ -1,32 +1,25 @@
|
|||||||
import useSWR from "swr";
|
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { getProxies } from "@/services/api";
|
import { useAppData } from "@/providers/app-data-provider";
|
||||||
import { getClashConfig } from "@/services/api";
|
|
||||||
|
// 定义代理组类型
|
||||||
|
interface ProxyGroup {
|
||||||
|
name: string;
|
||||||
|
now: string;
|
||||||
|
}
|
||||||
|
|
||||||
// 获取当前代理节点信息的自定义Hook
|
// 获取当前代理节点信息的自定义Hook
|
||||||
export const useCurrentProxy = () => {
|
export const useCurrentProxy = () => {
|
||||||
// 获取代理信息
|
// 从AppDataProvider获取数据
|
||||||
const { data: proxiesData, mutate: mutateProxies } = useSWR(
|
const { proxies, clashConfig, refreshProxy } = useAppData();
|
||||||
"getProxies",
|
|
||||||
getProxies,
|
|
||||||
{
|
|
||||||
refreshInterval: 2000,
|
|
||||||
revalidateOnFocus: false,
|
|
||||||
revalidateOnReconnect: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// 获取当前Clash配置(包含模式信息)
|
|
||||||
const { data: clashConfig } = useSWR("getClashConfig", getClashConfig);
|
|
||||||
|
|
||||||
// 获取当前模式
|
// 获取当前模式
|
||||||
const currentMode = clashConfig?.mode?.toLowerCase() || "rule";
|
const currentMode = clashConfig?.mode?.toLowerCase() || "rule";
|
||||||
|
|
||||||
// 获取当前代理节点信息
|
// 获取当前代理节点信息
|
||||||
const currentProxyInfo = useMemo(() => {
|
const currentProxyInfo = useMemo(() => {
|
||||||
if (!proxiesData) return { currentProxy: null, primaryGroupName: null };
|
if (!proxies) return { currentProxy: null, primaryGroupName: null };
|
||||||
|
|
||||||
const { global, groups, records } = proxiesData;
|
const { global, groups, records } = proxies;
|
||||||
|
|
||||||
// 默认信息
|
// 默认信息
|
||||||
let primaryGroupName = "GLOBAL";
|
let primaryGroupName = "GLOBAL";
|
||||||
@ -43,11 +36,11 @@ export const useCurrentProxy = () => {
|
|||||||
"自动选择",
|
"自动选择",
|
||||||
];
|
];
|
||||||
const primaryGroup =
|
const primaryGroup =
|
||||||
groups.find((group) =>
|
groups.find((group: ProxyGroup) =>
|
||||||
primaryKeywords.some((keyword) =>
|
primaryKeywords.some((keyword) =>
|
||||||
group.name.toLowerCase().includes(keyword.toLowerCase()),
|
group.name.toLowerCase().includes(keyword.toLowerCase()),
|
||||||
),
|
),
|
||||||
) || groups.filter((g) => g.name !== "GLOBAL")[0];
|
) || groups.filter((g: ProxyGroup) => g.name !== "GLOBAL")[0];
|
||||||
|
|
||||||
if (primaryGroup) {
|
if (primaryGroup) {
|
||||||
primaryGroupName = primaryGroup.name;
|
primaryGroupName = primaryGroup.name;
|
||||||
@ -71,12 +64,12 @@ export const useCurrentProxy = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return { currentProxy, primaryGroupName };
|
return { currentProxy, primaryGroupName };
|
||||||
}, [proxiesData, currentMode]);
|
}, [proxies, currentMode]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentProxy: currentProxyInfo.currentProxy,
|
currentProxy: currentProxyInfo.currentProxy,
|
||||||
primaryGroupName: currentProxyInfo.primaryGroupName,
|
primaryGroupName: currentProxyInfo.primaryGroupName,
|
||||||
mode: currentMode,
|
mode: currentMode,
|
||||||
refreshProxy: mutateProxies,
|
refreshProxy,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
29
src/hooks/use-system-state.ts
Normal file
29
src/hooks/use-system-state.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import useSWR from "swr";
|
||||||
|
import { getRunningMode, isAdmin } from "@/services/cmds";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义 hook 用于获取系统运行状态
|
||||||
|
* 包括运行模式和管理员状态
|
||||||
|
*/
|
||||||
|
export function useSystemState() {
|
||||||
|
// 获取运行模式
|
||||||
|
const { data: runningMode = "Sidecar", mutate: mutateRunningMode } =
|
||||||
|
useSWR("getRunningMode", getRunningMode, {
|
||||||
|
suspense: false,
|
||||||
|
revalidateOnFocus: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取管理员状态
|
||||||
|
const { data: isAdminMode = false } =
|
||||||
|
useSWR("isAdmin", isAdmin, {
|
||||||
|
suspense: false,
|
||||||
|
revalidateOnFocus: false
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
runningMode,
|
||||||
|
isAdminMode,
|
||||||
|
isSidecarMode: runningMode === "Sidecar",
|
||||||
|
mutateRunningMode
|
||||||
|
};
|
||||||
|
}
|
@ -27,6 +27,7 @@
|
|||||||
"Proxies": "Proxies",
|
"Proxies": "Proxies",
|
||||||
"Proxy Groups": "Proxy Groups",
|
"Proxy Groups": "Proxy Groups",
|
||||||
"Proxy Provider": "Proxy Provider",
|
"Proxy Provider": "Proxy Provider",
|
||||||
|
"Proxy Count": "Proxy Count",
|
||||||
"Update All": "Update All",
|
"Update All": "Update All",
|
||||||
"Update At": "Update At",
|
"Update At": "Update At",
|
||||||
"rule": "rule",
|
"rule": "rule",
|
||||||
@ -201,6 +202,8 @@
|
|||||||
"Tun Mode": "Tun Mode",
|
"Tun Mode": "Tun Mode",
|
||||||
"TUN requires Service Mode": "TUN mode requires install service",
|
"TUN requires Service Mode": "TUN mode requires install service",
|
||||||
"Install Service": "Install Service",
|
"Install Service": "Install Service",
|
||||||
|
"Install Service failed": "Install Service failed",
|
||||||
|
"Restart Core failed": "Restart Core failed",
|
||||||
"Reset to Default": "Reset to Default",
|
"Reset to Default": "Reset to Default",
|
||||||
"Tun Mode Info": "Tun (Virtual NIC) mode: Captures all system traffic, when enabled, there is no need to enable system proxy.",
|
"Tun Mode Info": "Tun (Virtual NIC) mode: Captures all system traffic, when enabled, there is no need to enable system proxy.",
|
||||||
"System Proxy Enabled": "System proxy is enabled, your applications will access the network through the proxy",
|
"System Proxy Enabled": "System proxy is enabled, your applications will access the network through the proxy",
|
||||||
@ -252,6 +255,7 @@
|
|||||||
"PAC Script Content": "PAC Script Content",
|
"PAC Script Content": "PAC Script Content",
|
||||||
"PAC URL": "PAC URL: ",
|
"PAC URL": "PAC URL: ",
|
||||||
"Auto Launch": "Auto Launch",
|
"Auto Launch": "Auto Launch",
|
||||||
|
"Administrator mode does not support auto launch": "Administrator mode does not support auto launch",
|
||||||
"Silent Start": "Silent Start",
|
"Silent Start": "Silent Start",
|
||||||
"Silent Start Info": "Start the program in background mode without displaying the panel",
|
"Silent Start Info": "Start the program in background mode without displaying the panel",
|
||||||
"TG Channel": "Telegram Channel",
|
"TG Channel": "Telegram Channel",
|
||||||
@ -488,8 +492,9 @@
|
|||||||
"Validation Failed": "Validation Failed",
|
"Validation Failed": "Validation Failed",
|
||||||
"Service Administrator Prompt": "Clash Verge requires administrator privileges to reinstall the system service",
|
"Service Administrator Prompt": "Clash Verge requires administrator privileges to reinstall the system service",
|
||||||
"DNS Settings": "DNS Settings",
|
"DNS Settings": "DNS Settings",
|
||||||
|
"DNS settings saved": "DNS settings saved",
|
||||||
"DNS Overwrite": "DNS Overwrite",
|
"DNS Overwrite": "DNS Overwrite",
|
||||||
"DNS Settings Warning": "If you are not familiar with these settings, please do not modify them and keep DNS Settings enabled",
|
"DNS Settings Warning": "If you are not familiar with these settings, please do not modify them and keep DNS Overwrite enabled",
|
||||||
"Enable DNS": "Enable DNS",
|
"Enable DNS": "Enable DNS",
|
||||||
"DNS Listen": "DNS Listen",
|
"DNS Listen": "DNS Listen",
|
||||||
"Enhanced Mode": "Enhanced Mode",
|
"Enhanced Mode": "Enhanced Mode",
|
||||||
@ -550,9 +555,19 @@
|
|||||||
"OS Info": "OS Info",
|
"OS Info": "OS Info",
|
||||||
"Running Mode": "Running Mode",
|
"Running Mode": "Running Mode",
|
||||||
"Sidecar Mode": "User Mode",
|
"Sidecar Mode": "User Mode",
|
||||||
|
"Administrator Mode": "Administrator Mode",
|
||||||
|
"Administrator + Service Mode": "Admin + Service Mode",
|
||||||
"Last Check Update": "Last Check Update",
|
"Last Check Update": "Last Check Update",
|
||||||
"Click to import subscription": "Click to import subscription",
|
"Click to import subscription": "Click to import subscription",
|
||||||
"Update subscription successfully": "Update subscription successfully",
|
"Update subscription successfully": "Update subscription successfully",
|
||||||
|
"Update failed, retrying with Clash proxy...": "Update failed, retrying with Clash proxy...",
|
||||||
|
"Update with Clash proxy successfully": "Update with Clash proxy successfully",
|
||||||
|
"Update failed even with Clash proxy": "Update failed even with Clash proxy",
|
||||||
|
"Profile creation failed, retrying with Clash proxy...": "Profile creation failed, retrying with Clash proxy...",
|
||||||
|
"Profile creation succeeded with Clash proxy": "Profile creation succeeded with Clash proxy",
|
||||||
|
"Import failed, retrying with Clash proxy...": "Import failed, retrying with Clash proxy...",
|
||||||
|
"Profile Imported with Clash proxy": "Profile Imported with Clash proxy",
|
||||||
|
"Import failed even with Clash proxy": "Import failed even with Clash proxy",
|
||||||
"Current Node": "Current Node",
|
"Current Node": "Current Node",
|
||||||
"No active proxy node": "No active proxy node",
|
"No active proxy node": "No active proxy node",
|
||||||
"Network Settings": "Network Settings",
|
"Network Settings": "Network Settings",
|
||||||
|
@ -27,6 +27,7 @@
|
|||||||
"Proxies": "代理",
|
"Proxies": "代理",
|
||||||
"Proxy Groups": "代理组",
|
"Proxy Groups": "代理组",
|
||||||
"Proxy Provider": "代理集合",
|
"Proxy Provider": "代理集合",
|
||||||
|
"Proxy Count": "节点数量",
|
||||||
"Update All": "更新全部",
|
"Update All": "更新全部",
|
||||||
"Update At": "更新于",
|
"Update At": "更新于",
|
||||||
"rule": "规则",
|
"rule": "规则",
|
||||||
@ -201,6 +202,8 @@
|
|||||||
"Tun Mode": "虚拟网卡模式",
|
"Tun Mode": "虚拟网卡模式",
|
||||||
"TUN requires Service Mode": "TUN 模式需要安装服务",
|
"TUN requires Service Mode": "TUN 模式需要安装服务",
|
||||||
"Install Service": "安装服务",
|
"Install Service": "安装服务",
|
||||||
|
"Install Service failed": "安装服务失败",
|
||||||
|
"Restart Core failed": "重启核心失败",
|
||||||
"Reset to Default": "重置为默认值",
|
"Reset to Default": "重置为默认值",
|
||||||
"Tun Mode Info": "TUN(虚拟网卡)模式接管系统所有流量,启用时无须打开系统代理",
|
"Tun Mode Info": "TUN(虚拟网卡)模式接管系统所有流量,启用时无须打开系统代理",
|
||||||
"System Proxy Enabled": "系统代理已启用,您的应用将通过代理访问网络",
|
"System Proxy Enabled": "系统代理已启用,您的应用将通过代理访问网络",
|
||||||
@ -252,6 +255,7 @@
|
|||||||
"PAC Script Content": "PAC 脚本内容",
|
"PAC Script Content": "PAC 脚本内容",
|
||||||
"PAC URL": "PAC 地址:",
|
"PAC URL": "PAC 地址:",
|
||||||
"Auto Launch": "开机自启",
|
"Auto Launch": "开机自启",
|
||||||
|
"Administrator mode does not support auto launch": "管理员模式不支持开机自启",
|
||||||
"Silent Start": "静默启动",
|
"Silent Start": "静默启动",
|
||||||
"Silent Start Info": "程序启动时以后台模式运行,不显示程序面板",
|
"Silent Start Info": "程序启动时以后台模式运行,不显示程序面板",
|
||||||
"TG Channel": "Telegram 频道",
|
"TG Channel": "Telegram 频道",
|
||||||
@ -353,7 +357,6 @@
|
|||||||
"clash_mode_direct": "直连模式",
|
"clash_mode_direct": "直连模式",
|
||||||
"toggle_system_proxy": "打开/关闭系统代理",
|
"toggle_system_proxy": "打开/关闭系统代理",
|
||||||
"toggle_tun_mode": "打开/关闭 TUN 模式",
|
"toggle_tun_mode": "打开/关闭 TUN 模式",
|
||||||
"toggle_lightweight_mode": "进入轻量模式",
|
|
||||||
"entry_lightweight_mode": "进入轻量模式",
|
"entry_lightweight_mode": "进入轻量模式",
|
||||||
"Backup Setting": "备份设置",
|
"Backup Setting": "备份设置",
|
||||||
"Backup Setting Info": "支持 WebDAV 备份配置文件",
|
"Backup Setting Info": "支持 WebDAV 备份配置文件",
|
||||||
@ -487,8 +490,9 @@
|
|||||||
"Validate Merge File": "验证覆写文件",
|
"Validate Merge File": "验证覆写文件",
|
||||||
"Validation Success": "验证成功",
|
"Validation Success": "验证成功",
|
||||||
"Validation Failed": "验证失败",
|
"Validation Failed": "验证失败",
|
||||||
"Service Administrator Prompt": "Clash Verge 需要使用管理员权限来重新安装系统服务",
|
"Service Administrator Prompt": "Clash Verge 需要管理员权限安装系统服务",
|
||||||
"DNS Settings": "DNS 设置",
|
"DNS Settings": "DNS 设置",
|
||||||
|
"DNS settings saved": "DNS 设置已保存",
|
||||||
"DNS Overwrite": "DNS 覆写",
|
"DNS Overwrite": "DNS 覆写",
|
||||||
"DNS Settings Warning": "如果你不清楚这里的设置请不要修改,并保持 DNS 覆写开启",
|
"DNS Settings Warning": "如果你不清楚这里的设置请不要修改,并保持 DNS 覆写开启",
|
||||||
"Enable DNS": "启用 DNS",
|
"Enable DNS": "启用 DNS",
|
||||||
@ -551,9 +555,19 @@
|
|||||||
"OS Info": "操作系统信息",
|
"OS Info": "操作系统信息",
|
||||||
"Running Mode": "运行模式",
|
"Running Mode": "运行模式",
|
||||||
"Sidecar Mode": "用户模式",
|
"Sidecar Mode": "用户模式",
|
||||||
|
"Administrator Mode": "管理员模式",
|
||||||
|
"Administrator + Service Mode": "管理员 + 服务模式",
|
||||||
"Last Check Update": "最后检查更新",
|
"Last Check Update": "最后检查更新",
|
||||||
"Click to import subscription": "点击导入订阅",
|
"Click to import subscription": "点击导入订阅",
|
||||||
"Update subscription successfully": "订阅更新成功",
|
"Update subscription successfully": "订阅更新成功",
|
||||||
|
"Update failed, retrying with Clash proxy...": "订阅更新失败,尝试使用 Clash 代理更新",
|
||||||
|
"Update with Clash proxy successfully": "使用 Clash 代理更新成功",
|
||||||
|
"Update failed even with Clash proxy": "使用 Clash 代理更新也失败",
|
||||||
|
"Profile creation failed, retrying with Clash proxy...": "订阅创建失败,尝试使用 Clash 代理创建",
|
||||||
|
"Profile creation succeeded with Clash proxy": "使用 Clash 代理创建订阅成功",
|
||||||
|
"Import failed, retrying with Clash proxy...": "订阅导入失败,尝试使用 Clash 代理导入",
|
||||||
|
"Profile Imported with Clash proxy": "使用 Clash 代理导入订阅成功",
|
||||||
|
"Import failed even with Clash proxy": "使用 Clash 代理导入订阅也失败",
|
||||||
"Current Node": "当前节点",
|
"Current Node": "当前节点",
|
||||||
"No active proxy node": "暂无激活的代理节点",
|
"No active proxy node": "暂无激活的代理节点",
|
||||||
"Network Settings": "网络设置",
|
"Network Settings": "网络设置",
|
||||||
|
@ -19,6 +19,7 @@ import {
|
|||||||
ThemeModeProvider,
|
ThemeModeProvider,
|
||||||
UpdateStateProvider,
|
UpdateStateProvider,
|
||||||
} from "./services/states";
|
} from "./services/states";
|
||||||
|
import { AppDataProvider } from "./providers/app-data-provider";
|
||||||
|
|
||||||
const mainElementId = "root";
|
const mainElementId = "root";
|
||||||
const container = document.getElementById(mainElementId);
|
const container = document.getElementById(mainElementId);
|
||||||
@ -51,9 +52,11 @@ createRoot(container).render(
|
|||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<ComposeContextProvider contexts={contexts}>
|
<ComposeContextProvider contexts={contexts}>
|
||||||
<BaseErrorBoundary>
|
<BaseErrorBoundary>
|
||||||
<BrowserRouter>
|
<AppDataProvider>
|
||||||
<Layout />
|
<BrowserRouter>
|
||||||
</BrowserRouter>
|
<Layout />
|
||||||
|
</BrowserRouter>
|
||||||
|
</AppDataProvider>
|
||||||
</BaseErrorBoundary>
|
</BaseErrorBoundary>
|
||||||
</ComposeContextProvider>
|
</ComposeContextProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
|
@ -249,8 +249,8 @@ const Layout = () => {
|
|||||||
? {
|
? {
|
||||||
borderRadius: "8px",
|
borderRadius: "8px",
|
||||||
border: "1px solid var(--divider-color)",
|
border: "1px solid var(--divider-color)",
|
||||||
width: "calc(100vw - 0px)",
|
width: "calc(100vw - 4px)",
|
||||||
height: "calc(100vh - 0px)",
|
height: "calc(100vh - 4px)",
|
||||||
}
|
}
|
||||||
: {},
|
: {},
|
||||||
]}
|
]}
|
||||||
|
@ -11,7 +11,6 @@ import {
|
|||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import { closeAllConnections } from "@/services/api";
|
import { closeAllConnections } from "@/services/api";
|
||||||
import { useConnectionSetting } from "@/services/states";
|
import { useConnectionSetting } from "@/services/states";
|
||||||
import { useClashInfo } from "@/hooks/use-clash";
|
|
||||||
import { BaseEmpty, BasePage } from "@/components/base";
|
import { BaseEmpty, BasePage } from "@/components/base";
|
||||||
import { ConnectionItem } from "@/components/connection/connection-item";
|
import { ConnectionItem } from "@/components/connection/connection-item";
|
||||||
import { ConnectionTable } from "@/components/connection/connection-table";
|
import { ConnectionTable } from "@/components/connection/connection-table";
|
||||||
@ -25,10 +24,9 @@ import {
|
|||||||
type SearchState,
|
type SearchState,
|
||||||
} from "@/components/base/base-search-box";
|
} from "@/components/base/base-search-box";
|
||||||
import { BaseStyledSelect } from "@/components/base/base-styled-select";
|
import { BaseStyledSelect } from "@/components/base/base-styled-select";
|
||||||
import useSWRSubscription from "swr/subscription";
|
|
||||||
import { createSockette, createAuthSockette } from "@/utils/websocket";
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
import { useVisibility } from "@/hooks/use-visibility";
|
import { useVisibility } from "@/hooks/use-visibility";
|
||||||
|
import { useAppData } from "@/providers/app-data-provider";
|
||||||
|
|
||||||
const initConn: IConnections = {
|
const initConn: IConnections = {
|
||||||
uploadTotal: 0,
|
uploadTotal: 0,
|
||||||
@ -40,12 +38,14 @@ type OrderFunc = (list: IConnectionsItem[]) => IConnectionsItem[];
|
|||||||
|
|
||||||
const ConnectionsPage = () => {
|
const ConnectionsPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { clashInfo } = useClashInfo();
|
|
||||||
const pageVisible = useVisibility();
|
const pageVisible = useVisibility();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isDark = theme.palette.mode === "dark";
|
const isDark = theme.palette.mode === "dark";
|
||||||
const [match, setMatch] = useState(() => (_: string) => true);
|
const [match, setMatch] = useState(() => (_: string) => true);
|
||||||
const [curOrderOpt, setOrderOpt] = useState("Default");
|
const [curOrderOpt, setOrderOpt] = useState("Default");
|
||||||
|
|
||||||
|
// 使用全局数据
|
||||||
|
const { connections } = useAppData();
|
||||||
|
|
||||||
const [setting, setSetting] = useConnectionSetting();
|
const [setting, setSetting] = useConnectionSetting();
|
||||||
|
|
||||||
@ -66,99 +66,37 @@ const ConnectionsPage = () => {
|
|||||||
const [isPaused, setIsPaused] = useState(false);
|
const [isPaused, setIsPaused] = useState(false);
|
||||||
const [frozenData, setFrozenData] = useState<IConnections | null>(null);
|
const [frozenData, setFrozenData] = useState<IConnections | null>(null);
|
||||||
|
|
||||||
const { data: connData = initConn } = useSWRSubscription<
|
// 使用全局连接数据
|
||||||
IConnections,
|
|
||||||
any,
|
|
||||||
"getClashConnections" | null
|
|
||||||
>(
|
|
||||||
clashInfo && pageVisible ? "getClashConnections" : null,
|
|
||||||
(_key, { next }) => {
|
|
||||||
const { server = "", secret = "" } = clashInfo!;
|
|
||||||
|
|
||||||
if (!server) {
|
|
||||||
console.warn("[Connections] 服务器地址为空,无法建立连接");
|
|
||||||
next(null, initConn);
|
|
||||||
return () => {};
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[Connections] 正在连接: ${server}/connections`);
|
|
||||||
|
|
||||||
// 设置较长的超时时间,确保连接可以建立
|
|
||||||
const s = createAuthSockette(`${server}/connections`, secret, {
|
|
||||||
timeout: 8000, // 8秒超时
|
|
||||||
onmessage(event) {
|
|
||||||
const data = JSON.parse(event.data) as IConnections;
|
|
||||||
next(null, (old = initConn) => {
|
|
||||||
const oldConn = old.connections;
|
|
||||||
const maxLen = data.connections?.length;
|
|
||||||
|
|
||||||
const connections: IConnectionsItem[] = [];
|
|
||||||
|
|
||||||
const rest = (data.connections || []).filter((each) => {
|
|
||||||
const index = oldConn.findIndex((o) => o.id === each.id);
|
|
||||||
|
|
||||||
if (index >= 0 && index < maxLen) {
|
|
||||||
const old = oldConn[index];
|
|
||||||
each.curUpload = each.upload - old.upload;
|
|
||||||
each.curDownload = each.download - old.download;
|
|
||||||
|
|
||||||
connections[index] = each;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
for (let i = 0; i < maxLen; ++i) {
|
|
||||||
if (!connections[i] && rest.length > 0) {
|
|
||||||
connections[i] = rest.shift()!;
|
|
||||||
connections[i].curUpload = 0;
|
|
||||||
connections[i].curDownload = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { ...data, connections };
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onerror(event) {
|
|
||||||
console.error("[Connections] WebSocket 连接错误", event);
|
|
||||||
// 报告错误但提供空数据,避免UI崩溃
|
|
||||||
next(null, initConn);
|
|
||||||
},
|
|
||||||
onclose(event) {
|
|
||||||
console.log("[Connections] WebSocket 连接关闭", event);
|
|
||||||
},
|
|
||||||
onopen(event) {
|
|
||||||
console.log("[Connections] WebSocket 连接已建立");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
console.log("[Connections] 清理WebSocket连接");
|
|
||||||
try {
|
|
||||||
s.close();
|
|
||||||
} catch (e) {
|
|
||||||
console.error("[Connections] 关闭连接时出错", e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const displayData = useMemo(() => {
|
const displayData = useMemo(() => {
|
||||||
return isPaused ? (frozenData ?? connData) : connData;
|
if (!pageVisible) return initConn;
|
||||||
}, [isPaused, frozenData, connData]);
|
|
||||||
|
if (isPaused) {
|
||||||
|
return frozenData ?? {
|
||||||
|
uploadTotal: connections.uploadTotal,
|
||||||
|
downloadTotal: connections.downloadTotal,
|
||||||
|
connections: connections.data
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
uploadTotal: connections.uploadTotal,
|
||||||
|
downloadTotal: connections.downloadTotal,
|
||||||
|
connections: connections.data
|
||||||
|
};
|
||||||
|
}, [isPaused, frozenData, connections, pageVisible]);
|
||||||
|
|
||||||
const [filterConn] = useMemo(() => {
|
const [filterConn] = useMemo(() => {
|
||||||
const orderFunc = orderOpts[curOrderOpt];
|
const orderFunc = orderOpts[curOrderOpt];
|
||||||
let connections = displayData.connections.filter((conn) => {
|
let conns = displayData.connections.filter((conn) => {
|
||||||
const { host, destinationIP, process } = conn.metadata;
|
const { host, destinationIP, process } = conn.metadata;
|
||||||
return (
|
return (
|
||||||
match(host || "") || match(destinationIP || "") || match(process || "")
|
match(host || "") || match(destinationIP || "") || match(process || "")
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (orderFunc) connections = orderFunc(connections);
|
if (orderFunc) conns = orderFunc(conns);
|
||||||
|
|
||||||
return [connections];
|
return [conns];
|
||||||
}, [displayData, match, curOrderOpt]);
|
}, [displayData, match, curOrderOpt]);
|
||||||
|
|
||||||
const onCloseAll = useLockFn(closeAllConnections);
|
const onCloseAll = useLockFn(closeAllConnections);
|
||||||
@ -172,13 +110,17 @@ const ConnectionsPage = () => {
|
|||||||
const handlePauseToggle = useCallback(() => {
|
const handlePauseToggle = useCallback(() => {
|
||||||
setIsPaused((prev) => {
|
setIsPaused((prev) => {
|
||||||
if (!prev) {
|
if (!prev) {
|
||||||
setFrozenData(connData);
|
setFrozenData({
|
||||||
|
uploadTotal: connections.uploadTotal,
|
||||||
|
downloadTotal: connections.downloadTotal,
|
||||||
|
connections: connections.data
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
setFrozenData(null);
|
setFrozenData(null);
|
||||||
}
|
}
|
||||||
return !prev;
|
return !prev;
|
||||||
});
|
});
|
||||||
}, [connData]);
|
}, [connections]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BasePage
|
<BasePage
|
||||||
|
@ -203,7 +203,7 @@ const HomeSettingsDialog = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const HomePage = () => {
|
export const HomePage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { verge } = useVerge();
|
const { verge } = useVerge();
|
||||||
const { current, mutateProfiles } = useProfiles();
|
const { current, mutateProfiles } = useProfiles();
|
||||||
@ -322,7 +322,6 @@ const HomePage = () => {
|
|||||||
title={t("Traffic Stats")}
|
title={t("Traffic Stats")}
|
||||||
icon={<SpeedOutlined />}
|
icon={<SpeedOutlined />}
|
||||||
iconColor="secondary"
|
iconColor="secondary"
|
||||||
minHeight={280}
|
|
||||||
>
|
>
|
||||||
<EnhancedTrafficStats />
|
<EnhancedTrafficStats />
|
||||||
</EnhancedCard>
|
</EnhancedCard>
|
||||||
@ -395,4 +394,4 @@ const ClashModeEnhancedCard = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default HomePage;
|
export default HomePage;
|
@ -142,15 +142,36 @@ const ProfilePage = () => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 尝试正常导入
|
||||||
await importProfile(url);
|
await importProfile(url);
|
||||||
Notice.success(t("Profile Imported Successfully"));
|
Notice.success(t("Profile Imported Successfully"));
|
||||||
setUrl("");
|
setUrl("");
|
||||||
setLoading(false);
|
|
||||||
mutateProfiles();
|
mutateProfiles();
|
||||||
await onEnhance(false);
|
await onEnhance(false);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
Notice.error(err.message || err.toString());
|
// 首次导入失败,尝试使用自身代理
|
||||||
setLoading(false);
|
const errmsg = err.message || err.toString();
|
||||||
|
Notice.info(t("Import failed, retrying with Clash proxy..."));
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 使用自身代理尝试导入
|
||||||
|
await importProfile(url, {
|
||||||
|
with_proxy: false,
|
||||||
|
self_proxy: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// 回退导入成功
|
||||||
|
Notice.success(t("Profile Imported with Clash proxy"));
|
||||||
|
setUrl("");
|
||||||
|
mutateProfiles();
|
||||||
|
await onEnhance(false);
|
||||||
|
} catch (retryErr: any) {
|
||||||
|
// 回退导入也失败
|
||||||
|
const retryErrmsg = retryErr?.message || retryErr.toString();
|
||||||
|
Notice.error(
|
||||||
|
`${t("Import failed even with Clash proxy")}: ${retryErrmsg}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setDisabled(false);
|
setDisabled(false);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
@ -1,28 +1,27 @@
|
|||||||
import useSWR from "swr";
|
|
||||||
import { useState, useMemo, useRef } from "react";
|
import { useState, useMemo, useRef } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Virtuoso, VirtuosoHandle } from "react-virtuoso";
|
import { Virtuoso, VirtuosoHandle } from "react-virtuoso";
|
||||||
import { Box } from "@mui/material";
|
import { Box } from "@mui/material";
|
||||||
import { getRules } from "@/services/api";
|
|
||||||
import { BaseEmpty, BasePage } from "@/components/base";
|
import { BaseEmpty, BasePage } from "@/components/base";
|
||||||
import RuleItem from "@/components/rule/rule-item";
|
import RuleItem from "@/components/rule/rule-item";
|
||||||
import { ProviderButton } from "@/components/rule/provider-button";
|
import { ProviderButton } from "@/components/rule/provider-button";
|
||||||
import { BaseSearchBox } from "@/components/base/base-search-box";
|
import { BaseSearchBox } from "@/components/base/base-search-box";
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
import { ScrollTopButton } from "@/components/layout/scroll-top-button";
|
import { ScrollTopButton } from "@/components/layout/scroll-top-button";
|
||||||
|
import { useAppData } from "@/providers/app-data-provider";
|
||||||
|
|
||||||
const RulesPage = () => {
|
const RulesPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { data = [] } = useSWR("getRules", getRules);
|
const { rules = [] } = useAppData();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isDark = theme.palette.mode === "dark";
|
const isDark = theme.palette.mode === "dark";
|
||||||
const [match, setMatch] = useState(() => (_: string) => true);
|
const [match, setMatch] = useState(() => (_: string) => true);
|
||||||
const virtuosoRef = useRef<VirtuosoHandle>(null);
|
const virtuosoRef = useRef<VirtuosoHandle>(null);
|
||||||
const [showScrollTop, setShowScrollTop] = useState(false);
|
const [showScrollTop, setShowScrollTop] = useState(false);
|
||||||
|
|
||||||
const rules = useMemo(() => {
|
const filteredRules = useMemo(() => {
|
||||||
return data.filter((item) => match(item.payload));
|
return rules.filter((item) => match(item.payload));
|
||||||
}, [data, match]);
|
}, [rules, match]);
|
||||||
|
|
||||||
const scrollToTop = () => {
|
const scrollToTop = () => {
|
||||||
virtuosoRef.current?.scrollTo({
|
virtuosoRef.current?.scrollTo({
|
||||||
@ -64,11 +63,11 @@ const RulesPage = () => {
|
|||||||
<BaseSearchBox onSearch={(match) => setMatch(() => match)} />
|
<BaseSearchBox onSearch={(match) => setMatch(() => match)} />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{rules.length > 0 ? (
|
{filteredRules.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<Virtuoso
|
<Virtuoso
|
||||||
ref={virtuosoRef}
|
ref={virtuosoRef}
|
||||||
data={rules}
|
data={filteredRules}
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
}}
|
}}
|
||||||
|
299
src/providers/app-data-provider.tsx
Normal file
299
src/providers/app-data-provider.tsx
Normal file
@ -0,0 +1,299 @@
|
|||||||
|
import { createContext, useContext, useMemo } from "react";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import useSWRSubscription from "swr/subscription";
|
||||||
|
import { getProxies, getConnections, getRules, getClashConfig, getProxyProviders, getRuleProviders } from "@/services/api";
|
||||||
|
import { getSystemProxy, getRunningMode, getAppUptime } from "@/services/cmds";
|
||||||
|
import { useClashInfo } from "@/hooks/use-clash";
|
||||||
|
import { createAuthSockette } from "@/utils/websocket";
|
||||||
|
import { useVisibility } from "@/hooks/use-visibility";
|
||||||
|
|
||||||
|
// 定义AppDataContext类型 - 使用宽松类型
|
||||||
|
interface AppDataContextType {
|
||||||
|
proxies: any;
|
||||||
|
clashConfig: any;
|
||||||
|
rules: any[];
|
||||||
|
sysproxy: any;
|
||||||
|
runningMode?: string;
|
||||||
|
uptime: number;
|
||||||
|
proxyProviders: any;
|
||||||
|
ruleProviders: any;
|
||||||
|
connections: {
|
||||||
|
data: any[];
|
||||||
|
count: number;
|
||||||
|
uploadTotal: number;
|
||||||
|
downloadTotal: number;
|
||||||
|
};
|
||||||
|
traffic: {up: number; down: number};
|
||||||
|
memory: {inuse: number};
|
||||||
|
refreshProxy: () => Promise<any>;
|
||||||
|
refreshClashConfig: () => Promise<any>;
|
||||||
|
refreshRules: () => Promise<any>;
|
||||||
|
refreshSysproxy: () => Promise<any>;
|
||||||
|
refreshProxyProviders: () => Promise<any>;
|
||||||
|
refreshRuleProviders: () => Promise<any>;
|
||||||
|
refreshAll: () => Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建上下文
|
||||||
|
const AppDataContext = createContext<AppDataContextType | null>(null);
|
||||||
|
|
||||||
|
// 全局数据提供者组件
|
||||||
|
export const AppDataProvider = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
const { clashInfo } = useClashInfo();
|
||||||
|
const pageVisible = useVisibility();
|
||||||
|
|
||||||
|
// 基础数据 - 中频率更新 (5秒)
|
||||||
|
const { data: proxiesData, mutate: refreshProxy } = useSWR(
|
||||||
|
"getProxies",
|
||||||
|
getProxies,
|
||||||
|
{
|
||||||
|
refreshInterval: 5000,
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
suspense: false,
|
||||||
|
errorRetryCount: 3
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: clashConfig, mutate: refreshClashConfig } = useSWR(
|
||||||
|
"getClashConfig",
|
||||||
|
getClashConfig,
|
||||||
|
{
|
||||||
|
refreshInterval: 5000,
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
suspense: false,
|
||||||
|
errorRetryCount: 3
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 提供者数据
|
||||||
|
const { data: proxyProviders, mutate: refreshProxyProviders } = useSWR(
|
||||||
|
"getProxyProviders",
|
||||||
|
getProxyProviders,
|
||||||
|
{
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
suspense: false,
|
||||||
|
errorRetryCount: 3
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: ruleProviders, mutate: refreshRuleProviders } = useSWR(
|
||||||
|
"getRuleProviders",
|
||||||
|
getRuleProviders,
|
||||||
|
{
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
suspense: false,
|
||||||
|
errorRetryCount: 3
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 低频率更新数据
|
||||||
|
const { data: rulesData, mutate: refreshRules } = useSWR(
|
||||||
|
"getRules",
|
||||||
|
getRules,
|
||||||
|
{
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
suspense: false,
|
||||||
|
errorRetryCount: 3
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: sysproxy, mutate: refreshSysproxy } = useSWR(
|
||||||
|
"getSystemProxy",
|
||||||
|
getSystemProxy,
|
||||||
|
{
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
suspense: false,
|
||||||
|
errorRetryCount: 3
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: runningMode } = useSWR(
|
||||||
|
"getRunningMode",
|
||||||
|
getRunningMode,
|
||||||
|
{
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
suspense: false,
|
||||||
|
errorRetryCount: 3
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 高频率更新数据 (1秒)
|
||||||
|
const { data: uptimeData } = useSWR(
|
||||||
|
"appUptime",
|
||||||
|
getAppUptime,
|
||||||
|
{
|
||||||
|
refreshInterval: 1000,
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
suspense: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 连接数据 - 使用WebSocket实时更新
|
||||||
|
const { data: connectionsData = { connections: [], uploadTotal: 0, downloadTotal: 0 } } =
|
||||||
|
useSWRSubscription(
|
||||||
|
clashInfo && pageVisible ? "connections" : null,
|
||||||
|
(_key, { next }) => {
|
||||||
|
if (!clashInfo || !pageVisible) return () => {};
|
||||||
|
|
||||||
|
const { server = "", secret = "" } = clashInfo;
|
||||||
|
if (!server) return () => {};
|
||||||
|
|
||||||
|
const socket = createAuthSockette(`${server}/connections`, secret, {
|
||||||
|
timeout: 5000,
|
||||||
|
onmessage(event) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
// 处理连接数据,计算当前上传下载速度
|
||||||
|
next(null, (prev: any = { connections: [], uploadTotal: 0, downloadTotal: 0 }) => {
|
||||||
|
const oldConns = prev.connections || [];
|
||||||
|
const newConns = data.connections || [];
|
||||||
|
|
||||||
|
// 计算当前速度
|
||||||
|
const processedConns = newConns.map((conn: any) => {
|
||||||
|
const oldConn = oldConns.find((old: any) => old.id === conn.id);
|
||||||
|
if (oldConn) {
|
||||||
|
return {
|
||||||
|
...conn,
|
||||||
|
curUpload: conn.upload - oldConn.upload,
|
||||||
|
curDownload: conn.download - oldConn.download
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { ...conn, curUpload: 0, curDownload: 0 };
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
connections: processedConns
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[Connections] 解析数据错误:", err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onerror() {
|
||||||
|
next(null, { connections: [], uploadTotal: 0, downloadTotal: 0 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => socket.close();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 流量和内存数据 - 通过WebSocket获取实时流量数据
|
||||||
|
const { data: trafficData = { up: 0, down: 0 } } = useSWRSubscription(
|
||||||
|
clashInfo && pageVisible ? "traffic" : null,
|
||||||
|
(_key, { next }) => {
|
||||||
|
if (!clashInfo || !pageVisible) return () => {};
|
||||||
|
|
||||||
|
const { server = "", secret = "" } = clashInfo;
|
||||||
|
if (!server) return () => {};
|
||||||
|
|
||||||
|
const socket = createAuthSockette(`${server}/traffic`, secret, {
|
||||||
|
onmessage(event) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
next(null, data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[Traffic] 解析数据错误:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => socket.close();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: memoryData = { inuse: 0 } } = useSWRSubscription(
|
||||||
|
clashInfo && pageVisible ? "memory" : null,
|
||||||
|
(_key, { next }) => {
|
||||||
|
if (!clashInfo || !pageVisible) return () => {};
|
||||||
|
|
||||||
|
const { server = "", secret = "" } = clashInfo;
|
||||||
|
if (!server) return () => {};
|
||||||
|
|
||||||
|
const socket = createAuthSockette(`${server}/memory`, secret, {
|
||||||
|
onmessage(event) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
next(null, data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[Memory] 解析数据错误:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => socket.close();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 提供统一的刷新方法
|
||||||
|
const refreshAll = async () => {
|
||||||
|
await Promise.all([
|
||||||
|
refreshProxy(),
|
||||||
|
refreshClashConfig(),
|
||||||
|
refreshRules(),
|
||||||
|
refreshSysproxy(),
|
||||||
|
refreshProxyProviders(),
|
||||||
|
refreshRuleProviders()
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 聚合所有数据
|
||||||
|
const value = useMemo(() => ({
|
||||||
|
// 数据
|
||||||
|
proxies: proxiesData,
|
||||||
|
clashConfig,
|
||||||
|
rules: rulesData || [],
|
||||||
|
sysproxy,
|
||||||
|
runningMode,
|
||||||
|
uptime: uptimeData || 0,
|
||||||
|
|
||||||
|
// 提供者数据
|
||||||
|
proxyProviders: proxyProviders || {},
|
||||||
|
ruleProviders: ruleProviders || {},
|
||||||
|
|
||||||
|
// 连接数据
|
||||||
|
connections: {
|
||||||
|
data: connectionsData.connections || [],
|
||||||
|
count: connectionsData.connections?.length || 0,
|
||||||
|
uploadTotal: connectionsData.uploadTotal || 0,
|
||||||
|
downloadTotal: connectionsData.downloadTotal || 0
|
||||||
|
},
|
||||||
|
|
||||||
|
// 实时流量数据
|
||||||
|
traffic: trafficData,
|
||||||
|
memory: memoryData,
|
||||||
|
|
||||||
|
// 刷新方法
|
||||||
|
refreshProxy,
|
||||||
|
refreshClashConfig,
|
||||||
|
refreshRules,
|
||||||
|
refreshSysproxy,
|
||||||
|
refreshProxyProviders,
|
||||||
|
refreshRuleProviders,
|
||||||
|
refreshAll
|
||||||
|
}), [
|
||||||
|
proxiesData, clashConfig, rulesData, sysproxy,
|
||||||
|
runningMode, uptimeData, connectionsData,
|
||||||
|
trafficData, memoryData, proxyProviders, ruleProviders,
|
||||||
|
refreshProxy, refreshClashConfig, refreshRules, refreshSysproxy,
|
||||||
|
refreshProxyProviders, refreshRuleProviders
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppDataContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</AppDataContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 自定义Hook访问全局数据
|
||||||
|
export const useAppData = () => {
|
||||||
|
const context = useContext(AppDataContext);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useAppData必须在AppDataProvider内使用");
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
};
|
@ -3,13 +3,9 @@ import { getClashInfo } from "./cmds";
|
|||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from "ahooks";
|
||||||
|
|
||||||
let axiosIns: AxiosInstance = null!;
|
let instancePromise: Promise<AxiosInstance> = null!;
|
||||||
|
|
||||||
/// initialize some information
|
|
||||||
/// enable force update axiosIns
|
|
||||||
export const getAxios = async (force: boolean = false) => {
|
|
||||||
if (axiosIns && !force) return axiosIns;
|
|
||||||
|
|
||||||
|
async function getInstancePromise() {
|
||||||
let server = "";
|
let server = "";
|
||||||
let secret = "";
|
let secret = "";
|
||||||
|
|
||||||
@ -26,13 +22,22 @@ export const getAxios = async (force: boolean = false) => {
|
|||||||
if (info?.secret) secret = info?.secret;
|
if (info?.secret) secret = info?.secret;
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
axiosIns = axios.create({
|
const axiosIns = axios.create({
|
||||||
baseURL: `http://${server}`,
|
baseURL: `http://${server}`,
|
||||||
headers: secret ? { Authorization: `Bearer ${secret}` } : {},
|
headers: secret ? { Authorization: `Bearer ${secret}` } : {},
|
||||||
timeout: 15000,
|
timeout: 15000,
|
||||||
});
|
});
|
||||||
axiosIns.interceptors.response.use((r) => r.data);
|
axiosIns.interceptors.response.use((r) => r.data);
|
||||||
return axiosIns;
|
return axiosIns;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// initialize some information
|
||||||
|
/// enable force update axiosIns
|
||||||
|
export const getAxios = async (force: boolean = false) => {
|
||||||
|
if (!instancePromise || force) {
|
||||||
|
instancePromise = getInstancePromise();
|
||||||
|
}
|
||||||
|
return instancePromise;
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Get Version
|
/// Get Version
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import dayjs from "dayjs";
|
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { Notice } from "@/components/base";
|
import { Notice } from "@/components/base";
|
||||||
|
|
||||||
@ -37,10 +36,10 @@ export async function saveProfileFile(index: string, fileData: string) {
|
|||||||
return invoke<void>("save_profile_file", { index, fileData });
|
return invoke<void>("save_profile_file", { index, fileData });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function importProfile(url: string) {
|
export async function importProfile(url: string, option?: IProfileOption) {
|
||||||
return invoke<void>("import_profile", {
|
return invoke<void>("import_profile", {
|
||||||
url,
|
url,
|
||||||
option: { with_proxy: true },
|
option: option || { with_proxy: true },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -320,15 +319,39 @@ export const getAppUptime = async () => {
|
|||||||
return invoke<number>("get_app_uptime");
|
return invoke<number>("get_app_uptime");
|
||||||
};
|
};
|
||||||
|
|
||||||
// 安装/重装系统服务
|
// 安装系统服务
|
||||||
export const installService = async () => {
|
export const installService = async () => {
|
||||||
return invoke<void>("install_service");
|
return invoke<void>("install_service");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 卸载系统服务
|
||||||
|
export const uninstallService = async () => {
|
||||||
|
return invoke<void>("uninstall_service");
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重装系统服务
|
||||||
|
export const reinstallService = async () => {
|
||||||
|
return invoke<void>("reinstall_service");
|
||||||
|
};
|
||||||
|
|
||||||
|
// 修复系统服务
|
||||||
|
export const repairService = async () => {
|
||||||
|
return invoke<void>("repair_service");
|
||||||
|
};
|
||||||
|
|
||||||
export const entry_lightweight_mode = async () => {
|
export const entry_lightweight_mode = async () => {
|
||||||
return invoke<void>("entry_lightweight_mode");
|
return invoke<void>("entry_lightweight_mode");
|
||||||
}
|
};
|
||||||
|
|
||||||
export const exit_lightweight_mode = async () => {
|
export const exit_lightweight_mode = async () => {
|
||||||
return invoke<void>("exit_lightweight_mode");
|
return invoke<void>("exit_lightweight_mode");
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export const isAdmin = async () => {
|
||||||
|
try {
|
||||||
|
return await invoke<boolean>("is_admin");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("检查管理员权限失败:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -36,11 +36,96 @@ export default defineConfig({
|
|||||||
entry: "monaco-yaml/yaml.worker",
|
entry: "monaco-yaml/yaml.worker",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
globalAPI: false,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
build: {
|
build: {
|
||||||
outDir: "../dist",
|
outDir: "../dist",
|
||||||
emptyOutDir: true,
|
emptyOutDir: true,
|
||||||
|
target: "es2020",
|
||||||
|
minify: "terser",
|
||||||
|
chunkSizeWarningLimit: 4000,
|
||||||
|
reportCompressedSize: false,
|
||||||
|
sourcemap: false,
|
||||||
|
cssCodeSplit: true,
|
||||||
|
cssMinify: true,
|
||||||
|
rollupOptions: {
|
||||||
|
treeshake: {
|
||||||
|
preset: "recommended",
|
||||||
|
moduleSideEffects: (id) => !/\.css$/.test(id),
|
||||||
|
tryCatchDeoptimization: false,
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
compact: true,
|
||||||
|
experimentalMinChunkSize: 30000,
|
||||||
|
dynamicImportInCjs: true,
|
||||||
|
manualChunks(id) {
|
||||||
|
if (id.includes("node_modules")) {
|
||||||
|
// Monaco Editor should be a separate chunk
|
||||||
|
if (id.includes("monaco-editor")) return "monaco-editor";
|
||||||
|
|
||||||
|
// React-related libraries (react, react-dom, react-router-dom, etc.)
|
||||||
|
if (
|
||||||
|
id.includes("react") ||
|
||||||
|
id.includes("react-dom") ||
|
||||||
|
id.includes("react-router-dom") ||
|
||||||
|
id.includes("react-transition-group") ||
|
||||||
|
id.includes("react-error-boundary") ||
|
||||||
|
id.includes("react-hook-form") ||
|
||||||
|
id.includes("react-markdown") ||
|
||||||
|
id.includes("react-virtuoso")
|
||||||
|
) {
|
||||||
|
return "react";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utilities chunk: group commonly used utility libraries
|
||||||
|
if (
|
||||||
|
id.includes("axios") ||
|
||||||
|
id.includes("lodash-es") ||
|
||||||
|
id.includes("dayjs") ||
|
||||||
|
id.includes("js-base64") ||
|
||||||
|
id.includes("js-yaml") ||
|
||||||
|
id.includes("cli-color") ||
|
||||||
|
id.includes("nanoid")
|
||||||
|
) {
|
||||||
|
return "utils";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tauri-related plugins: grouping together Tauri plugins
|
||||||
|
if (
|
||||||
|
id.includes("@tauri-apps/api") ||
|
||||||
|
id.includes("@tauri-apps/plugin-clipboard-manager") ||
|
||||||
|
id.includes("@tauri-apps/plugin-dialog") ||
|
||||||
|
id.includes("@tauri-apps/plugin-fs") ||
|
||||||
|
id.includes("@tauri-apps/plugin-global-shortcut") ||
|
||||||
|
id.includes("@tauri-apps/plugin-notification") ||
|
||||||
|
id.includes("@tauri-apps/plugin-process") ||
|
||||||
|
id.includes("@tauri-apps/plugin-shell") ||
|
||||||
|
id.includes("@tauri-apps/plugin-updater")
|
||||||
|
) {
|
||||||
|
return "tauri-plugins";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Material UI libraries (grouped together)
|
||||||
|
if (
|
||||||
|
id.includes("@mui/material") ||
|
||||||
|
id.includes("@mui/icons-material") ||
|
||||||
|
id.includes("@mui/lab") ||
|
||||||
|
id.includes("@mui/x-data-grid")
|
||||||
|
) {
|
||||||
|
return "mui";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Small vendor packages
|
||||||
|
const pkg = id.match(/node_modules\/([^\/]+)/)?.[1];
|
||||||
|
if (pkg && pkg.length < 8) return "small-vendors";
|
||||||
|
|
||||||
|
// Large vendor packages
|
||||||
|
return "large-vendor";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user