mirror of
https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation.git
synced 2026-08-09 15:19:11 +02:00
Compare commits
1,403 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44abeefb71 |
||
|
|
67f880b507 |
||
|
|
14d2f29f0a |
||
|
|
f3df24beb9 |
||
|
|
2379e852ce |
||
|
|
3c4011807b |
||
|
|
9dbdd59b0e |
||
|
|
ef94066d65 |
||
|
|
266f1b9597 |
||
|
|
856606c58b |
||
|
|
9336f25e7d |
||
|
|
3f55e6cdba |
||
|
|
c38c1cd2d0 |
||
|
|
31234bed3c |
||
|
|
067f2251e9 |
||
|
|
43d323332f |
||
|
|
4ec90fa807 |
||
|
|
85c0d67776 |
||
|
|
3bc538292d |
||
|
|
93c83863a6 |
||
|
|
596120a215 |
||
|
|
d98677c7ca |
||
|
|
469ad5f710 |
||
|
|
81291e0655 |
||
|
|
f9e4f141c0 |
||
|
|
1623d39b55 |
||
|
|
258a50cdfe |
||
|
|
ded17cf91d |
||
|
|
b051be8162 |
||
|
|
bd1ed7ea76 | ||
|
|
6bb0155c3a |
||
|
|
4773be1f00 |
||
|
|
7d023fadef |
||
|
|
826b159d03 |
||
|
|
ddd9b16ced |
||
|
|
1f561b8d2d | ||
|
|
02ba6e2cca |
||
|
|
da7482601e |
||
|
|
7dcb7e9315 |
||
|
|
b86c41187c | ||
|
|
1bb1453257 |
||
|
|
649d0d0bcf |
||
|
|
0e5203f2fa |
||
|
|
b11ac0bff9 | ||
|
|
6dea60dfc0 |
||
|
|
6e02459ee2 |
||
|
|
5492d39683 |
||
|
|
fb0dfa8d14 |
||
|
|
96dd6d937e |
||
|
|
2739d9b213 |
||
|
|
8b0e15eb9a |
||
|
|
51a929009c |
||
|
|
9108e3d4b4 |
||
|
|
e0e87e39ea |
||
|
|
01dd13117c |
||
|
|
c16ee8b379 | ||
|
|
b47675e913 |
||
|
|
1731d39a2d | ||
|
|
dc6d383f58 |
||
|
|
a3de7464d4 |
||
|
|
0415bab6c0 |
||
|
|
f788c87daa |
||
|
|
20fcc6e7b6 |
||
|
|
ede2b02501 |
||
|
|
d6a0d0733e |
||
|
|
1d93422e6f |
||
|
|
e7a96d7e0f |
||
|
|
3bc499db42 | ||
|
|
0a76d615ba |
||
|
|
bf3d6ccc59 |
||
|
|
01a37e2d33 | ||
|
|
8e160ff738 |
||
|
|
f85c85a592 |
||
|
|
7eaff8f810 |
||
|
|
acef695f8e |
||
|
|
3e52ff6ba1 |
||
|
|
644b30ae73 |
||
|
|
23cdfafefb |
||
|
|
49250576f3 |
||
|
|
a5d095e140 | ||
|
|
fd94a84a71 |
||
|
|
52f56a42cf |
||
|
|
6a44fda50a | ||
|
|
e2c47a7a2d |
||
|
|
a8ed2ea1ec |
||
|
|
648f5a088f |
||
|
|
a6865c01a5 |
||
|
|
5b52c2dbe6 | ||
|
|
2cac0cb2ca |
||
|
|
1ef7a9f67c |
||
|
|
745f03654c |
||
|
|
06e5dbd2db |
||
|
|
26b470d8a2 |
||
|
|
e23a0c9058 |
||
|
|
f8ccfa686c |
||
|
|
4cec1c0a91 |
||
|
|
8e852266b2 |
||
|
|
ae3c59cf89 |
||
|
|
82adb10fc0 |
||
|
|
32360a04b6 |
||
|
|
afe7deb29f |
||
|
|
37eaee257d |
||
|
|
5a60cfbe53 |
||
|
|
61e1bd29ad |
||
|
|
794fe96910 |
||
|
|
51eb6e0a87 |
||
|
|
0363d93e89 | ||
|
|
21da7c0b27 |
||
|
|
9aa03efdd6 |
||
|
|
d2e81c6629 |
||
|
|
8caff5c7fd |
||
|
|
2caad7db20 |
||
|
|
6047737118 |
||
|
|
c93331e8c8 | ||
|
|
b9216c0120 | ||
|
|
9f0e06774f | ||
|
|
513688c091 | ||
|
|
758a2639a1 |
||
|
|
a4f03d1b43 |
||
|
|
da0fdd238c |
||
|
|
5590b2f809 |
||
|
|
44a041bf74 |
||
|
|
17e5d07a84 |
||
|
|
9a6fbbd610 |
||
|
|
af50b48c68 |
||
|
|
6286b657ef |
||
|
|
6f4a13dbaf |
||
|
|
3e4b8d65e9 |
||
|
|
4fddde5cc9 |
||
|
|
aa280b042b |
||
|
|
718a7d32de |
||
|
|
a5136a6674 |
||
|
|
04c91de237 |
||
|
|
36841b4f12 |
||
|
|
03bcdfbfbf |
||
|
|
79f48b76dc |
||
|
|
5ef4eeac60 |
||
|
|
a1f29f1f71 |
||
|
|
eb3ba6b5a1 |
||
|
|
0084a70da5 | ||
|
|
ebec2292ce |
||
|
|
3db14bff8f |
||
|
|
6e22b26b46 |
||
|
|
0768731425 | ||
|
|
22e5afdfce |
||
|
|
55fafca735 |
||
|
|
e6d44912a7 | ||
|
|
33f15cb6df | ||
|
|
1c2d5ded76 |
||
|
|
8e7f9e2107 |
||
|
|
9befa59723 |
||
|
|
70fd5be601 |
||
|
|
1385d5dfb2 |
||
|
|
351147888c |
||
|
|
f60c8982a7 |
||
|
|
d8441bda83 |
||
|
|
93a71719b0 | ||
|
|
21d71f99fc |
||
|
|
3acd2301e7 |
||
|
|
8b7fe2b98a |
||
|
|
b47f81b641 |
||
|
|
c6b1f44bc9 |
||
|
|
106058826f |
||
|
|
7b6182722c | ||
|
|
6562bddfc1 |
||
|
|
77f5c30410 |
||
|
|
651f424029 |
||
|
|
7a1ca4f4c2 |
||
|
|
ea86289346 |
||
|
|
2f10530701 |
||
|
|
a0813db165 |
||
|
|
540a123e77 |
||
|
|
dfe9921aa0 |
||
|
|
9431c82637 |
||
|
|
0154056d5e | ||
|
|
9f140a4b23 |
||
|
|
e494a45717 | ||
|
|
435c68dca8 |
||
|
|
40e5281ee0 | ||
|
|
d288860ac8 |
||
|
|
ee1068ddf8 |
||
|
|
0a60b713db |
||
|
|
9ba456884c |
||
|
|
00b242368d |
||
|
|
99fb08e9d6 | ||
|
|
f92b806002 | ||
|
|
0f857fb92f |
||
|
|
4eda9c8c1c |
||
|
|
9f5e134d0a | ||
|
|
dcaf90396e |
||
|
|
c4af1434c0 |
||
|
|
91e77493ee |
||
|
|
9cc69e6a7d | ||
|
|
ec5a98109d | ||
|
|
52eaead284 | ||
|
|
686b41084e |
||
|
|
0a81642567 | ||
|
|
94d8321e2b |
||
|
|
424ddeb522 | ||
|
|
9f4327aa4b |
||
|
|
b82c71e1c5 |
||
|
|
76e54c63f8 |
||
|
|
ba8e6ee472 | ||
|
|
b05ebbf603 |
||
|
|
6867c56dae |
||
|
|
2d4789cb68 |
||
|
|
1dd2fc5ec1 |
||
|
|
38627b716f |
||
|
|
5dd2d52bbb |
||
|
|
74575c5541 |
||
|
|
fe238d0e2d |
||
|
|
634c91d1db |
||
|
|
a2d112392a |
||
|
|
841f0ee6b2 |
||
|
|
c598e4cf9b |
||
|
|
cbc0496577 |
||
|
|
050c8c371e |
||
|
|
4832da0f97 |
||
|
|
0c80fc9392 |
||
|
|
bf57e9804d |
||
|
|
60fb42c5e7 |
||
|
|
a68e391bb6 |
||
|
|
9d147a691e |
||
|
|
65a9bf9efe | ||
|
|
2eff902f69 | ||
|
|
61c08773bf |
||
|
|
67dbb137c9 | ||
|
|
a3d4412c8a | ||
|
|
9c29d6c408 | ||
|
|
d839546612 |
||
|
|
cf74cead29 | ||
|
|
3a0529300a |
||
|
|
6aa8c42487 | ||
|
|
f7a37df382 |
||
|
|
190967fd12 | ||
|
|
f7ca723c83 | ||
|
|
8c77d463d9 |
||
|
|
3fd3fe334e |
||
|
|
7dec7926d4 | ||
|
|
d903483f46 | ||
|
|
929dfba58d | ||
|
|
c90bd4cdd7 |
||
|
|
a5c18ee295 | ||
|
|
fdca1495f8 | ||
|
|
5a8d1faa5a |
||
|
|
0d82aa1c11 |
||
|
|
d00dd63d10 | ||
|
|
a12d8412f7 | ||
|
|
56e267c85d | ||
|
|
944a704ee7 | ||
|
|
83f9f43fde |
||
|
|
f62edc16e8 | ||
|
|
fca7922b3c |
||
|
|
581f5d4d37 |
||
|
|
539fa098d0 |
||
|
|
bca4148fb9 |
||
|
|
6b60ca4926 |
||
|
|
1ea4fdca30 |
||
|
|
9c87b56bb3 |
||
|
|
5b2258661d |
||
|
|
ddde5e6811 |
||
|
|
e6cdca6c22 |
||
|
|
1f71a85071 |
||
|
|
69e63a9a15 |
||
|
|
e73021eb20 |
||
|
|
d876382ae7 |
||
|
|
4651fe6c49 |
||
|
|
07ac838964 | ||
|
|
7c641bf258 | ||
|
|
e184121f5f | ||
|
|
88997bdccc | ||
|
|
7a15e18844 | ||
|
|
93a5e56813 |
||
|
|
ebcaf5e31f | ||
|
|
dbd33f2cdb |
||
|
|
aee17d7f6a |
||
|
|
be5e421e81 |
||
|
|
553656e5fa |
||
|
|
7580105910 | ||
|
|
4ab886490e | ||
|
|
369cfaa946 | ||
|
|
ef3803eba5 | ||
|
|
e6d672f817 |
||
|
|
6fdda4133e |
||
|
|
3f0f0684e2 |
||
|
|
e430a6d2da |
||
|
|
c197541964 |
||
|
|
6594a15442 |
||
|
|
d29e927856 |
||
|
|
337d5e5d96 | ||
|
|
f0655e7bd6 |
||
|
|
07807130ab |
||
|
|
6eb6df63b3 |
||
|
|
877df6b75e |
||
|
|
7ccb17c886 |
||
|
|
51455899dd |
||
|
|
bde5f2af70 |
||
|
|
7d84630e5c | ||
|
|
2211d1bf97 | ||
|
|
9e26136cb3 |
||
|
|
621c650d84 |
||
|
|
141c8df7b4 |
||
|
|
8917703189 |
||
|
|
a1135dd1ce |
||
|
|
8db8bcfcb2 |
||
|
|
a5508bb4ec |
||
|
|
78165eed83 |
||
|
|
58606e31dd |
||
|
|
2f3016537f |
||
|
|
f956c5d452 |
||
|
|
15fcd106f7 |
||
|
|
d74a30b194 |
||
|
|
ab3081b11d |
||
|
|
fb131df206 |
||
|
|
0dc61c825f |
||
|
|
956d64aa64 |
||
|
|
6a8ac88b67 |
||
|
|
7477f02f83 |
||
|
|
3fabd4a8d0 |
||
|
|
760b21da09 |
||
|
|
d383da804d |
||
|
|
54308e1c58 |
||
|
|
2729c72c76 |
||
|
|
840bc3d408 | ||
|
|
4cd0936596 |
||
|
|
9c63d20643 |
||
|
|
b5d655550f |
||
|
|
7368c5de47 | ||
|
|
3285ddac66 |
||
|
|
93280a65aa |
||
|
|
f91f135c0e |
||
|
|
aa0f168f9d |
||
|
|
fd07a012ca |
||
|
|
d3e833b14e | ||
|
|
c97ed5dcc1 |
||
|
|
af64ee4a12 |
||
|
|
2ce14936fb | ||
|
|
1fcdcaa239 |
||
|
|
5e9c1c31d3 | ||
|
|
b8402242cb | ||
|
|
9b64a5d685 |
||
|
|
3932ca66f2 | ||
|
|
bc160c5685 |
||
|
|
9c1ab60eeb |
||
|
|
7f76ba0fdc |
||
|
|
e1c77a4668 |
||
|
|
88fc302c05 |
||
|
|
125ef7c820 |
||
|
|
f29afcba6c |
||
|
|
26c7be73bd |
||
|
|
72835c67cd |
||
|
|
1852d04284 |
||
|
|
d1db1dd4a4 | ||
|
|
ff0566ca42 | ||
|
|
e87e2aa5c7 | ||
|
|
5d6b4b6c2e | ||
|
|
d52205ed1c | ||
|
|
65e926f458 | ||
|
|
999bb00114 | ||
|
|
374ecba632 |
||
|
|
9812f7f624 |
||
|
|
826693c79f |
||
|
|
3aa7f56d0c |
||
|
|
4eaaca55cf |
||
|
|
fad0ee9630 |
||
|
|
3b7c13dafd | ||
|
|
26afc32b69 | ||
|
|
be03d78ffd |
||
|
|
23ac7ae6ee | ||
|
|
2ca3e05a8e | ||
|
|
a20c53a1bc | ||
|
|
4ca22002b5 |
||
|
|
207e312741 |
||
|
|
c5799702e2 |
||
|
|
ca0cfd400d |
||
|
|
5633b077c2 |
||
|
|
b34492e98c |
||
|
|
4fc05ab24c |
||
|
|
3e57580c1d | ||
|
|
474a784df4 | ||
|
|
6d93efe94e |
||
|
|
b385b1ea2e |
||
|
|
df5c051ab5 |
||
|
|
ebfbf62e5e |
||
|
|
161ee6dec2 |
||
|
|
be13905042 | ||
|
|
b778966760 | ||
|
|
2069dbf6c3 | ||
|
|
8707cf296e | ||
|
|
712d61a63c | ||
|
|
a6cbcd9e6e | ||
|
|
9d723c6a44 | ||
|
|
94add2dd90 | ||
|
|
d471b798c9 |
||
|
|
bb1f245bc2 |
||
|
|
1fda255b69 |
||
|
|
f34e603213 |
||
|
|
4ffdecd123 |
||
|
|
7666ef9c85 |
||
|
|
b86bb3cf69 |
||
|
|
4f25c20072 |
||
|
|
45ad468a0e |
||
|
|
725bfe183c |
||
|
|
d3331b4d45 |
||
|
|
11ee8ba214 |
||
|
|
0b37fac065 |
||
|
|
85068a3181 |
||
|
|
fbe42ef1b2 |
||
|
|
c7901cbcd0 |
||
|
|
b3b195a7e1 |
||
|
|
01be1f5e0f |
||
|
|
ddf454f885 |
||
|
|
e9a17409dd |
||
|
|
6f9e70b647 |
||
|
|
ade70a121a |
||
|
|
d857bef9b6 |
||
|
|
8a5a3b7f8f |
||
|
|
96e5af27f6 |
||
|
|
a1541b533a |
||
|
|
c0a1f78075 |
||
|
|
ad29532caf |
||
|
|
99a5cf8805 |
||
|
|
3a24739a2f |
||
|
|
ffbbe27856 | ||
|
|
6f278a6c3a | ||
|
|
2a146a1c99 | ||
|
|
263905b71a | ||
|
|
ae1f2931bd | ||
|
|
758e6bcbf1 | ||
|
|
10bdbd30cb | ||
|
|
a04104630e |
||
|
|
59854c4c7c |
||
|
|
c0a876da83 |
||
|
|
17a48aae02 |
||
|
|
a7fd492f5a |
||
|
|
c2614ac892 |
||
|
|
d651a73928 |
||
|
|
5f0c689c95 | ||
|
|
6302fbb50b |
||
|
|
c177fdaac6 |
||
|
|
cd068e2e47 |
||
|
|
07e1c2ea83 | ||
|
|
85440777a3 |
||
|
|
25e0ac0cfd |
||
|
|
2d8d06fc3d |
||
|
|
1d70755766 |
||
|
|
5c5499bc6b |
||
|
|
38554ca2d7 |
||
|
|
ce341df6c3 |
||
|
|
1890a214e8 |
||
|
|
7d3a9947d2 |
||
|
|
a5fb797f9e |
||
|
|
19f7ce8187 |
||
|
|
f7e15e9c80 | ||
|
|
979df43032 |
||
|
|
830e6883bd | ||
|
|
cc49ce3c63 | ||
|
|
3e32074267 |
||
|
|
5f2deaa64c |
||
|
|
e20b9aef72 |
||
|
|
6cb12ea91b |
||
|
|
246c2208b0 |
||
|
|
59493b5f9e |
||
|
|
f0ca2dfe7f |
||
|
|
9bf8c1abe3 |
||
|
|
6ebd13aaf5 |
||
|
|
d562efe813 |
||
|
|
173c600f0b |
||
|
|
7506112daf |
||
|
|
f6bf0048b5 |
||
|
|
9fbf8cfdd4 |
||
|
|
c649a261b3 |
||
|
|
1dbaeb56d8 |
||
|
|
f9f8212562 |
||
|
|
adeed4c1a5 |
||
|
|
db719e0cb4 |
||
|
|
2e1e364e29 |
||
|
|
9dac3a6934 |
||
|
|
be39579115 |
||
|
|
b22d38ca07 | ||
|
|
657ebcb16e | ||
|
|
7e0a9f3b64 | ||
|
|
340d49a99f | ||
|
|
3b3690316b | ||
|
|
780c0d7a6d |
||
|
|
3cc2f1d004 |
||
|
|
9508bd8aa8 |
||
|
|
87b5a9a924 |
||
|
|
44b1b92f47 |
||
|
|
f7e10a6c4c |
||
|
|
be869dace6 |
||
|
|
3c25c88109 |
||
|
|
0c86e71b06 |
||
|
|
68a89d2473 |
||
|
|
90544ac77b |
||
|
|
8ab29be4f6 |
||
|
|
7bffe4c303 |
||
|
|
4338731dd8 |
||
|
|
66102f9720 |
||
|
|
f7d77819c1 |
||
|
|
aa6bd2417a |
||
|
|
cf03839ac7 |
||
|
|
96d4fa6577 |
||
|
|
c9a8a0bbf5 |
||
|
|
161f816093 |
||
|
|
1d657e899e |
||
|
|
d3c0ae26c5 |
||
|
|
fdc220febc |
||
|
|
045d956f40 |
||
|
|
ce84dedd29 |
||
|
|
6743b4d4af |
||
|
|
630cebd4cf |
||
|
|
499823aa65 | ||
|
|
b8abd7d27a | ||
|
|
d10e4dcc3a | ||
|
|
d0f190bf2a | ||
|
|
b71608a95f | ||
|
|
5a682e2889 | ||
|
|
135b1e8615 | ||
|
|
237d944536 |
||
|
|
4811873f5c |
||
|
|
132d7f2dee |
||
|
|
90e5af9089 |
||
|
|
8aede76b86 |
||
|
|
601fe1f36f |
||
|
|
fdfcfe8c9a |
||
|
|
63c8a72061 |
||
|
|
14ed2dd284 |
||
|
|
607475ff24 |
||
|
|
1d296b3728 |
||
|
|
ab1edfc0d7 | ||
|
|
6dc8da102b | ||
|
|
7bebbc9019 | ||
|
|
498b42540c |
||
|
|
3d580bc6c5 |
||
|
|
3dec08bee0 |
||
|
|
1ec58c9df4 | ||
|
|
3362e52382 | ||
|
|
a1908e8c84 | ||
|
|
60c89c887c |
||
|
|
13bab7f0c4 |
||
|
|
fc9a771b85 |
||
|
|
98077cf5ac |
||
|
|
52b78a99b4 |
||
|
|
91790f7612 |
||
|
|
1e94996dbb |
||
|
|
231fd76d6c |
||
|
|
4a2787c52d |
||
|
|
c0b29d5d30 |
||
|
|
071325903e |
||
|
|
83d1309409 |
||
|
|
955f5bb65e |
||
|
|
e806ba7cc9 |
||
|
|
023e3d3ca5 |
||
|
|
f4ef57b9cc |
||
|
|
07fe9ec53a |
||
|
|
d1bf9e9ba3 |
||
|
|
2665e1bcaa |
||
|
|
9e73ea23a5 |
||
|
|
a574dfcd37 |
||
|
|
396d43a036 |
||
|
|
bdb2c053dc |
||
|
|
16e75e8f99 |
||
|
|
837ac0ee04 | ||
|
|
1afc131979 |
||
|
|
2a9b2675d0 |
||
|
|
b69cd828f9 |
||
|
|
e2bf544fc6 |
||
|
|
1adfc3c60d |
||
|
|
bb78bc4a35 |
||
|
|
a981fb832b |
||
|
|
0045a6cee5 |
||
|
|
81a6f8459c |
||
|
|
e69b9ef7f3 |
||
|
|
06361349ba |
||
|
|
080fef93bb |
||
|
|
0238982a53 |
||
|
|
83f053c413 |
||
|
|
8e8395f1d9 |
||
|
|
bfb97d591a |
||
|
|
a16525a4f0 |
||
|
|
4f484ae7e5 |
||
|
|
09f3ce1ebd |
||
|
|
a1762c20c9 |
||
|
|
0f48489e5c |
||
|
|
74a5c5e3ff |
||
|
|
12ca4f64bc |
||
|
|
ef160366c7 |
||
|
|
bfda7d0a8e |
||
|
|
37b39e8622 |
||
|
|
f57f5d39b5 |
||
|
|
31fccce97f |
||
|
|
8bf930ffa6 | ||
|
|
0cff895c8e |
||
|
|
60ec9f3f1a |
||
|
|
66021c2828 |
||
|
|
d14ed459f7 |
||
|
|
2673fd5c70 |
||
|
|
fadef4ef39 |
||
|
|
c9eeed3d7d |
||
|
|
2d77c15fe1 |
||
|
|
ad4598ea1f |
||
|
|
467aa379b6 |
||
|
|
6991c2adb6 |
||
|
|
784678c8c2 |
||
|
|
a97f10c063 |
||
|
|
fb5a3f9d09 |
||
|
|
83b24d5f2e |
||
|
|
5b41af4d05 |
||
|
|
40603935ac |
||
|
|
f64e62758d |
||
|
|
de58efad11 |
||
|
|
cfd6835bfa |
||
|
|
374bf82e97 |
||
|
|
2b615d7bc4 |
||
|
|
d9c0e508a0 |
||
|
|
3a95292f97 | ||
|
|
da03c8bab2 |
||
|
|
efa828f8da |
||
|
|
f9e460222e |
||
|
|
beeaf94769 |
||
|
|
63deae41c5 |
||
|
|
49d6234bbe | ||
|
|
fc72efc639 |
||
|
|
5e27a52481 |
||
|
|
e84e08118b |
||
|
|
2e2114d778 |
||
|
|
9aeba115a6 |
||
|
|
4ec1acd769 |
||
|
|
4c61dc32f5 |
||
|
|
77a52be830 |
||
|
|
240dde1a97 |
||
|
|
a566733df0 |
||
|
|
c172baede7 |
||
|
|
1ed216ac6a |
||
|
|
647aafe921 |
||
|
|
f38ad6fe1d |
||
|
|
3519662fa8 |
||
|
|
30359cb684 |
||
|
|
87fa332082 |
||
|
|
b74947edff |
||
|
|
04b3ef597e |
||
|
|
89680fc217 |
||
|
|
ac056ddafa |
||
|
|
dc1ce193fa |
||
|
|
f0f6c51749 |
||
|
|
7052646e9a |
||
|
|
1da5b69cfe |
||
|
|
65349ecbcd |
||
|
|
b8f6688e81 |
||
|
|
f151f54df4 |
||
|
|
160c4c9bc4 |
||
|
|
619fc83996 |
||
|
|
5761e8df9b |
||
|
|
5300befea8 |
||
|
|
db15305185 |
||
|
|
fed1580e20 |
||
|
|
cf054c8808 |
||
|
|
c0fa888a5a |
||
|
|
25dfa8b075 |
||
|
|
e3eb8454d0 | ||
|
|
fc125a5f67 |
||
|
|
052f6d5166 |
||
|
|
88d9464f60 |
||
|
|
b198a707cc |
||
|
|
b3bfd21cf4 |
||
|
|
fd5eb599ec |
||
|
|
c8c0288dac | ||
|
|
0702ae4800 | ||
|
|
d1f5cd3822 | ||
|
|
f2c0c1d4a9 | ||
|
|
5fa7a0e426 |
||
|
|
af12a9a102 | ||
|
|
0ef8bdf7aa |
||
|
|
2c5aee846d |
||
|
|
fa69801e99 |
||
|
|
55af9605a4 |
||
|
|
36da706cc6 |
||
|
|
f4ad450b20 |
||
|
|
9295c8bcd7 |
||
|
|
1e8204b400 |
||
|
|
a25efe0286 |
||
|
|
91341a5a43 |
||
|
|
62df44fce8 |
||
|
|
cb83e84789 |
||
|
|
fb3286d9d0 |
||
|
|
29673ef74c |
||
|
|
897ef8af6f |
||
|
|
317c12b4a4 |
||
|
|
24a61c8b04 |
||
|
|
47980f635e |
||
|
|
d098b58528 |
||
|
|
88b6ab468c |
||
|
|
620e00b34a |
||
|
|
1966bfdb75 |
||
|
|
00633d2bcc |
||
|
|
ee31d1e4e3 |
||
|
|
c3fe1a4666 |
||
|
|
26aee1ef43 |
||
|
|
2e0a87a97b |
||
|
|
d6cb3ce59e |
||
|
|
70d278db68 |
||
|
|
56aedda47d |
||
|
|
252ca79b08 |
||
|
|
907a34780d |
||
|
|
0636dbb9f8 |
||
|
|
17b637d06c |
||
|
|
8dc818e0df |
||
|
|
0e5a58ca5f |
||
|
|
ac7eab55b9 |
||
|
|
97221d1a3c |
||
|
|
9ef748ec1d |
||
|
|
510afbe992 |
||
|
|
c79c8826d6 |
||
|
|
c70877dfdb |
||
|
|
f22f5e3672 |
||
|
|
4541552975 |
||
|
|
b3c1d4787c |
||
|
|
ee7fdd9184 |
||
|
|
66be4a2610 |
||
|
|
8ee7b0b4e9 |
||
|
|
0dbcfd62d7 |
||
|
|
3157744954 |
||
|
|
6cc8c6bdee |
||
|
|
aeddfdf1bf |
||
|
|
7603c0f767 |
||
|
|
146a15943c |
||
|
|
63707f343f | ||
|
|
3727469c8f |
||
|
|
0f22407f06 |
||
|
|
f4ed1e5c8f |
||
|
|
14fed78d16 |
||
|
|
8bead68916 | ||
|
|
6032b8e5e4 | ||
|
|
8a80cdc921 | ||
|
|
8ddbe68ebe | ||
|
|
e595973cb4 |
||
|
|
6515aaf2b2 |
||
|
|
5394f720b4 |
||
|
|
a04d597c04 |
||
|
|
9807b42665 |
||
|
|
0f42a6974a |
||
|
|
c8f109290a |
||
|
|
eb197ce933 |
||
|
|
1b3e1f380e |
||
|
|
4b12d034d1 |
||
|
|
a0b0d6e178 |
||
|
|
50a27d6883 |
||
|
|
5679cf66c6 |
||
|
|
b50d7af4b8 |
||
|
|
5c408b903d |
||
|
|
0f59a412e4 |
||
|
|
2a1ebbaa1d |
||
|
|
821587d045 |
||
|
|
527837bed1 |
||
|
|
ee4958abd0 |
||
|
|
56d2459317 |
||
|
|
e9c4f680a2 |
||
|
|
d1a7eb1cd6 |
||
|
|
15895fe036 | ||
|
|
f78082d679 | ||
|
|
f90a9e4a63 | ||
|
|
4f9332c2c2 |
||
|
|
e6e2f9ff66 |
||
|
|
255733c420 |
||
|
|
628fe0c87f |
||
|
|
dfa33cf82b | ||
|
|
dcfe757e30 |
||
|
|
f0fcf44620 | ||
|
|
f011a0776c | ||
|
|
584451d364 |
||
|
|
594eeed7d0 | ||
|
|
84b2363aec |
||
|
|
a834757db6 | ||
|
|
e626ac0871 | ||
|
|
03807ae290 | ||
|
|
4b7c96e429 |
||
|
|
d95217c3ac |
||
|
|
413634eca8 |
||
|
|
742f0b6744 |
||
|
|
748ea1e355 |
||
|
|
ff86c4b046 |
||
|
|
196814492d |
||
|
|
f737dd4d72 |
||
|
|
da9b34c25b |
||
|
|
0fc6166b40 | ||
|
|
0a7cb0d5f0 | ||
|
|
c894f469ba | ||
|
|
bd486ea21d |
||
|
|
9cad1db3f3 |
||
|
|
4b5a079288 |
||
|
|
4edca0678c |
||
|
|
8105eb788b |
||
|
|
f6e3c1b9b1 |
||
|
|
15d0ae7a9e |
||
|
|
0209d28ddf |
||
|
|
4c0615ad1b | ||
|
|
20683fa6c4 | ||
|
|
6a456c278d | ||
|
|
8b8a6c8f4b | ||
|
|
a070d1f179 | ||
|
|
9948472c41 | ||
|
|
d7f7d98c8f | ||
|
|
6e52a3d554 | ||
|
|
fdaeb2368d | ||
|
|
82223608d8 | ||
|
|
c0f2ea5beb | ||
|
|
ae2cf07fc5 | ||
|
|
b01d310601 | ||
|
|
1915ea04f4 | ||
|
|
897b658d14 | ||
|
|
3b10ea4bcb | ||
|
|
55833e9d60 |
||
|
|
1d5c1d2364 |
||
|
|
bae253bb29 |
||
|
|
c4abc241e7 |
||
|
|
178a575d54 | ||
|
|
f6a1c11566 |
||
|
|
2bfdb10c01 |
||
|
|
ff150c372a |
||
|
|
33f910c048 |
||
|
|
0dda83a6f1 | ||
|
|
e40951eec5 | ||
|
|
ae4d56d6ba | ||
|
|
d2098d3050 |
||
|
|
76aba1e548 |
||
|
|
6c1c530e8e |
||
|
|
2cda1265c8 |
||
|
|
6da6cd1b15 |
||
|
|
7855b9f355 | ||
|
|
2e44eefef5 | ||
|
|
2620c89302 | ||
|
|
bd5cec5c74 | ||
|
|
f950156527 | ||
|
|
5f43725ac4 | ||
|
|
72bf6fa08b | ||
|
|
e500d50068 |
||
|
|
4e954c0624 | ||
|
|
4c4b78044f |
||
|
|
ab56e2c89e | ||
|
|
38522e0579 | ||
|
|
c461df3c3a | ||
|
|
de8bd2e0a7 | ||
|
|
2de6e730ad |
||
|
|
2cbc35948b |
||
|
|
e8ea46da48 |
||
|
|
42cec15a7a |
||
|
|
d9524e689a |
||
|
|
b83619f08a |
||
|
|
83a832a39a |
||
|
|
87f7976dfe | ||
|
|
aa8840311b | ||
|
|
b4b160a145 |
||
|
|
ee4db0c62b |
||
|
|
8f67e33ada | ||
|
|
05dfeedd79 |
||
|
|
aa153cb6ad |
||
|
|
81ea79237e |
||
|
|
48b7496d54 | ||
|
|
6ac7fbfaca | ||
|
|
ab3077101e |
||
|
|
05f5f24aff |
||
|
|
29e57a4bdf |
||
|
|
84f143bc17 |
||
|
|
8fa40248bc | ||
|
|
133a60249a |
||
|
|
6d3e6ab7d3 |
||
|
|
cfc6ac5671 |
||
|
|
b6b73c22f3 | ||
|
|
9578de14b6 |
||
|
|
06fbeb4f75 |
||
|
|
1065084b6d |
||
|
|
4d8c0e0c05 | ||
|
|
e857158331 | ||
|
|
f8369ea6e2 | ||
|
|
cdbd44ca6e | ||
|
|
2664db85e7 | ||
|
|
c814057ee7 |
||
|
|
eff8796839 |
||
|
|
5e2aae9095 |
||
|
|
18c479ab21 |
||
|
|
9b547a2b62 | ||
|
|
355f56fab8 |
||
|
|
422d208182 |
||
|
|
ae59dcfbb8 | ||
|
|
33a04ad15d | ||
|
|
fd8b777312 |
||
|
|
24c1c9409c |
||
|
|
eb92af334f |
||
|
|
f7dae28726 |
||
|
|
359289a96f |
||
|
|
c2c029023d |
||
|
|
11ccc231e5 |
||
|
|
4e6ea38f2d |
||
|
|
ad0d480fac |
||
|
|
589ecbb915 |
||
|
|
cbd58d5341 |
||
|
|
9925a766bb |
||
|
|
493192a9a7 |
||
|
|
ec7635633a |
||
|
|
dd8541d189 |
||
|
|
928ac3032d |
||
|
|
d9044c03d7 |
||
|
|
0f8f25bd23 |
||
|
|
1b0b24c55e |
||
|
|
bf88bd0b90 |
||
|
|
271fd48866 |
||
|
|
79abf25e6e |
||
|
|
4701422065 |
||
|
|
565e991364 |
||
|
|
59a6605046 |
||
|
|
5f4e9e439a |
||
|
|
9cc1799f30 |
||
|
|
43c37d5a03 |
||
|
|
3ccbb29cde |
||
|
|
1eefb0159f |
||
|
|
a31c7a8c50 |
||
|
|
85bf9422c7 |
||
|
|
9bdaa62011 |
||
|
|
0dbd9f9930 |
||
|
|
6f59744bd0 |
||
|
|
2655fcc7cb |
||
|
|
622b55979a |
||
|
|
65e78c0ffe | ||
|
|
189cf4ae39 |
||
|
|
2b9b7cb81c |
||
|
|
d51fe9b9c5 |
||
|
|
bcd5df0c76 |
||
|
|
1193509485 |
||
|
|
a654d2a324 | ||
|
|
24006541b1 | ||
|
|
0060258021 | ||
|
|
95848fb5c2 | ||
|
|
064c329423 | ||
|
|
bf1ea93082 |
||
|
|
22054511f6 |
||
|
|
0855d7d93e |
||
|
|
54cb618593 |
||
|
|
678480a0d6 |
||
|
|
2335e84c43 | ||
|
|
8c55879c40 |
||
|
|
4dae79a665 |
||
|
|
384a155942 |
||
|
|
e358ba2013 |
||
|
|
f7ca3071f6 | ||
|
|
7f25afc4b7 | ||
|
|
6931fb422f | ||
|
|
4a3de42560 |
||
|
|
11e386dc3c |
||
|
|
39cb07a012 |
||
|
|
97d98e5dba |
||
|
|
b5cd560d37 |
||
|
|
eded82c2de |
||
|
|
2fdda45355 |
||
|
|
9540116146 |
||
|
|
429399f91a |
||
|
|
5c80fb83e1 |
||
|
|
36fa2003c7 | ||
|
|
7d879b3d04 |
||
|
|
a28dee26f8 | ||
|
|
2787754c43 | ||
|
|
8b2d71136b |
||
|
|
053f1eee0d |
||
|
|
e9f97fc244 |
||
|
|
9aa26dadd3 |
||
|
|
a15ac1ad39 |
||
|
|
8df133c703 |
||
|
|
80edde756b |
||
|
|
9b82148f5d |
||
|
|
2948d5e1c5 |
||
|
|
8001609bbd |
||
|
|
1a78cfe3c9 |
||
|
|
7ab6f39ab6 |
||
|
|
898f815963 |
||
|
|
bc74fa1e54 |
||
|
|
e955dadfdb |
||
|
|
30161d820b |
||
|
|
75287baa36 |
||
|
|
01209d4577 |
||
|
|
e86c3f7891 |
||
|
|
2fe88e68c0 |
||
|
|
3869bfc59a |
||
|
|
13a928a6e2 |
||
|
|
6ed6c1f36e |
||
|
|
018f115d94 |
||
|
|
45b22318c1 |
||
|
|
f7837f3d98 |
||
|
|
94d9e77519 |
||
|
|
3613af51cd |
||
|
|
f5b9d62778 |
||
|
|
d54df7c38e |
||
|
|
e066e1809d |
||
|
|
4ddc0b6cf7 |
||
|
|
78ff8d075c |
||
|
|
282ec9b691 |
||
|
|
89ec418f82 |
||
|
|
3a5971749f |
||
|
|
261d7bba33 |
||
|
|
5faf5f1804 |
||
|
|
23c021c431 |
||
|
|
2e5c2aa0b8 |
||
|
|
2a2bfd182d |
||
|
|
eb3af17d07 |
||
|
|
f6e46450f4 |
||
|
|
ad9297dca0 |
||
|
|
04611d710b |
||
|
|
47e866bd0f |
||
|
|
d7eb48657f |
||
|
|
e60aba0452 |
||
|
|
b48ac872cc |
||
|
|
4eb4fc9060 |
||
|
|
2c12e8e952 |
||
|
|
5ae2ed405d |
||
|
|
b77dd8b71c |
||
|
|
61bddf711d |
||
|
|
8f6a857fc0 |
||
|
|
190942c67f |
||
|
|
7bffd15dff | ||
|
|
eb2e0fb15a |
||
|
|
bef938ad8e |
||
|
|
9607176475 |
||
|
|
bf15806cc9 |
||
|
|
34ea50de5a |
||
|
|
9def319125 |
||
|
|
fb04a25291 |
||
|
|
26e182e4e8 |
||
|
|
d5b6a135c8 | ||
|
|
c074a22355 | ||
|
|
67d13f140d | ||
|
|
5fc63cca5c | ||
|
|
7750a19abd |
||
|
|
fa5240f70d | ||
|
|
1c85948049 | ||
|
|
eeca54b7bb | ||
|
|
a440f3d2fc |
||
|
|
10dc650a74 | ||
|
|
0f9ba4693e |
||
|
|
c757f1a506 |
||
|
|
1a20417081 |
||
|
|
07759732fa |
||
|
|
d8e20b522e |
||
|
|
ee6994f524 |
||
|
|
d5fb9d546e |
||
|
|
78ebb7c2c5 |
||
|
|
44be2a1670 |
||
|
|
4e88658094 |
||
|
|
4089a8e36a |
||
|
|
7b15cb5919 |
||
|
|
c5848c91e6 |
||
|
|
1580e25a2d |
||
|
|
5b5afe5a64 |
||
|
|
7e155c396f |
||
|
|
9459a3e1c4 |
||
|
|
da197e219b | ||
|
|
7883e2f3cb |
||
|
|
e74e9c82ac | ||
|
|
cfa9011906 | ||
|
|
f549d33855 |
||
|
|
57d9cdd50b |
||
|
|
a6ee1c3e63 |
||
|
|
400a88d84f |
||
|
|
a07c7eb3dd |
||
|
|
d7bbed124d |
||
|
|
054796024b | ||
|
|
4a947e0bfb |
||
|
|
f643f1dacf |
||
|
|
b3d754f3b2 |
||
|
|
50e3e78a63 |
||
|
|
99a8460dce |
||
|
|
1c112ee81b |
||
|
|
e6c3bb8649 | ||
|
|
6ae9c519bc | ||
|
|
6ba24f6270 |
||
|
|
4d29bd40e8 | ||
|
|
bfbabdc666 | ||
|
|
06cff88759 | ||
|
|
8705fd817b |
||
|
|
f83987f1f2 | ||
|
|
b678bff339 | ||
|
|
f347d50843 |
||
|
|
9bac34748f | ||
|
|
39fae0aea2 | ||
|
|
cac50adcd9 | ||
|
|
6190d76191 |
||
|
|
ada01dff42 |
||
|
|
7e58d05819 | ||
|
|
33f979ddcf |
||
|
|
02d08d50d1 |
||
|
|
d60b3c4327 |
||
|
|
d8b30a7f52 |
||
|
|
7d34061c07 |
||
|
|
bb05703ec1 |
||
|
|
8049812b80 |
||
|
|
579a270830 | ||
|
|
b0348a9b3b |
||
|
|
bfdb734481 |
||
|
|
b73d3e7328 |
||
|
|
77b4da2683 |
||
|
|
e62419666c |
||
|
|
f0c1dae0e1 | ||
|
|
45848a4819 | ||
|
|
43443d43b4 | ||
|
|
48c9181dbe | ||
|
|
72b6cf88f8 | ||
|
|
8082025750 | ||
|
|
68ed81bc4a |
||
|
|
03e1a5eb09 |
||
|
|
befb529fc7 |
||
|
|
e729e98fd1 |
||
|
|
052d67b0ac |
||
|
|
34b8095168 | ||
|
|
b925942f71 |
||
|
|
20c669d215 |
||
|
|
44c12cc447 |
||
|
|
beceefb18f | ||
|
|
bf4a7dc5cd | ||
|
|
d2a594c2ed | ||
|
|
d01e7a4c88 | ||
|
|
6729a1606a |
||
|
|
c6d195f143 |
||
|
|
363480cd68 | ||
|
|
0b38a6e033 | ||
|
|
cc646f1a6a | ||
|
|
f3f611dbc9 |
||
|
|
64f775e51c |
||
|
|
3764132e2a |
||
|
|
f3a318e9ac |
||
|
|
fd18abed35 |
||
|
|
621bf60604 |
||
|
|
d1095404e2 |
||
|
|
f85ad7d102 |
||
|
|
e0d181e409 |
||
|
|
fa7139628b |
||
|
|
4ba71d18e2 |
||
|
|
4c6621d8d0 |
||
|
|
372a8899e0 |
||
|
|
abfaba38de |
||
|
|
f8788096d2 |
||
|
|
6d0d1ae6d8 |
||
|
|
9fa5ebd483 |
||
|
|
ad39df22f9 | ||
|
|
cd6ea05ff3 |
||
|
|
930849c163 |
||
|
|
42106ac4fc |
||
|
|
dfe7e52cd0 |
||
|
|
22ed7415aa |
||
|
|
a5a3d9cae6 |
||
|
|
3d6d84a617 | ||
|
|
f2ca79cd37 | ||
|
|
527a907689 | ||
|
|
d7cd243706 |
||
|
|
e51513de5f |
||
|
|
0923e214c6 |
||
|
|
8ebf77b401 | ||
|
|
64dff49d11 | ||
|
|
fa8994aef1 |
||
|
|
7c3d09a5e3 | ||
|
|
31bc67e220 | ||
|
|
5c03245dc2 |
||
|
|
acd905c6ec |
||
|
|
52114fe575 |
||
|
|
ad17c3dd5d |
||
|
|
3272d35f98 |
||
|
|
7c75cd50fd | ||
|
|
019e4c417f | ||
|
|
0efa49af8c |
||
|
|
ac672d96ce |
||
|
|
a97d645db4 |
||
|
|
8e6afa382f | ||
|
|
029acf66d3 |
||
|
|
fd20e7d23d | ||
|
|
e09542a9a4 | ||
|
|
109f3ffbc9 | ||
|
|
421384004c | ||
|
|
778d7f7f52 |
||
|
|
151c343069 |
||
|
|
bac98ac067 |
||
|
|
dedd8c0455 |
||
|
|
6c512c7964 |
||
|
|
d87f5b3236 |
||
|
|
a07345eb8d |
||
|
|
b328916152 |
||
|
|
feafb7f1e4 |
||
|
|
17a6656f07 |
||
|
|
157bfbc51f |
||
|
|
7736508998 |
||
|
|
2d48daafe0 |
||
|
|
c6c75523a2 |
||
|
|
de4b079cf3 | ||
|
|
b826a54777 |
||
|
|
64d0299619 |
||
|
|
136e490732 |
||
|
|
c56a6f6f87 |
||
|
|
8d9a5834b5 | ||
|
|
0796c7d1b1 |
||
|
|
17b854d919 |
||
|
|
de83d90e26 |
||
|
|
ea05f48514 |
||
|
|
05fae9e681 |
||
|
|
1c71ddb4db |
||
|
|
06597064d3 |
||
|
|
87fd2f5338 |
||
|
|
3caf767e6c |
||
|
|
b43a4615d2 |
||
|
|
4e90e1b17c |
||
|
|
b78e630c83 |
||
|
|
c62ad0eb36 |
||
|
|
9b9a81e10c |
||
|
|
363df04e47 |
||
|
|
9961bdc7a2 |
||
|
|
37a96980ce | ||
|
|
8f8f556167 |
||
|
|
7d4e25b2a2 |
||
|
|
c6de23daed |
||
|
|
1c99d73b7c |
||
|
|
10f12f45dd |
||
|
|
8ca6e69574 |
||
|
|
5a63869714 |
||
|
|
4780577a6c |
||
|
|
70247ef369 | ||
|
|
9b521cf513 | ||
|
|
c06bde1d68 |
||
|
|
37c09f9ac6 | ||
|
|
4ced64412e | ||
|
|
2cccec28c2 |
||
|
|
e7401a1f17 |
||
|
|
33ba195f48 |
||
|
|
b7850f8d85 | ||
|
|
5428e17372 | ||
|
|
aba6af3486 | ||
|
|
0b9eabc33d | ||
|
|
3526951917 |
||
|
|
238a563f2b | ||
|
|
4ce499553d | ||
|
|
414f9918bd | ||
|
|
684ee331ef | ||
|
|
0f64f23dc7 | ||
|
|
d93e3b498e |
||
|
|
84e87f71c2 |
||
|
|
72b64f096f |
||
|
|
ed33e613eb |
||
|
|
ccfc3ef3b5 |
||
|
|
9977cb6c33 |
||
|
|
6a6f92ffae |
||
|
|
c4b84b8bdb |
||
|
|
838034927b | ||
|
|
9f742390a1 | ||
|
|
75b2d098fe |
||
|
|
c6f9bffb1d | ||
|
|
17cd6b9573 | ||
|
|
1a18b4a277 |
||
|
|
3bd139302e |
||
|
|
9f8b45728e |
||
|
|
a2ae18e5a1 |
||
|
|
18be429f50 |
||
|
|
84ba6e77ab |
||
|
|
be56c87cb2 |
||
|
|
8784764ac5 |
||
|
|
02e8ebb022 |
||
|
|
1085ec96d1 | ||
|
|
a6df57f186 | ||
|
|
9eb03288da | ||
|
|
c13e1a22e9 | ||
|
|
5882a70047 |
||
|
|
5a61187c4b |
||
|
|
dbd2619425 |
||
|
|
d71a07696f |
||
|
|
56e0eb51a8 |
||
|
|
5d0f0ce02c |
||
|
|
8eb8aa1f04 |
||
|
|
fde97a1c57 | ||
|
|
d416bae8e7 |
||
|
|
3361d80cd5 |
||
|
|
138379bd31 |
||
|
|
eac72936df |
||
|
|
aaf5d0a566 |
||
|
|
670870577c | ||
|
|
9760200a06 |
||
|
|
9aee3a6bfa |
||
|
|
db7bea9fd2 | ||
|
|
d209cc8278 |
||
|
|
684555f79b | ||
|
|
b9bc352372 |
||
|
|
ac5ded0193 |
||
|
|
9cc176efc5 |
||
|
|
7abaea151b |
||
|
|
622ea98ce7 |
||
|
|
a4aab2ecf3 |
||
|
|
60602bb9b9 |
||
|
|
da37301253 |
||
|
|
b2426ce450 |
||
|
|
216761e520 |
||
|
|
399461f972 |
||
|
|
8b38b91873 |
||
|
|
3d14f5f951 | ||
|
|
38aaecbef8 | ||
|
|
e4abc917a2 | ||
|
|
1caa1f08ca |
||
|
|
7f7917b416 |
||
|
|
cc1cd817c6 | ||
|
|
b9260dd5b6 |
||
|
|
a444a8c1c5 |
||
|
|
e18231ee59 |
||
|
|
59f0337380 | ||
|
|
7ef7e0eaae | ||
|
|
0ef99c1f75 | ||
|
|
48b5365ba5 |
||
|
|
a95844f033 |
||
|
|
51a671783e |
||
|
|
d888536966 |
||
|
|
5114fdef48 |
||
|
|
574705209f |
||
|
|
14139dcbca |
||
|
|
3e0fef4b8e |
||
|
|
618014bd26 | ||
|
|
6ffba9d40e | ||
|
|
a1387edb42 |
||
|
|
01b9f244ca |
||
|
|
5735aa57b0 |
||
|
|
8f66647ad2 | ||
|
|
69f35faa7e | ||
|
|
53d8520998 | ||
|
|
5058d8bcb3 |
||
|
|
db1a1a5fc8 |
||
|
|
9785944558 |
||
|
|
410e6a0720 |
||
|
|
a283202c9c |
||
|
|
1d985bd254 |
||
|
|
167b9b746e |
||
|
|
637912e408 | ||
|
|
8322123cf1 |
||
|
|
0bf83d08f9 |
||
|
|
6434e51941 |
||
|
|
7e4da6b9b4 |
||
|
|
2bcc026f31 |
||
|
|
654628e93b | ||
|
|
bffef62a5c |
||
|
|
f6bb364a63 | ||
|
|
540402852c |
||
|
|
16d03a0e90 |
||
|
|
bb292f4f4b | ||
|
|
b199086be5 | ||
|
|
06897b2bc7 | ||
|
|
461eaa61a1 | ||
|
|
d6d0b94a69 | ||
|
|
dfec55cb07 | ||
|
|
e585a70fe8 | ||
|
|
c28e767644 | ||
|
|
17b18c4ca7 | ||
|
|
5e5779ac70 | ||
|
|
c765e8edc9 |
||
|
|
278de6bd8a |
||
|
|
6a1fee9acc | ||
|
|
56d328de75 |
||
|
|
6f8e388a19 |
||
|
|
dae8a29fe5 | ||
|
|
f3e9d6c0e6 | ||
|
|
ed71ae00b8 |
||
|
|
b8d1c04b79 | ||
|
|
8d6558aa0c |
||
|
|
5e5718ce8c |
||
|
|
8829e65458 |
||
|
|
88de115c54 |
||
|
|
a35e5817c4 |
||
|
|
b2a3de1d45 | ||
|
|
77a7abf98d | ||
|
|
e049bc6ec5 |
||
|
|
8be24250eb |
||
|
|
c5ea5aa2dd |
||
|
|
7048ecaf4a |
||
|
|
1e77f48685 |
||
|
|
88eaee3cc1 |
||
|
|
787c7d1f63 |
||
|
|
2acb494849 |
||
|
|
3f18b8249f |
||
|
|
09ad83bedc |
||
|
|
388fdef53a |
||
|
|
fef6ab8c28 |
||
|
|
5fb4c2c3b8 |
||
|
|
6f31025110 |
||
|
|
02dbd757ab |
||
|
|
9e1e776230 | ||
|
|
bfab91f0c7 |
||
|
|
6248c892eb |
||
|
|
e08c876cd8 |
||
|
|
9daa469603 | ||
|
|
c76c00aa27 | ||
|
|
f234b506c3 |
||
|
|
488fce1f70 |
||
|
|
67485180a1 |
||
|
|
26151a9c8e |
||
|
|
a12cf41195 |
||
|
|
1bc9357822 |
||
|
|
c850dd1199 |
||
|
|
75ae571a01 |
||
|
|
db40ccbca0 |
||
|
|
27376ac499 |
||
|
|
cbcf59ca69 |
||
|
|
0feeaccf7b |
||
|
|
0c4ed54eb9 |
||
|
|
a4c0cdc148 |
||
|
|
3236d01793 |
||
|
|
861b427703 |
91 changed files with 36101 additions and 22160 deletions
|
|
@ -1,6 +0,0 @@
|
|||
[target.x86_64-pc-windows-msvc]
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
|
||||
[target.x86_64-unknown-linux-gnu]
|
||||
linker = "clang"
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
2
.cargo/config.toml
Normal file
2
.cargo/config.toml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
[target.x86_64-pc-windows-msvc]
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
version = 1
|
||||
|
||||
[[analyzers]]
|
||||
name = "rust"
|
||||
|
||||
[analyzers.meta]
|
||||
msrv = "stable"
|
||||
|
||||
[[transformers]]
|
||||
name = "rustfmt"
|
||||
94
.github/ISSUE_TEMPLATE/1-bug-report.yml
vendored
Normal file
94
.github/ISSUE_TEMPLATE/1-bug-report.yml
vendored
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
name: Bug Report
|
||||
description: You have an issue with the UAD-ng software itself
|
||||
title: "bug(scope): "
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
|
||||
Wherever you see "If relevant", you can omit the field, if you prefer privacy.
|
||||
If we believe it's relevant to the bug, we may later ask you to fill the info.
|
||||
|
||||
Examples:
|
||||
- If your OS is Windows or Mac, then [WM](https://en.wikipedia.org/wiki/Window_manager)/[WS](https://en.wikipedia.org/wiki/Windowing_system) and [DE](https://en.wikipedia.org/wiki/Desktop_environment) are irrelevant, because it's implied by the OS.
|
||||
- If the bug is related to ADB, then it's unlikely to be related to GUI, DE, or WM. No need to specify them.
|
||||
- If you use the latest-stable official version of any software (such as a [distro](https://en.wikipedia.org/wiki/Linux_distribution)), you can omit the version number.
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: A clear and concise description of what the bug is, and how to reproduce it
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: A clear and concise description of what you expected to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: suggested-solution
|
||||
attributes:
|
||||
label: You have a solution?
|
||||
description: What to do to fix the issue.
|
||||
- type: dropdown
|
||||
id: kernel # technically accurate
|
||||
attributes:
|
||||
label: Operating System
|
||||
options:
|
||||
- Linux
|
||||
- MacOS # Darwin+XNU
|
||||
- Windows # NT
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: os-v
|
||||
attributes:
|
||||
label: Distro version
|
||||
description: If relevant, please fill it
|
||||
placeholder: Win11 Home, Debian 12, etc...
|
||||
- type: input
|
||||
id: ws-and-wm
|
||||
attributes:
|
||||
label: Window System + Manager
|
||||
description: If relevant, please fill it
|
||||
placeholder: Wayland 1.23 + Mutter 46, xorg + i3, etc...
|
||||
- type: input
|
||||
id: de
|
||||
attributes:
|
||||
label: Desktop Environment
|
||||
description: If relevant, please fill it
|
||||
placeholder: GNOME 46, KDE Plasma 4, etc...
|
||||
- type: input
|
||||
id: adb-v
|
||||
attributes:
|
||||
label: ADB version
|
||||
description: If relevant, please fill it
|
||||
placeholder: 1.0.41
|
||||
- type: textarea
|
||||
id: logfiles
|
||||
attributes:
|
||||
label: Provide logs
|
||||
description: "Paste the relevant (notice the line timestamps) slice (contiguous range) of [the logfile generated by UAD-ng](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/wiki/FAQ#where-are-the-logfiles)"
|
||||
value: |
|
||||
<details><summary>Log</summary>
|
||||
|
||||
```
|
||||
Please replace this line by the log-file contents
|
||||
```
|
||||
</details>
|
||||
- type: checkboxes
|
||||
id: acknowledgements
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
description: "Your bug report will be closed if you don't follow the checklist below."
|
||||
options:
|
||||
- label: This issue is not a duplicate of an existing bug report.
|
||||
required: true
|
||||
- label: I have chosen an appropriate title.
|
||||
required: true
|
||||
- label: All requested information has been provided properly.
|
||||
required: true
|
||||
25
.github/ISSUE_TEMPLATE/2-feature-request.yml
vendored
Normal file
25
.github/ISSUE_TEMPLATE/2-feature-request.yml
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
name: Feature Request
|
||||
description: You want a new feature for UAD-ng
|
||||
title: "feat(scope): "
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: textarea
|
||||
id: feature-description
|
||||
attributes:
|
||||
label: Describe the feature you want
|
||||
description: A clear and concise description of the feature you want.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: acknowledgements
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
description: Your feature request will be closed if you don't follow the checklist below.
|
||||
options:
|
||||
- label: This issue is not a duplicate of an existing feature request.
|
||||
required: true
|
||||
- label: I have chosen an appropriate title.
|
||||
required: true
|
||||
- label: All requested information has been provided properly.
|
||||
required: true
|
||||
56
.github/ISSUE_TEMPLATE/3-add-new-packages.yml
vendored
Normal file
56
.github/ISSUE_TEMPLATE/3-add-new-packages.yml
vendored
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
name: Add new package(s)
|
||||
description: You want to add new apps to the debloat list
|
||||
title: "pkg(scope): "
|
||||
labels: ["package::addition"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Consider submitting a Pull Request
|
||||
|
||||
We receive many package-related issues and it's hard to keep up. **Consider submitting a Pull Request yourself**—it helps us process changes much faster! See our [guide on how to contribute](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/wiki/How-to-contribute) for instructions.
|
||||
|
||||
---
|
||||
|
||||
## Document each package the best you can
|
||||
|
||||
**List**: `Google`|`Misc`|`OEM` (manufacturer) |`AOSP`|`Pending`|`Carrier` (ISP).
|
||||
|
||||
**Removal**: `Recommended`|`Advanced`|`Expert`|`Unsafe`. See the [Removal Definitions](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/wiki/Debloat-Lists#removal-definitions) on the wiki for details. **Please ensure you follow these definitions when proposing changes.**
|
||||
|
||||
- type: textarea
|
||||
id: package-listing
|
||||
attributes:
|
||||
label: List your packages
|
||||
description: Put the package name, list, removal and description of the package. Copy & paste the below default values when adding multiple packages.
|
||||
value: |
|
||||
- Package name:
|
||||
- List:
|
||||
- Removal:
|
||||
- Description:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: removal-definitions
|
||||
attributes:
|
||||
label: Removal Definitions
|
||||
description: Please ensure you follow the removal definitions when proposing changes.
|
||||
options:
|
||||
- label: I've read the [Removal Definitions](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/wiki/Debloat-Lists#removal-definitions)
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: acknowledgements
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
description: Your bug report will be closed if you don't follow the checklist below.
|
||||
options:
|
||||
- label: I removed all those packages on my phone. If not, why? Explain in the issue.
|
||||
- label: I'm willing to submit a Pull Request for this change.
|
||||
- label: This issue is not a duplicate of an existing one.
|
||||
required: true
|
||||
- label: I have chosen an appropriate title.
|
||||
required: true
|
||||
- label: All requested information has been provided properly.
|
||||
required: true
|
||||
47
.github/ISSUE_TEMPLATE/4-debloat-issue-report.yml
vendored
Normal file
47
.github/ISSUE_TEMPLATE/4-debloat-issue-report.yml
vendored
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
name: Debloat issue report
|
||||
description: Your phone has unexpected issues after debloating
|
||||
title: "pkg(scope): "
|
||||
labels: ["package::breakage"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Consider submitting a Pull Request
|
||||
|
||||
If you know which package caused the issue and how the documentation should be updated, **consider submitting a Pull Request yourself**—it helps us process changes much faster! See our [guide on how to contribute](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/wiki/How-to-contribute) for instructions.
|
||||
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: Describe the problem
|
||||
description: A clear and concise description of what the problem is, including details (e.g. Android version, phone model)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: suggested-solution
|
||||
attributes:
|
||||
label: You have a solution?
|
||||
description: What to do to fix the issue.
|
||||
|
||||
- type: textarea
|
||||
id: logfiles
|
||||
attributes:
|
||||
label: Provide logs
|
||||
description: Upload the logfile generated by UAD-ng. See https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/wiki/FAQ#where-are-the-logfiles for the location of logfiles.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: acknowledgements
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
description: Your bug report will be closed if you don't follow the checklist below.
|
||||
options:
|
||||
- label: I'm willing to submit a Pull Request to fix the package documentation.
|
||||
- label: This issue is not a duplicate of an existing one.
|
||||
required: true
|
||||
- label: I have chosen an appropriate title.
|
||||
required: true
|
||||
- label: All requested information has been provided properly.
|
||||
required: true
|
||||
58
.github/ISSUE_TEMPLATE/5-update-apps-description-or-recommendation.yml
vendored
Normal file
58
.github/ISSUE_TEMPLATE/5-update-apps-description-or-recommendation.yml
vendored
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
name: Update apps description or recommendation
|
||||
description: You want to improve/update a description/recommendation
|
||||
title: "pkg(scope): "
|
||||
labels: ["package::documentation"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Consider submitting a Pull Request
|
||||
|
||||
We receive many package documentation updates and it's hard to keep up. **Consider submitting a Pull Request yourself**—it helps us process changes much faster! See our [guide on how to contribute](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/wiki/How-to-contribute) for instructions.
|
||||
|
||||
---
|
||||
|
||||
## Document each package the best you can
|
||||
|
||||
**List**: `Google`|`Misc`|`OEM` (manufacturer) |`AOSP`|`Pending`|`Carrier` (ISP).
|
||||
|
||||
**Removal**: `Recommended`|`Advanced`|`Expert`|`Unsafe`. See the [Removal Definitions](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/wiki/Debloat-Lists#removal-definitions) on the wiki for details. **Please ensure you follow these definitions when proposing changes.**
|
||||
|
||||
- type: textarea
|
||||
id: package-listing
|
||||
attributes:
|
||||
label: List your packages
|
||||
description: Put the package name, list, removal and description of the package. Copy & paste the below default values when adding multiple packages.
|
||||
value: |
|
||||
- Package name:
|
||||
- Current list:
|
||||
- Proposed list:
|
||||
- Current removal:
|
||||
- Proposed removal:
|
||||
- Current description:
|
||||
- Proposed description:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: removal-definitions
|
||||
attributes:
|
||||
label: Removal Definitions
|
||||
description: Please ensure you follow the removal definitions when proposing changes.
|
||||
options:
|
||||
- label: I've read the [Removal Definitions](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/wiki/Debloat-Lists#removal-definitions)
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: acknowledgements
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
description: Your bug report will be closed if you don't follow the checklist below.
|
||||
options:
|
||||
- label: I'm willing to submit a Pull Request for this change.
|
||||
- label: This issue is not a duplicate of an existing one.
|
||||
required: true
|
||||
- label: I have chosen an appropriate title.
|
||||
required: true
|
||||
- label: All requested information has been provided properly.
|
||||
required: true
|
||||
25
.github/ISSUE_TEMPLATE/6-documentation.yml
vendored
Normal file
25
.github/ISSUE_TEMPLATE/6-documentation.yml
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
name: UAD-ng documentation
|
||||
description: You want to improve the documentation of UAD-ng which is either a Markdown file or the Wiki
|
||||
title: "docs(scope): "
|
||||
labels: ["documentation"]
|
||||
body:
|
||||
- type: textarea
|
||||
id: documentation-description
|
||||
attributes:
|
||||
label: Describe the documentation you want to change
|
||||
description: A clear description of what you want to change.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: acknowledgements
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
description: Your feature request will be closed if you don't follow the checklist below.
|
||||
options:
|
||||
- label: This issue is not a duplicate of an existing documentation change request.
|
||||
required: true
|
||||
- label: I have chosen an appropriate title.
|
||||
required: true
|
||||
- label: All requested information has been provided properly.
|
||||
required: true
|
||||
38
.github/ISSUE_TEMPLATE/7-ci.yml
vendored
Normal file
38
.github/ISSUE_TEMPLATE/7-ci.yml
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
name: CI/CD
|
||||
description: File an issue for CI/CD
|
||||
title: "ci(scope): "
|
||||
labels: ["ci"]
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: A clear and concise description of what the problem is and how it could be approached.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Alternate solutions
|
||||
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Anything else?
|
||||
description: |
|
||||
Links? References? Anything that will give us more context about the issue you are encountering!
|
||||
|
||||
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
|
||||
validations:
|
||||
required: false
|
||||
- type: checkboxes
|
||||
id: acknowledgements
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
description: Your issue will be closed if you don't follow the checklist below.
|
||||
options:
|
||||
- label: This issue is not a duplicate of an existing issue.
|
||||
required: true
|
||||
- label: I have chosen an appropriate title.
|
||||
required: true
|
||||
- label: All requested information has been provided properly.
|
||||
required: true
|
||||
34
.github/ISSUE_TEMPLATE/add-new-package-s-.md
vendored
34
.github/ISSUE_TEMPLATE/add-new-package-s-.md
vendored
|
|
@ -1,34 +0,0 @@
|
|||
---
|
||||
name: Add new package(s)
|
||||
about: You want to add new apps in the debloat list
|
||||
title: ""
|
||||
labels: package::addition
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**Your phone model:**
|
||||
|
||||
**Packages:**
|
||||
|
||||
```
|
||||
com.this.is.a.bad.application
|
||||
com.this.is.another.bad.application
|
||||
...
|
||||
```
|
||||
|
||||
- [ ] **I removed all those packages on my phone**
|
||||
If not why. Leave the brackets blank and explain why.
|
||||
|
||||
## Document each package the best you can
|
||||
|
||||
**List**: `Google`|`Misc`|`OEM` (manufacturer)|`AOSP`|`Pending`|`Carrier` (isp).
|
||||
|
||||
**Removal**: `Recommended`, `Advanced`, `Expert` (this can break important features),
|
||||
or `Unsafe` (this can bootloop the phone or break extremely important features).
|
||||
|
||||
### \<package name\>
|
||||
|
||||
**List**: \<list\>
|
||||
**Removal**: \<recommendation list\>
|
||||
|
||||
> Description. Link to its Playstore page if it exists.
|
||||
19
.github/ISSUE_TEMPLATE/bug_report.md
vendored
19
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
|
@ -1,19 +0,0 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: You have an issue with the UAD software itself
|
||||
title: ""
|
||||
labels: bug
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**You have a solution?**
|
||||
What to do to fix the issue.
|
||||
|
||||
**UAD log**
|
||||
Upload the logfile generated by UAD.
|
||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
|
|
@ -1 +1 @@
|
|||
blank_issues_enabled: false
|
||||
blank_issues_enabled: false
|
||||
18
.github/ISSUE_TEMPLATE/debloat-issue-report.md
vendored
18
.github/ISSUE_TEMPLATE/debloat-issue-report.md
vendored
|
|
@ -1,18 +0,0 @@
|
|||
---
|
||||
name: Debloat issue report
|
||||
about: Your phone has unexpected issues after debloating
|
||||
title: ""
|
||||
labels: package::breakage
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**Your phone model**:
|
||||
|
||||
**Describe the issue**
|
||||
A clear and concise description of what the problem is.
|
||||
|
||||
**You have a solution?**
|
||||
What to do to fix the issue.
|
||||
|
||||
**UAD log**
|
||||
Upload the logfile generated by UAD. It would be difficult to help you without it.
|
||||
10
.github/ISSUE_TEMPLATE/feature_request.md
vendored
10
.github/ISSUE_TEMPLATE/feature_request.md
vendored
|
|
@ -1,10 +0,0 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: You want a new feature
|
||||
title: ""
|
||||
labels: enhancement
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**Describe the feature you want**
|
||||
A clear description of what you want to happen.
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
---
|
||||
name: Update apps description or recommendation
|
||||
about: You want to improve/update a description/recommendation
|
||||
title: ""
|
||||
labels: package::documentation
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**Your phone model**:
|
||||
|
||||
**Packages documentation to update:**
|
||||
|
||||
```
|
||||
com.this.is.a.application
|
||||
com.this.is.another.application
|
||||
...
|
||||
```
|
||||
|
||||
## Documentation Change
|
||||
|
||||
**List**: `Google`|`Misc`|`OEM` (manufacturer)|`AOSP`|`Pending`|`Carrier` (isp).
|
||||
**Removal**: `Recommended`, `Advanced`, `Expert` (this can break important features),
|
||||
or `Unsafe` (this can bootloop the phone or break extremely important features).
|
||||
|
||||
### \<package name\>
|
||||
|
||||
**List**: \<current list\> :arrow_right: \<proposed list\>
|
||||
**Removal**: \<current recommendation list\>
|
||||
:arrow_right: \<proposed recommendation list\>
|
||||
|
||||
### Current description
|
||||
|
||||
> Current description
|
||||
|
||||
### Proposed description
|
||||
|
||||
> Proposed description
|
||||
31
.github/PULL_REQUEST_TEMPLATE/app.md
vendored
Normal file
31
.github/PULL_REQUEST_TEMPLATE/app.md
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
## Description
|
||||
|
||||
<!-- Please include a summary of your changes and any relevant context. -->
|
||||
|
||||
### Related issues
|
||||
|
||||
<!-- Link to related issues using keywords (e.g. Fixes <issue_number>) -->
|
||||
<!-- Learn more: https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests -->
|
||||
|
||||
## How has this been tested?
|
||||
|
||||
<!-- Describe the tests you ran to verify your changes. -->
|
||||
|
||||
- [ ] Unit tests added/updated
|
||||
- [ ] Manual testing performed
|
||||
- [ ] Tested on the following environment(s):
|
||||
- OS:
|
||||
- Desktop Environment:
|
||||
- Window System/Manager:
|
||||
- ADB version:
|
||||
- Rust version:
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] I have read the [CONTRIBUTING guidelines](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/blob/main/CONTRIBUTING.md)
|
||||
- [ ] My code follows the project's code style
|
||||
- [ ] I have performed a self-review of my own code
|
||||
- [ ] I have commented my code, particularly in hard-to-understand areas
|
||||
- [ ] My changes generate no new warnings
|
||||
- [ ] I have tested the application with these changes
|
||||
- [ ] The CI/CD pipeline passes
|
||||
18
.github/PULL_REQUEST_TEMPLATE/documentation.md
vendored
Normal file
18
.github/PULL_REQUEST_TEMPLATE/documentation.md
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
## Description
|
||||
|
||||
<!-- Please describe the documentation changes you're adding or updating. -->
|
||||
<!-- Link to related issues using keywords (e.g. Fixes <issue_number>) -->
|
||||
<!-- Learn more: https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests -->
|
||||
|
||||
## Documentation Changes
|
||||
|
||||
<!-- Briefly describe what documentation was added or modified, and why -->
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] I have read the [CONTRIBUTING guidelines](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/blob/main/CONTRIBUTING.md)
|
||||
- [ ] My changes are clear and easy to understand
|
||||
- [ ] I have checked for grammar and spelling errors
|
||||
- [ ] I have verified all links are correct
|
||||
- [ ] I have tested any code examples provided
|
||||
- [ ] The documentation is properly formatted with markdown
|
||||
21
.github/PULL_REQUEST_TEMPLATE/packages.md
vendored
Normal file
21
.github/PULL_REQUEST_TEMPLATE/packages.md
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
## Description
|
||||
|
||||
<!-- Please describe the package changes you're adding or updating. -->
|
||||
<!-- Link to related issues using keywords (e.g. Fixes <issue_number>) -->
|
||||
<!-- Learn more: https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests -->
|
||||
|
||||
## Testing
|
||||
|
||||
- [ ] I have tested these packages on a real device or emulator
|
||||
- [ ] Package names follow correct naming conventions (e.g., `com.example.app`)
|
||||
- [ ] Package names have been verified against device/ADB output
|
||||
- [ ] Descriptions are accurate and helpful
|
||||
- [ ] No duplicate packages added
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
- [ ] I have read the [CONTRIBUTING guidelines](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/blob/main/CONTRIBUTING.md)
|
||||
- [ ] I have followed the package addition guidelines
|
||||
- [ ] I have reviewed the [Removal Definitions](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/wiki/Debloat-Lists#removal-definitions) in the wiki
|
||||
- [ ] All package information is accurate and tested
|
||||
- [ ] I understand my PR may be modified before merging
|
||||
21
.github/pull_request_template.md
vendored
Normal file
21
.github/pull_request_template.md
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
## Description
|
||||
|
||||
<!-- Please include a clear summary of the changes in this pull request. -->
|
||||
|
||||
## Related issues
|
||||
|
||||
<!-- Link to related issues using keywords (e.g. Fixes <issue_number>) -->
|
||||
<!-- Learn more: https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests -->
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] I have read the [CONTRIBUTING guidelines](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/blob/main/CONTRIBUTING.md)
|
||||
- [ ] I have performed a self-review of my changes
|
||||
- [ ] My changes generate no new warnings
|
||||
- [ ] The CI/CD pipeline passes (or is expected to pass)
|
||||
|
||||
<!--
|
||||
Optional: specialized templates are kept in .github/PULL_REQUEST_TEMPLATE/
|
||||
For app/package/documentation-focused PRs, you can use it by including the template in the URL when creating the PR, e.g.
|
||||
https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/pull/new/<branch_name>?template=app.md, https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/pull/new/<branch_name>?template=package.md or https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/pull/new/<branch_name>?template=documentation.md.
|
||||
-->
|
||||
26
.github/release.yml
vendored
26
.github/release.yml
vendored
|
|
@ -1,17 +1,23 @@
|
|||
changelog:
|
||||
exclude:
|
||||
labels:
|
||||
- package::documentation
|
||||
- package::addition
|
||||
- package::breakage
|
||||
- package::bootloop
|
||||
categories:
|
||||
- title: Added
|
||||
labels:
|
||||
- feature
|
||||
- title: Changed
|
||||
- title: Features
|
||||
labels:
|
||||
- enhancement
|
||||
- title: Fixed
|
||||
labels:
|
||||
- bug
|
||||
- title: Refactors
|
||||
labels:
|
||||
- refactor
|
||||
- title: Development
|
||||
labels:
|
||||
- ci
|
||||
- dependencies
|
||||
- title: Documentation
|
||||
labels:
|
||||
- documentation
|
||||
- title: Packages/apps
|
||||
labels:
|
||||
- package::documentation
|
||||
- package::addition
|
||||
- package::breakage
|
||||
|
|
|
|||
113
.github/workflows/build_artifacts.yml
vendored
113
.github/workflows/build_artifacts.yml
vendored
|
|
@ -5,36 +5,32 @@ on:
|
|||
|
||||
jobs:
|
||||
build:
|
||||
name: Building ${{ matrix.build_target }} [${{ matrix.graphics }}] [${{ matrix.update_feature }}]
|
||||
name: Building ${{ matrix.build_target }} [${{ matrix.update_feature }}]
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
build_target: [linux, macos, windows]
|
||||
graphics: [glow, wgpu]
|
||||
build_target: [linux, macos, macos-intel, windows]
|
||||
update_feature: [self-update, no-self-update]
|
||||
exclude:
|
||||
- build_target: windows
|
||||
update_feature: no-self-update
|
||||
include:
|
||||
- build_target: linux
|
||||
os: ubuntu-latest
|
||||
os: ubuntu-22.04
|
||||
- build_target: macos
|
||||
os: macos-latest
|
||||
os: macos-15
|
||||
- build_target: macos-intel
|
||||
os: macos-15-intel
|
||||
- build_target: windows
|
||||
os: windows-latest
|
||||
- graphics: glow
|
||||
renderer: "-opengl"
|
||||
- graphics: wgpu
|
||||
renderer: "" # Vulkan but we don't want this in the binary filename
|
||||
os: windows-2022
|
||||
- update_feature: self-update
|
||||
update_name: "" # we don't want this in the binary filename
|
||||
- update_feature: no-self-update
|
||||
update_name: "-noseflupdate"
|
||||
update_name: "-noselfupdate"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: rui314/setup-mold@v1 # faster linker
|
||||
- uses: actions/cache@v3
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
|
|
@ -44,22 +40,87 @@ jobs:
|
|||
target
|
||||
key: ${{ runner.os }}-release-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: ${{ runner.OS }}-release-
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
- name: Building
|
||||
run: cargo build --release --no-default-features --features ${{ matrix.graphics }},${{ matrix.update_feature }}
|
||||
- name: Creating ./bin directory
|
||||
run: mkdir -p bin
|
||||
run: cargo build --release -p uad-gui --no-default-features --features wgpu,${{ matrix.update_feature }},img
|
||||
- name: Renaming binaries [Windows]
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: mv target/release/uad_gui.exe bin/uad_gui-${{ matrix.build_target }}${{ matrix.renderer }}.exe
|
||||
if: matrix.os == 'windows-2022'
|
||||
run: mv target/release/uad-ng.exe uad-ng-${{ matrix.build_target }}.exe
|
||||
- name: Renaming binaries [Others]
|
||||
if: matrix.os != 'windows-latest'
|
||||
run: mv target/release/uad_gui bin/uad_gui${{ matrix.update_name }}-${{ matrix.build_target }}${{ matrix.renderer }}
|
||||
if: matrix.os != 'windows-2022'
|
||||
run: mv target/release/uad-ng uad-ng${{ matrix.update_name }}-${{ matrix.build_target }}
|
||||
- name: Tarball Linux/MacOS binary
|
||||
if: matrix.os != 'windows-latest'
|
||||
run: tar -czf bin/uad_gui${{ matrix.update_name }}-${{ matrix.build_target }}${{ matrix.renderer }}{.tar.gz,}
|
||||
if: matrix.os != 'windows-2022'
|
||||
run: tar -czf uad-ng${{ matrix.update_name }}-${{ matrix.build_target }}{.tar.gz,}
|
||||
- name: Install coreutils for macOS
|
||||
if: startsWith(matrix.os, 'macos')
|
||||
run: brew install coreutils
|
||||
- name: Create checksums for binaries and archives [Windows]
|
||||
if: matrix.os == 'windows-2022'
|
||||
run: sha256sum uad-ng-${{ matrix.build_target }}.exe | tee uad-ng-${{ matrix.build_target }}.exe-checksum.txt
|
||||
- name: Create checksums for binaries and archives [Others]
|
||||
if: matrix.os != 'windows-2022'
|
||||
run: |
|
||||
sha256sum uad-ng${{ matrix.update_name }}-${{ matrix.build_target }} | tee uad-ng${{ matrix.update_name }}-${{ matrix.build_target }}-checksum
|
||||
sha256sum uad-ng${{ matrix.update_name }}-${{ matrix.build_target }}.tar.gz | tee uad-ng${{ matrix.update_name }}-${{ matrix.build_target }}.tar.gz-checksum
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: uad_gui${{ matrix.update_name }}-${{ matrix.build_target }}${{ matrix.renderer }}
|
||||
path: bin/uad_gui-*
|
||||
name: uad-ng${{ matrix.update_name }}-${{ matrix.build_target }}
|
||||
path: uad-ng-*
|
||||
|
||||
build-cli:
|
||||
name: Building CLI ${{ matrix.build_target }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- build_target: linux
|
||||
os: ubuntu-22.04
|
||||
- build_target: macos
|
||||
os: macos-15
|
||||
- build_target: macos-intel
|
||||
os: macos-15-intel
|
||||
- build_target: windows
|
||||
os: windows-2022
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target
|
||||
key: ${{ runner.os }}-release-cli-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: ${{ runner.OS }}-release-cli-
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
- name: Building
|
||||
run: cargo build --release -p uad-cli
|
||||
- name: Renaming binaries [Windows]
|
||||
if: matrix.os == 'windows-2022'
|
||||
run: mv target/release/uad.exe uad-cli-${{ matrix.build_target }}.exe
|
||||
- name: Renaming binaries [Others]
|
||||
if: matrix.os != 'windows-2022'
|
||||
run: mv target/release/uad uad-cli-${{ matrix.build_target }}
|
||||
- name: Tarball Linux/MacOS binary
|
||||
if: matrix.os != 'windows-2022'
|
||||
run: tar -czf uad-cli-${{ matrix.build_target }}{.tar.gz,}
|
||||
- name: Install coreutils for macOS
|
||||
if: startsWith(matrix.os, 'macos')
|
||||
run: brew install coreutils
|
||||
- name: Create checksums for binaries and archives [Windows]
|
||||
if: matrix.os == 'windows-2022'
|
||||
run: sha256sum uad-cli-${{ matrix.build_target }}.exe | tee uad-cli-${{ matrix.build_target }}.exe-checksum.txt
|
||||
- name: Create checksums for binaries and archives [Others]
|
||||
if: matrix.os != 'windows-2022'
|
||||
run: |
|
||||
sha256sum uad-cli-${{ matrix.build_target }} | tee uad-cli-${{ matrix.build_target }}-checksum
|
||||
sha256sum uad-cli-${{ matrix.build_target }}.tar.gz | tee uad-cli-${{ matrix.build_target }}.tar.gz-checksum
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: uad-cli-${{ matrix.build_target }}
|
||||
path: uad-cli-*
|
||||
|
|
|
|||
60
.github/workflows/ci.yml
vendored
60
.github/workflows/ci.yml
vendored
|
|
@ -2,46 +2,49 @@ name: Continuous Integration
|
|||
on:
|
||||
push:
|
||||
paths:
|
||||
- "**.rs"
|
||||
- "Cargo.lock"
|
||||
- "Cargo.toml"
|
||||
- "**.json"
|
||||
- Cargo.lock
|
||||
- Cargo.toml
|
||||
- resources/assets/*.ttf
|
||||
- resources/assets/uad_lists.json
|
||||
- src/**
|
||||
pull_request:
|
||||
paths:
|
||||
- "**.rs"
|
||||
- "Cargo.lock"
|
||||
- "Cargo.toml"
|
||||
- "**.json"
|
||||
- Cargo.lock
|
||||
- Cargo.toml
|
||||
- resources/assets/*.ttf
|
||||
- resources/assets/uad_lists.json
|
||||
- src/**
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: lint
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||
os: [ubuntu-22.04, windows-2022, macos-15]
|
||||
lint: [check, test, clippy, fmt]
|
||||
exclude: # https://github.com/community/community/discussions/7835
|
||||
- os: windows-latest
|
||||
- os: windows-2022
|
||||
lint: clippy
|
||||
- os: windows-latest
|
||||
- os: windows-2022
|
||||
lint: fmt
|
||||
- os: macOS-latest
|
||||
- os: macos-15
|
||||
lint: clippy
|
||||
- os: macOS-latest
|
||||
- os: macos-15
|
||||
lint: fmt
|
||||
include:
|
||||
- lint: check
|
||||
args: " --all-features"
|
||||
env-flag: "-D warnings"
|
||||
- lint: test
|
||||
args: ""
|
||||
- lint: clippy
|
||||
args: " --all --all-features -- -D warnings"
|
||||
args: " --all-features -- -D clippy::all -W clippy::style"
|
||||
- lint: fmt
|
||||
args: " --all -- --check"
|
||||
args: " -- --check"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: rui314/setup-mold@v1 # faster linker
|
||||
- uses: actions/cache@v3
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
|
|
@ -54,4 +57,25 @@ jobs:
|
|||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy,rustfmt
|
||||
- run: cargo ${{ matrix.lint }}${{ matrix.args }}
|
||||
- shell: bash
|
||||
run: RUSTFLAGS='${{ matrix.env-flag }}' cargo ${{ matrix.lint }}${{ matrix.args }}
|
||||
|
||||
coverage:
|
||||
name: coverage
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- name: Download grcov
|
||||
run: |
|
||||
mkdir -p "${HOME}/.local/bin"
|
||||
curl -sL https://github.com/mozilla/grcov/releases/download/v0.8.19/grcov-x86_64-unknown-linux-gnu.tar.bz2 | tar jxf - -C "${HOME}/.local/bin"
|
||||
echo "$HOME/.local/bin" >> $GITHUB_PATH
|
||||
- name: Install LLVM-profdata
|
||||
run: rustup component add llvm-tools-preview
|
||||
- name: Run tests
|
||||
run: CARGO_INCREMENTAL=0 RUSTFLAGS='-Cinstrument-coverage' LLVM_PROFILE_FILE='cargo-test-%p-%m.profraw' cargo test
|
||||
- name: make coverage directory
|
||||
run: mkdir ./target/coverage
|
||||
- name: Run GRCOV
|
||||
run: grcov . --binary-path ./target/debug/ -s . -t lcov --branch --ignore-not-existing --ignore '../*' --ignore "/*" -o ./target/coverage/tests.lcov
|
||||
|
|
|
|||
50
.github/workflows/release.yml
vendored
50
.github/workflows/release.yml
vendored
|
|
@ -4,49 +4,31 @@ on:
|
|||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "**.rs"
|
||||
- "Cargo.lock"
|
||||
- "Cargo.toml"
|
||||
tags-ignore:
|
||||
- dev-build
|
||||
|
||||
env:
|
||||
dev_tag: dev-build
|
||||
tags:
|
||||
- v?[0-9]+.[0-9]+.[0-9]+ # semver
|
||||
- '!dev-build' # not dev-build
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||
uses: ./.github/workflows/build_artifacts.yml
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-22.04
|
||||
needs: build
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Downloads artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
- name: Create release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
path: bin
|
||||
- name: Create pre-release
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
body_path: ${{ github.workspace }}/CHANGELOG.md
|
||||
files: bin/*/uad_gui-*
|
||||
prerelease: true
|
||||
# - name: Update dev-build tag
|
||||
# if: ${{ github.event_name == 'push' }}
|
||||
# run: |
|
||||
# git tag -d ${{ env.dev_tag }} || true
|
||||
# git push origin :refs/tags/${{ env.dev_tag }} || true
|
||||
# git tag ${{ env.dev_tag }}
|
||||
# git push origin ${{ env.dev_tag }}
|
||||
# - name: Create dev-build release
|
||||
# if: ${{ github.event_name == 'push' }}
|
||||
# uses: softprops/action-gh-release@v1
|
||||
# with:
|
||||
# generate_release_notes: true
|
||||
# files: bin/*/uad_gui-*
|
||||
# prerelease: true
|
||||
# tag_name: ${{ env.dev_tag }}
|
||||
files: |
|
||||
uad-ng-*/*
|
||||
uad-cli-*/*
|
||||
generate_release_notes: true
|
||||
body: |
|
||||
> [!NOTE]
|
||||
> 📦 **Package list updates don't require a new app version**—UAD-ng automatically fetches the latest package definitions on launch (requires internet connection).
|
||||
|
|
|
|||
11
.mailmap
Normal file
11
.mailmap
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
Rudxain <rudxain@localhost.localdomain>
|
||||
Rudxain <76864299+Rudxain@users.noreply.github.com> Ricardo Fernández Serrata <76864299+Rudxain@users.noreply.github.com>
|
||||
Anonymoussaurus <50231698+AnonymousWP@users.noreply.github.com> AnonymousWP <50231698+AnonymousWP@users.noreply.github.com>
|
||||
Anonymoussaurus <anonymouswp@oxygenupdater.com>
|
||||
# w1nst0n
|
||||
# 0x192
|
||||
# “Frigyes06”
|
||||
# Frigyes
|
||||
# Frigyes06
|
||||
# Frigyes Erdosi-Szucs
|
||||
# Frigyes Erdosi Szucs
|
||||
223
CHANGELOG.md
223
CHANGELOG.md
|
|
@ -1,223 +0,0 @@
|
|||
# Changelog
|
||||
|
||||
**THIS IS AN UNOFFICIAL FORK**
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
The sections should follow the order `Apps`, `Added`, `Changed`, `Fixed`, `Packaging`
|
||||
and `Removed`.
|
||||
|
||||
## [0.6.2] - Released
|
||||
|
||||
### Apps
|
||||
|
||||
- [[#65](https://github.com/Universal-Debloater-Alliance/universal-android-debloater/pull/65)] improved package descriptions
|
||||
- [[#61](https://github.com/Universal-Debloater-Alliance/universal-android-debloater/pull/61)] pkg(`com.samsung.android.providers.contacts`): improve description
|
||||
- [[#59](https://github.com/Universal-Debloater-Alliance/universal-android-debloater/pull/59)] pkg(`com.qualcomm.qcrilmsgtunnel`): change removal and description
|
||||
- [[#58](https://github.com/Universal-Debloater-Alliance/universal-android-debloater/pull/58)] pkg(`com.google.android.googlequicksearchbox`): improve description
|
||||
- [[#39](https://github.com/Universal-Debloater-Alliance/universal-android-debloater/pull/39)] Added new Wi-Fi related packages, custom Bluetooth overlay package, Google system packages, and systemui package
|
||||
- [[#40](https://github.com/Universal-Debloater-Alliance/universal-android-debloater/pull/40)] Added new package for MediaTek devices and the POCO Launcher resources, IMS Service Entitlement feature, UWB communication feature, Mediatek Duraspeed, and Mediatek Soc Package.
|
||||
- [[#44](https://github.com/Universal-Debloater-Alliance/universal-android-debloater/pull/44)] Added a lot of new packages
|
||||
- [[#46](https://github.com/Universal-Debloater-Alliance/universal-android-debloater/pull/46)] Added more packages and updated descriptions
|
||||
|
||||
### Fixed
|
||||
|
||||
- [[#72](https://github.com/Universal-Debloater-Alliance/universal-android-debloater/pull/72)] Reduced startup window size
|
||||
|
||||
### Changed
|
||||
|
||||
- [[#71](https://github.com/Universal-Debloater-Alliance/universal-android-debloater/pull/71)] Reversed #18 to only list system packages
|
||||
|
||||
## [0.6.1] - Released
|
||||
|
||||
### Apps
|
||||
- [[#21](https://github.com/Universal-Debloater-Alliance/universal-android-debloater/pull/21)] Added android.autoinstalls.config.Xiaomi.model package
|
||||
- [[#23](https://github.com/Universal-Debloater-Alliance/universal-android-debloater/pull/23)] Modified the description for ASOP camera app.
|
||||
- [[#25](https://github.com/Universal-Debloater-Alliance/universal-android-debloater/pull/25)] Added and edited packages
|
||||
- [[#27](https://github.com/Universal-Debloater-Alliance/universal-android-debloater/pull/27)] Added com.android.cellbroadcastreceiver.module
|
||||
|
||||
### Fixed
|
||||
- [[#35](https://github.com/Universal-Debloater-Alliance/universal-android-debloater/pull/35)] OpenGL Drivers
|
||||
|
||||
### Changed
|
||||
- [[#34](https://github.com/Universal-Debloater-Alliance/universal-android-debloater/pull/34)] Updated links to new repo
|
||||
- [[#26](https://github.com/Universal-Debloater-Alliance/universal-android-debloater/pull/26)] Updated GUI libraries.
|
||||
|
||||
## [0.6.0] - Released
|
||||
|
||||
### Apps
|
||||
- [[#13](https://github.com/Universal-Debloater-Alliance/universal-android-debloater/pull/13)] Addedcom.telus.myaccount package.
|
||||
- [[#15](https://github.com/Universal-Debloater-Alliance/universal-android-debloater/pull/15)] Added More Wiko oem packages.
|
||||
|
||||
### Fixed
|
||||
- [[#14](https://github.com/Universal-Debloater-Alliance/universal-android-debloater/pull/14)] Fixed typos in uad_list.json
|
||||
- [[#18](https://github.com/Universal-Debloater-Alliance/universal-android-debloater/pull/18)] Fixed ADB command to include all packages, not just system packages. Also fixed uad_list update url.
|
||||
- TikTok app description.
|
||||
|
||||
## [0.5.2] - Released
|
||||
|
||||
**WARNING: Settings specification has changed. Previous user settings will be erased**.
|
||||
|
||||
### Added
|
||||
- [[#11](https://github.com/Universal-Debloater-Alliance/universal-android-debloater/pull/11)] All of AnonymousWP's patches. This release notes will probably be restarted as issue counter is reset on fork.
|
||||
|
||||
- [[#374](https://github.com/0x192/universal-android-debloater/pull/374)] **Device-specific persistent configuration:** Some settings are now device-specific which means you can maintain different settings across several devices.
|
||||
|
||||
- [[#447](https://github.com/0x192/universal-android-debloater/pull/447)] **Backup/Restore the state of a device:** Quick and easy way to save the state of all the system apps on a device and restore it.
|
||||
|
||||
- [[#450](https://github.com/0x192/universal-android-debloater/pull/450)] **Warn the user when a work profile is detected:** Displays a warning message when switching to a work profile user and displays unavailable work profile users in the settings.
|
||||
|
||||
### Changed
|
||||
- [[#374](https://github.com/0x192/universal-android-debloater/pull/374)] ALL settings are now persistent.
|
||||
|
||||
### Fixed
|
||||
- [[#448](https://github.com/0x192/universal-android-debloater/pull/448)] UAD crash when interacting with work profiles on recent phones.
|
||||
|
||||
### Removed
|
||||
- The `Export current selection` button and the unintuitive auto import selection (see https://github.com/0x192/universal-android-debloater/issues/192) have been replaced by the new backup/restore system.
|
||||
|
||||
## [0.5.1] - 2022-07-03
|
||||
|
||||
Since `0.5.0`, all changes related to apps are available to users without downloading a new version of UAD as the software directly download the json debloat list from Github. These changes can be tracked in commits with `[Pkg]` in their name. [See the commits](https://github.com/0x192/universal-android-debloater/commits/main)
|
||||
|
||||
### Added
|
||||
- [[#209](https://github.com/0x192/universal-android-debloater/issues/209)] Persistent highlighting when you click on a package
|
||||
|
||||
### Changed
|
||||
- `neededBy` and `dependencies` field can now list multiple packages (feature not visible in the UI yet)
|
||||
|
||||
### Fixed
|
||||
- [[#286](https://github.com/0x192/universal-android-debloater/issues/286)] UAD stuck on "Downloading UAD lists. Please wait" screen
|
||||
|
||||
## Packaging
|
||||
- [[#256](https://github.com/0x192/universal-android-debloater/issues/256)] Fixed typo in the release name of the noseflupdate variation
|
||||
- Bump dependencies
|
||||
|
||||
## [0.5.0] - 2022-04-03
|
||||
|
||||
### Apps
|
||||
|
||||
- [[#115](https://github.com/0x192/universal-android-debloater/issues/115)] Added `com.tblenovo.lenovotips` to the recommended list.
|
||||
- [[#120](https://github.com/0x192/universal-android-debloater/pull/120)] Move Google keyboard to `Advanced` list (Default keyboards should not be in the `Recommended` list)
|
||||
- [[#169](https://github.com/0x192/universal-android-debloater/issues/154) Move `com.android.htmlviewer` to the `Expert` list. Removing it bootloop the device on MIUI 12.5.4+.
|
||||
|
||||
Huge thanks to [@KarlRamstedt](https://github.com/KarlRamstedt) for their help in this major debloat list update:
|
||||
- [[#122](https://github.com/0x192/universal-android-debloater/pull/122)] Added a bunch of new packages
|
||||
- [[#122](https://github.com/0x192/universal-android-debloater/pull/122)] A lot of description updates and fixes
|
||||
- [[#122](https://github.com/0x192/universal-android-debloater/pull/122) | [#138](https://github.com/0x192/universal-android-debloater/pull/138)] Big revision of the recommendations according to more consistent criteria ([see the wiki](https://github.com/0x192/universal-android-debloater/wiki/FAQ#how-are-the-recommendations-chosen))
|
||||
|
||||
### Added
|
||||
- [[#68](https://github.com/0x192/universal-android-debloater/issue/68)] **Unselect all button**: Let's you unselect all the packages you see on screen (i.e in the current filtered list).
|
||||
- [[#119](https://github.com/0x192/universal-android-debloater/issue/119)] **Reboot button**: Let's you quickly reboot the currently selected device.
|
||||
- [[#110](https://github.com/0x192/universal-android-debloater/pull/110)] **Remote `uad_lists.json` download**: The debloat list is now directly fetched from the main branch of this repo when you launch UAD. This means there is no longer the need to release a new version of UAD for updating the debloat lists! :rocket:
|
||||
- [[#121](https://github.com/0x192/universal-android-debloater/pull/121)] :arrows_counterclockwise: **UAD self-update**: UAD will now check at launch if there is a new version of itself and enable you to perform the update directly from the app! :rocket:
|
||||
|
||||
### Changed
|
||||
- [[#165](https://github.com/0x192/universal-android-debloater/issues/165)] UAD now tries every 500ms (for 1min) to initiate an ADB connection until a device is found during the `FindingPhones` loading state.
|
||||
- All the init process was reworked and a status message is displayed at each stage (`DownloadingList`, `FindingPhones`,`LoadingPackages`,`UpdatingUad` `Ready`) so you know what is happening.
|
||||
- Minor UI changes
|
||||
|
||||
### Packaging
|
||||
- Add a `no-self-update` build for MacOS and Linux. Useful if UAD is distributed into repositories. The update process will then be managed by a package manager.
|
||||
- MacOS builds are now also be released as a compressed tarball (like for Linux). You won't need to manually add the executable permission anymore. ([more info](https://github.com/actions/upload-artifact/issues/38))
|
||||
- Bump dependencies
|
||||
|
||||
|
||||
## [0.4.1] - 2022-01-31
|
||||
|
||||
### Fixed
|
||||
- Selection counter never decreasing.
|
||||
|
||||
## [0.4] - 2022-01-30
|
||||
|
||||
### Apps
|
||||
- [[#92](https://github.com/0x192/universal-android-debloater/pull/92)] Added 3 Fairphone packages + 7 Qualcomm packages (thanks [@VeH-c](https://github.com/VeH-c))
|
||||
- [[#87](https://github.com/0x192/universal-android-debloater/pull/87)] Added 2 Unihertz packages (thanks [@rar0ch](https://github.com/rar0ch))
|
||||
- [[#52](https://github.com/0x192/universal-android-debloater/issues/52)] Added `uk.co.ee.myee` to the debloat lists (thanks [@lawson58](https://github.com/lawson85)).
|
||||
- [[#58](https://github.com/0x192/universal-android-debloater/issues/52)] Added `android` to the debloat lists with the tag `Unsafe`.
|
||||
- Added 2 new Xiaomi packages to the `Recommended` list.
|
||||
- Multiple package description improvement (thanks [@jonas-ott](https://github.com/jonas-ott) and [@felurx](https://github.com/felurx) for the help)
|
||||
- Review of the package lists recommendations. The `Recommended` debloat list is now safer (less likely to remove something you'd want to keep).
|
||||
|
||||
### Added
|
||||
- [[#49](https://github.com/0x192/universal-android-debloater/issues/49)] Multi-device support: You are now able to select a device among the list of all ADB connected devices/emulators.
|
||||
- [[#44](https://github.com/0x192/universal-android-debloater/issues/44)] Persistent settings: Settings (only `theme` for now) are saved to a config file. Its location follows [the standards of the different OS](https://github.com/dirs-dev/dirs-rs#example).
|
||||
- Links to the Github page, wiki, github issues and logfiles in the `About` page.
|
||||
|
||||
### Changed
|
||||
- [[#65](https://github.com/0x192/universal-android-debloater/issues/65)] ADB commands now run in parallel and asynchronously! This means no more UI freeze when performing long/many actions! :rocket:
|
||||
- UI now updates itself in real time when performing ADB actions (thanks to async & multithreading). Before, it waited for the end of all actions.
|
||||
- Logfiles are now located in a more conventional place: [cache_dir](https://docs.rs/dirs/latest/dirs/).
|
||||
- Previous logs are no longer overwritten. The logger now only appends to the current logfile of the day (UAD_%Y%m%d.log).
|
||||
- Each new day the logger will create a new file on UAD launch.
|
||||
- [[#78](https://github.com/0x192/universal-android-debloater/issues/78)] Disable mode is now only available on Android 6+ because the disable ADB commands do not work without root on older devices. The setting will be greyed-out for those devices.
|
||||
- Minor light theme update
|
||||
|
||||
|
||||
### Fixed
|
||||
- [[#50](https://github.com/0x192/universal-android-debloater/issues/50)] Resync button flipping theme back to `Lupin`.
|
||||
- [Regression ([048e7f](https://github.com/0x192/universal-android-debloater/commit/048e7fc8fd6d44b0e8ba933c289249366254a9cc))] Weird disabled/greyed action button with older devices (< Android 8.0). Package could be selected but no action was performed.
|
||||
- [[#78](https://github.com/0x192/universal-android-debloater/issues/78)] Packages not being actually uninstalled on older devices (< Android 6.0). Without root we can only use `pm block`/`pm unblock` for Android KitKit (4.4) and `pm hide`/`pm unhide` on Android Lollipop (5.x).
|
||||
|
||||
### Packaging
|
||||
- For Arch-based users, UAD is now available in the AUR: `universal-android-debloater-bin` (binary) and `universal-android-debloater` (from source)
|
||||
- Bump dependencies
|
||||
|
||||
|
||||
## [0.3] - 2021-10-10
|
||||
|
||||
### Added
|
||||
- [[#16](https://github.com/0x192/universal-android-debloater/issues/16)] Multi-user support: You can now debloat/restore apps for any user of the phone (not only the primary user 0).
|
||||
- `Multi user mode` setting (default to `on` for Android 5+) allowing to remove packages for all users ([a work profile is another user](https://developer.android.com/work/managed-profiles)) instead of only the selected user.
|
||||
- User switcher (picklist).
|
||||
- [[#11](https://github.com/0x192/universal-android-debloater/issues/11)] New themes: light, dark and lupin. Lupin theme is now the new default theme. Themes can be changed from the settings.
|
||||
- [[#40](https://github.com/0x192/universal-android-debloater/issues/40)] Description field scrollbar: you can now scroll long descriptions.
|
||||
|
||||
### Fixed
|
||||
- [Regression] Unsafe packages can be deleted without enabling `expert mode`.
|
||||
- The refresh button doesn't update settings when a (new) phone is connected.
|
||||
- [Regression] Restore buttons are disabled when connecting an Android 8.0 phone.
|
||||
- [[#17](https://github.com/0x192/universal-android-debloater/issues/17)] Refresh icon does not appear.
|
||||
|
||||
## [0.2.2] - 2021-09-30
|
||||
|
||||
### Fixed
|
||||
- Crash when connecting a LG device (#33)
|
||||
|
||||
## [0.2.1] - 2021-09-28
|
||||
|
||||
### Added
|
||||
- Software version in the navigation panel
|
||||
|
||||
### Packaging
|
||||
- `wgpu` renderer is not the default renderer (you don't need to add `--features wgpu` if you want to build UAD with `wgpu`)
|
||||
|
||||
### Fixed
|
||||
- [[#35](https://github.com/0x192/universal-android-debloater/issues/35)] Exported selection not found
|
||||
|
||||
## [0.2] - 2021-09-26
|
||||
|
||||
### Added
|
||||
- [[#2](https://github.com/0x192/universal-android-debloater/issues/2)] UAD now comes with a logger. Debug information will be written to a `uad.log` file (Warning level log in *stdout*)
|
||||
- [[#15](https://github.com/0x192/universal-android-debloater/issues/15)] Support for older phone (< Android 8.0):
|
||||
- Disable mode in settings: clear and disable packages instead of uninstalling them (default for old phones because you can't restore uninstalled packages)
|
||||
- [[#8](https://github.com/0x192/universal-android-debloater/issues/8)] Export your selection in the `uad_exported_selection.txt` file. Packages from this file (if found in the current directory) will be automatically selected upon the start of UAD (or after a refresh).
|
||||
|
||||
### Changed
|
||||
- [[#25](https://github.com/0x192/universal-android-debloater/issues/25)] UAD will no longer crash at start if it doesn't find ADB but will display a useful error message
|
||||
- [[#3](https://github.com/0x192/universal-android-debloater/issues/3)] Better handling of ADB errors
|
||||
- Updated dependencies (compatibility with [Iced](https://github.com/iced-rs/iced) main branch latest commit)
|
||||
- Cleanup and refactoring of the code
|
||||
- Performance improvement
|
||||
- Various UI/UX improvement
|
||||
- The `Debloat/Restore selection` button has been split in 2 buttons: `removing` and `restoring`
|
||||
|
||||
### Packaging
|
||||
- Added an alternative build that uses [OpenGL](https://fr.wikipedia.org/wiki/OpenGL) (instead of [Vulkan](https://fr.wikipedia.org/wiki/Vulkan_(API))) for compatibility with older computers. If you encouter some visual glitches with the default Vulkan build you should try the OpenGL build.
|
||||
|
||||
### Fixed
|
||||
- Spelling mistake
|
||||
- Failed build with MSVC toolchain
|
||||
92
CONTRIBUTING.md
Normal file
92
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
# Contributors Guide
|
||||
|
||||
## For who is this guide?
|
||||
|
||||
This guide is meant for users who want to contribute to the codebase of Universal Android Debloater Next Generation, whether that is the application code or [the "database"-file for adding packages](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/wiki/How-to-contribute). To keep all processes streamlined and consistent, we're asking you to stick to this guide whenever contributing.
|
||||
|
||||
Even though the guide is made for contributors, it's also strongly recommended that the UAD-ng team sticks to these guidelines. After all, we're a prime example.
|
||||
|
||||
## What are the guidelines?
|
||||
|
||||
### Mass edits
|
||||
|
||||
If you've performed massive edits via automation, you must specify what automation you used. For example, if you ran the command:
|
||||
|
||||
```sh
|
||||
sed -i 's/a/b/' resources/assets/uad_lists.json
|
||||
```
|
||||
|
||||
You should copy-paste that cmd to the pull-request description. This rule applies even if you use "proper" cmds such as `jq`, which safely edit structured data. And it also applies to AI, *especially* LLMs.
|
||||
|
||||
This is required so that we can review your big patch without reading it. This improves [transparency](https://en.wikipedia.org/wiki/Transparency_(behavior)), which makes you more trust-worthy! Bonus points if you can provide a sequence of cmds that proves your patch contains exactly what you said it does. For the simple `sed` case, something like:
|
||||
|
||||
```sh
|
||||
# alt: `gh pr checkout 0000 && git checkout main`
|
||||
git remote add fork https://github.com/user/uadng
|
||||
git fetch fork
|
||||
# example commit hash from upstream (not fork);
|
||||
# more reproducible than branch-name
|
||||
git checkout beefcafe
|
||||
sed -i 's/a/b/' resources/assets/uad_lists.json
|
||||
# compare unstaged change with commit from fork
|
||||
git diff cafebeef
|
||||
```
|
||||
|
||||
Should be enough.
|
||||
|
||||
You should be careful with mass-editing anyways. Even if the verification is successful, the effects [might not be what you expect](https://en.wikipedia.org/wiki/Scunthorpe_problem).
|
||||
|
||||
Since AI is non-deterministic, the version number is optional, but it's helpful to know how "intelligent" it is. AI edits are non-reproducible, so "verification scripts" are useless.
|
||||
|
||||
### Branching strategy
|
||||
|
||||
As for our branching strategy, we're using [Trunk-Based Development](https://trunkbaseddevelopment.com/#one-line-summary).
|
||||
|
||||
In short, there's one trunk branch named `main` (`master`). Apart from `main`/`master` there can be different short-lived branches, such as:
|
||||
|
||||
- Features (`feature/*`)
|
||||
- Fixes (`hotfix/*` or simply `fix/*`)
|
||||
- Dependency updates (`deps/*`)
|
||||
- Etc.
|
||||
|
||||
Do mind that these branch names do only not apply to the addition of packages; for that, we use the following scheme: `[issue number][issue title]`. This can be done [automatically](https://docs.github.com/en/issues/tracking-your-work-with-issues/creating-a-branch-for-an-issue) too.
|
||||
|
||||
This is how it looks like and works:
|
||||

|
||||
|
||||
### Syncing branches
|
||||
|
||||
If you want to [back-port](https://en.wikipedia.org/wiki/Backporting) (GitHub's "Update" and "Sync" buttons) from upstream `main` to any other branch (for testing purposes, or any other reasonable case), it's recommended that you `rebase` rather than `merge` the changes.
|
||||
|
||||
**Don't apply changes to/from arbitrary branches**, that's a recipe for chaos! If you encounter a situation where it's absolutely necessary to sync non-`main` branches, whether to `merge` or `rebase` should be decided by the UAD-ng team (with some contributor feedback) on a case-by-case basis.
|
||||
|
||||
### Commit messages
|
||||
|
||||
As for commits, we prefer using [Conventional Commit Messages](https://gist.github.com/qoomon/5dfcdf8eec66a051ecd85625518cfd13). When working in any of the branches listed above (if there's an existing issue for it), close it using a [closing keyword](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword). For more information regarding Conventional Commit Messages, see <https://www.conventionalcommits.org/en/v1.0.0/> as well.
|
||||
|
||||
### Issue labels
|
||||
|
||||
When creating issues, they are automatically labeled based on the [issue template](.github/ISSUE_TEMPLATE/) you choose. These labels help categorize work and generate release notes.
|
||||
|
||||
| Label | Purpose | Issue template |
|
||||
| ----- | ------- | -------------- |
|
||||
| `bug` | Software bugs in UAD-ng | Bug Report |
|
||||
| `enhancement` | New feature requests | Feature Request |
|
||||
| `package::addition` | Adding new packages to the debloat list | Add new package(s) |
|
||||
| `package::breakage` | Issues caused by debloating | Debloat issue report |
|
||||
| `package::documentation` | Updating package descriptions or recommendations | Update apps description or recommendation |
|
||||
| `documentation` | Improvements to docs/wiki | UAD-ng documentation |
|
||||
| `ci` | CI/CD related issues | CI/CD |
|
||||
| `refactor` | Code refactoring | — (maintainer-assigned) |
|
||||
| `dependencies` | Dependency updates | — (maintainer-assigned) |
|
||||
|
||||
Labels are also used to automatically generate categorized [release notes](.github/release.yml). Each label maps to a changelog section:
|
||||
|
||||
| Label(s) | Changelog category |
|
||||
| -------- | ------------------ |
|
||||
| `enhancement` | Features |
|
||||
| `bug` | Fixed |
|
||||
| `refactor` | Refactors |
|
||||
| `package::addition`, `package::breakage`, `package::documentation` | Packages/apps |
|
||||
| `ci`, `dependencies` | Development |
|
||||
| `documentation` | Documentation |
|
||||
5722
Cargo.lock
generated
5722
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
82
Cargo.toml
82
Cargo.toml
|
|
@ -1,43 +1,63 @@
|
|||
[package]
|
||||
name = "uad_gui"
|
||||
description = "A cross-platform GUI debloater for android devices"
|
||||
version = "0.6.2"
|
||||
[workspace]
|
||||
members = [
|
||||
"crates/uad-core",
|
||||
"crates/uad-gui",
|
||||
"crates/uad-cli"
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "1.2.0"
|
||||
authors = ["Universal-Debloater-Alliance"]
|
||||
license = "GPL-3.0"
|
||||
homepage = "https://github.com/Universal-Debloater-Alliance/universal-android-debloater"
|
||||
repository = "https://github.com/Frigyes06/universal-android-debloater"
|
||||
homepage = "https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation"
|
||||
repository = "https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation"
|
||||
readme = "README.md"
|
||||
keywords = ["debloater", "android", "adb", "privacy", "bloatware"]
|
||||
categories = ["gui"]
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
default = ["wgpu", "self-update"]
|
||||
wgpu = [] # Iced/wgpu is default
|
||||
glow = ["iced/glow"] # OpenGL support
|
||||
self-update = ["flate2", "tar"]
|
||||
no-self-update = []
|
||||
|
||||
[dependencies]
|
||||
[workspace.dependencies]
|
||||
# Core dependencies
|
||||
serde = { version = "^1.0", features = ["derive"] }
|
||||
serde_json = "^1.0"
|
||||
static_init = "^1.0"
|
||||
fern = { version = "^0", features = ["colored"] }
|
||||
chrono = { version = "^0.4", default-features = false, features = ["std", "clock"] }
|
||||
log = "^0.4"
|
||||
regex = "^1.10.2"
|
||||
chrono = { version = "^0.4", default-features = false, features = ["std", "clock"] }
|
||||
toml = "^0"
|
||||
dirs = "^5.0.0"
|
||||
ureq = { version = "*", features = ["json"] }
|
||||
retry = { version = "^2.0.0" }
|
||||
iced = "0.9.0"
|
||||
iced_native = "^0.10.3"
|
||||
dirs = "^6"
|
||||
ureq = { version = "3", features = ["json"] }
|
||||
retry = "^2.0.0"
|
||||
csv = "^1.3"
|
||||
|
||||
[target.'cfg(not(target_os = "windows"))'.dependencies]
|
||||
flate2 = { version = "^1", optional = true }
|
||||
tar = { version = "^0.4", optional = true }
|
||||
# GUI dependencies
|
||||
iced = { version = "=0.14.0", features = ["advanced"] }
|
||||
image = "0.25"
|
||||
rfd = "^0.17.1"
|
||||
dark-light = "2"
|
||||
fern = { version = "^0", features = ["colored"] }
|
||||
|
||||
[profile.release]
|
||||
opt-level = "s"
|
||||
lto = true
|
||||
strip = "symbols"
|
||||
# CLI dependencies
|
||||
clap = { version = "4.6.1", features = ["derive"] }
|
||||
tokio = { version = "1.52.3", features = ["full"] }
|
||||
|
||||
# Platform-specific dependencies
|
||||
flate2 = "^1"
|
||||
tar = "^0.4"
|
||||
win32console = "^0.1.5"
|
||||
|
||||
[workspace.lints.rust]
|
||||
deprecated_safe = "warn"
|
||||
|
||||
[workspace.lints.clippy]
|
||||
undocumented_unsafe_blocks = "forbid"
|
||||
panic_in_result_fn = "warn"
|
||||
infinite_loop = "warn"
|
||||
format_push_string = "warn"
|
||||
large_include_file = "warn"
|
||||
shadow_unrelated = "warn"
|
||||
struct_field_names = "allow" # annoying
|
||||
|
||||
disallowed_types = "deny"
|
||||
disallowed_methods = "deny"
|
||||
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
6
LICENSE
6
LICENSE
|
|
@ -631,8 +631,8 @@ to attach them to the start of each source file to most effectively
|
|||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
Universal Android Debloater GUI
|
||||
Copyright (C) 2021 W1nst0n
|
||||
Universal Android Debloater Next Generation
|
||||
Copyright (C) 2023 Universal Debloater Alliance
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
|
|
@ -652,7 +652,7 @@ Also add information on how to contact you by electronic and paper mail.
|
|||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
Universal Android Debloater GUI Copyright (C) 2021 W1nst0n
|
||||
Universal Android Debloater Next Generation Copyright (C) 2023 Universal Debloater Alliance
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
|
|
|||
217
README.md
217
README.md
|
|
@ -1,206 +1,53 @@
|
|||
# Universal Android Debloater GUI
|
||||
# Universal Android Debloater Next Generation
|
||||
|
||||
**DISCLAIMER**: Use at your own risk. I am not responsible for anything that
|
||||
could happen to your phone.
|
||||
> [!warning]
|
||||
> **DISCLAIMER**: Use at your own risk. We're not responsible for anything that could happen to your devices.
|
||||
|
||||
<img src="/resources/screenshots/v0.5.0.png" width="850" alt="uad_screenshot">
|
||||
<div align=center>
|
||||
<img width=75% alt="screenshot of apps view" src=./resources/screenshots/v1.0.2.png>
|
||||
</div>
|
||||
|
||||
**This software is still in an early stage of development. Check out the issues, and feel free to contribute!**
|
||||
This is a detached fork of the [UAD project](https://github.com/0x192/universal-android-debloater). This aims to improve privacy and efficiency (energy, speed, memory) by removing unnecessary and obscure system apps.
|
||||
This can also improve security by reducing the [attack surface](https://en.wikipedia.org/wiki/Attack_surface). Read [the wiki](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/wiki) for more details.
|
||||
|
||||
## Summary
|
||||
## Documentation
|
||||
|
||||
This is a complete rewrite in Rust of the [UAD project](https://gitlab.com/W1nst0n/universal-android-debloater),
|
||||
which aims to improve privacy and battery performance by removing unnecessary
|
||||
and obscure system apps.
|
||||
This can also contribute to improving security by reducing [the attack surface](https://en.wikipedia.org/wiki/Attack_surface).
|
||||
Everything about UAD-ng (and related stuff) can be found on the Wiki, such as:
|
||||
|
||||
Packages are as well documented as possible to provide a better understanding
|
||||
of what you can delete or not. The worst issue that could happen is removing
|
||||
an essential system package needed during boot causing then an unfortunate
|
||||
bootloop. After about 5 failed system boots, the phone will automatically reboot
|
||||
in recovery mode, and you'll have to perform a FACTORY RESET. Make a backup first!
|
||||
- [Getting started](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/wiki/Getting-started)
|
||||
- Features of this app
|
||||
- [Usage guide](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/wiki/Usage)
|
||||
- Suggested Android app replacements
|
||||
- How to get the cutting-edge version by [building the source-code](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/wiki/Building-from-source)
|
||||
- Weird things OEMs do
|
||||
- [Suggested apps for analyzing APKs](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/wiki/How-to-contribute#useful-apps)
|
||||
|
||||
In any case, you **CANNOT** brick your device with this software!
|
||||
That's the main point, right?
|
||||
## Privacy
|
||||
|
||||
## Features
|
||||
UAD-ng does not collect or transmit any user data. The only external connections are `GET` requests to GitHub for fetching the [package list](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/blob/main/resources/assets/uad_lists.json) ([src/core/uad_lists.rs](src/core/uad_lists.rs)) and checking for updates ([src/core/update.rs](src/core/update.rs)).
|
||||
|
||||
- [x] Uninstall/Disable and Restore/Enable system packages
|
||||
- [x] Multi-user support (e.g. apps in work profiles)
|
||||
- [x] Export/Import your selection in `uad_exported_selection.txt`
|
||||
- [x] Multi-device support: you can connect multiple phones at the same time
|
||||
- [x] All your actions are logged, so you never forget what you've done
|
||||
## Contact
|
||||
|
||||
NB : System apps cannot truly be uninstalled without root (see the [FAQ](https://github.com/Universal-Debloater-Alliance/universal-android-debloater/wiki/FAQ))
|
||||
**For real-time communication and support, consider joining our Discord guild:**
|
||||
|
||||
## Universal Debloat Lists
|
||||
[<img width=64em alt="Discord symbol" src=./resources/images/icon_clyde_blurple.svg>](https://discord.gg/CzwbMCPEZa)
|
||||
|
||||
- [x] GFAM (Google/Facebook/Amazon/Microsoft)
|
||||
- [x] AOSP
|
||||
- [x] Manufacturers (OEM)
|
||||
- [x] Mobile carriers
|
||||
- [x] Qualcomm / Mediatek / Miscellaneous
|
||||
**If you prefer using Matrix, we have a bridge to Discord:**
|
||||
|
||||
## Manufacturers debloat lists
|
||||
* [ ] Archos
|
||||
* [X] Asus
|
||||
* [ ] Blackberry
|
||||
* [X] Fairphone
|
||||
* [ ] Gionee
|
||||
* [X] Google
|
||||
* [x] Honor
|
||||
* [ ] HTC
|
||||
* [X] Huawei
|
||||
* [X] iQOO
|
||||
* [X] LG
|
||||
* [X] Lenovo
|
||||
* [X] Motorola
|
||||
* [X] Nokia
|
||||
* [X] Nubia
|
||||
* [X] Nothing Phone
|
||||
* [X] OnePlus
|
||||
* [X] Oppo
|
||||
* [X] Realme
|
||||
* [X] Samsung
|
||||
* [X] Sony
|
||||
* [X] Tecno
|
||||
* [X] TCL
|
||||
* [X] Unihertz
|
||||
* [X] Vivo/iQOO
|
||||
* [X] Wiko
|
||||
* [X] Xiaomi
|
||||
* [X] ZTE
|
||||
[<img width=64em src=https://matrix.org/images/matrix-favicon.svg>](https://matrix.to/#/#uad-ng:matrix.org)
|
||||
|
||||
## Mobile carriers debloat lists
|
||||
## Friends
|
||||
|
||||
| Country | Carriers |
|
||||
| ------- | ------------------------------- |
|
||||
| France | Orange, SFR, Free, Bouygues |
|
||||
| USA | T-Mobile, Verizon, Sprint, AT&T |
|
||||
| Germany | Telekom |
|
||||
| UK | EE |
|
||||
Several projects integrate with or depend on UAD-ng's data and tools:
|
||||
|
||||
## How to use it
|
||||
- **[Canta](https://github.com/samolego/Canta)** — A mobile Android debloater that uses Shizuku for rootless privilege escalation. It integrates with UAD-ng's Universal Debloat List to provide safe package recommendations without requiring a PC.
|
||||
|
||||
- **Read the [FAQ](https://github.com/Universal-Debloater-Alliance/universal-android-debloater/wiki/FAQ)!**
|
||||
- **Do a proper backup of your data! You can never be too careful!**
|
||||
- Enable _Developer Options_ on your smartphone.
|
||||
- Turn on _USB Debugging_ from the developer panel.
|
||||
- From the settings, disconnect from any OEM accounts (when you delete an OEM
|
||||
account package it could lock you on the lock screen because the phone can't
|
||||
associate your identity anymore)
|
||||
- Install ADB (see the instructions by clicking on your OS below):
|
||||
<p>
|
||||
<details>
|
||||
<summary>LINUX</summary>
|
||||
|
||||
- Install _Android platform tools_ on your PC :
|
||||
|
||||
Debian Base:
|
||||
|
||||
```bash
|
||||
sudo apt install android-sdk-platform-tools
|
||||
```
|
||||
|
||||
Arch-Linux Base:
|
||||
|
||||
```bash
|
||||
sudo pacman -S android-tools
|
||||
```
|
||||
|
||||
Red Hat Base:
|
||||
|
||||
```bash
|
||||
sudo yum install android-tools
|
||||
```
|
||||
|
||||
OpenSUSE Base:
|
||||
|
||||
```bash
|
||||
sudo zypper install android-tools
|
||||
```
|
||||
|
||||
Gentoo Base:
|
||||
|
||||
```bash
|
||||
sudo emerge -av dev-util/android-tools
|
||||
```
|
||||
|
||||
NixOS Base:
|
||||
|
||||
```bash
|
||||
sudo nix-shell -p android-tools
|
||||
```
|
||||
|
||||
Alpine Linux or FreeBSD Base:
|
||||
|
||||
```bash
|
||||
sudo pkg install android-tools
|
||||
```
|
||||
|
||||
Fedora Base:
|
||||
|
||||
```bash
|
||||
sudo dnf install android-tools
|
||||
```
|
||||
|
||||
Replace sudo with doas depending on what you use
|
||||
|
||||
</details>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<details>
|
||||
<summary>MAC OS</summary>
|
||||
|
||||
- Install [Homebrew](https://brew.sh/)
|
||||
- Install _Android platform tools_
|
||||
|
||||
```bash
|
||||
brew install android-platform-tools
|
||||
```
|
||||
|
||||
</details>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<details>
|
||||
<summary>WINDOWS</summary>
|
||||
|
||||
- Download [android platform tools](https://dl.google.com/android/repository/platform-tools-latest-windows.zip)
|
||||
and unzip it somewhere.
|
||||
- [Add the android platform tools to your PATH](https://www.architectryan.com/2018/03/17/add-to-the-path-on-windows-10/)
|
||||
**OR** make sure to launch UAD from the same directory.
|
||||
|
||||
- [Install USB drivers for your device](https://developer.android.com/studio/run/oem-usb#Drivers)
|
||||
- Check your device is detected:
|
||||
|
||||
```bash
|
||||
adb devices
|
||||
```
|
||||
|
||||
</details>
|
||||
</p>
|
||||
|
||||
- Download the latest release of UAD GUI for your Operating System [here](https://github.com/Universal-Debloater-Alliance/universal-android-debloater/releases). Take the `opengl` version only if the default version (with a Vulkan backend) doesn't launch.
|
||||
|
||||
**NOTE:** Chinese phone users may need to use the AOSP list to remove some stock
|
||||
apps because those Chinese manufacturers (especially Xiaomi and Huawei) have been
|
||||
using the name of AOSP packages for their own (modified & closed-source) apps.
|
||||
|
||||
**IMPORTANT NOTE:** You will have to run this software whenever your OEM pushes
|
||||
an update to your phone as some _uninstalled_ system apps could be reinstalled.
|
||||
|
||||
## How to contribute
|
||||
|
||||
Hey-hey-hey! Don't go away so fast! This is a community project.
|
||||
That means I need you! I'm sure you want to make this project better anyway.
|
||||
|
||||
==> [How to contribute](https://github.com/Universal-Debloater-Alliance/universal-android-debloater/wiki)
|
||||
- **Projects by [Muntashir Akon](https://github.com/MuntashirAkon):**
|
||||
- **[AppManager](https://github.com/MuntashirAkon/AppManager)** — An advanced Android app manager with powerful debloating features.
|
||||
- **[android-debloat-list](https://github.com/MuntashirAkon/android-debloat-list)** — A comprehensive, community-driven list of Android packages to debloat, based on UAD-ng's list.
|
||||
|
||||
## Special thanks
|
||||
|
||||
- [@mawilms](https://github.com/mawilms) for his LotRO plugin manager ([Lembas](https://github.com/mawilms/lembas))
|
||||
which helped me a lot to understand how to use the [Iced](https://github.com/hecrj/iced)
|
||||
GUI library.
|
||||
- [@0x192](https://github.com/0x192) who created the original UAD project.
|
||||
- [@mawilms](https://github.com/mawilms) for his LotRO plugin manager ([Lembas](https://github.com/mawilms/lembas)) which helped a lot to understand how to use the [Iced](https://github.com/iced-rs/iced) GUI library.
|
||||
- [@casperstorm](https://github.com/casperstorm) for the UI/UX inspiration.
|
||||
|
|
|
|||
38
RELEASE.md
Normal file
38
RELEASE.md
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# Release management
|
||||
|
||||
## Semantic versioning
|
||||
|
||||
We're making use of [semantic versioning](https://semver.org/). We use semantic versioning for two things:
|
||||
|
||||
1. GitHub Milestones
|
||||
1. UAD-ng releases
|
||||
|
||||
Speaking of milestones: it could be that we have a bug-fix release (e.g. 1.0.3), but a contributor submits a PR that adds a new feature. What do we do? Merge after we released 1.0.3? Change the name of the milestone to for example 1.1.0? Or another approach?
|
||||
|
||||
In that case, we should closely review the PR and 'compare' it with what semantic versioning has to say about the submitted feature. If the changes are significant enough, we're renaming the milestone to (for example) 1.1.0.
|
||||
|
||||
## Checklist before releasing a new version
|
||||
|
||||
- [ ] Make sure the Milestone is completed and if not, check what needs to be done to complete it.
|
||||
- [ ] Make sure all merged PRs regarding the application follow the Conventional Commit Messages style. This makes sure that when generating a changelog, the changes look consistent, which in turn improves readability.
|
||||
- [ ] Make sure all merged PRs have the correct labels assigned, as the changelog is generated based on labels.
|
||||
- [ ] Create a release preparation PR that bumps the UAD-ng version in `Cargo.toml` and `Cargo.lock`.
|
||||
|
||||
When all the above has been checked, you can push a tag to `main` to trigger the GitHub Action that creates a release:
|
||||
|
||||
- Create the tag: `git tag -s v1.2.3 -am '1.2.3'`
|
||||
- Push the tag: `git push origin v1.2.3`
|
||||
|
||||
Change the version number to whatever version we're releasing.
|
||||
|
||||
## Development process
|
||||
|
||||
1. Update dependencies right after a new release, so that we start a new milestone with updated dependencies
|
||||
1. On-going development of bug-fixes and new features etc, along with fixing potential bugs, potentially due to the updated dependencies
|
||||
1. Release
|
||||
|
||||
## Faulty releases
|
||||
|
||||
If we publish a faulty release, for example containing a critical bug, this is how we should deal with it:
|
||||
|
||||
We issue a hotfix and keep the faulty release but we'll add warnings to the faulty release in the changelog.
|
||||
11
build.rs
Normal file
11
build.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
const META: &str = "resources/assets/manifest.rc";
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("cargo::rerun-if-changed={META}");
|
||||
if std::env::var("CARGO_CFG_TARGET_FAMILY")? == "windows" {
|
||||
embed_resource::compile(META, embed_resource::NONE)
|
||||
.manifest_optional()
|
||||
.unwrap();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
7
clippy.toml
Normal file
7
clippy.toml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
disallowed-types = [
|
||||
{ path = "iced::widget::Text", reason = "Use [`crate::gui::widgets::text`] instead. For further details, see #523 and #858" },
|
||||
]
|
||||
|
||||
disallowed-methods= [
|
||||
{ path = "iced::widget::text", reason = "Use [`crate::gui::widgets::text`] instead. For further details, see #523 and #858" },
|
||||
]
|
||||
52
crates/uad-cli/Cargo.toml
Normal file
52
crates/uad-cli/Cargo.toml
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
[package]
|
||||
name = "uad-cli"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
readme.workspace = true
|
||||
keywords.workspace = true
|
||||
categories = ["command-line-utilities"]
|
||||
edition.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "uad"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
uad-core = { path = "../uad-core" }
|
||||
clap.workspace = true
|
||||
clap_complete = "4.4"
|
||||
tokio.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
log.workspace = true
|
||||
chrono.workspace = true
|
||||
toml.workspace = true
|
||||
dirs.workspace = true
|
||||
ureq.workspace = true
|
||||
retry.workspace = true
|
||||
csv.workspace = true
|
||||
rustyline = "14.0"
|
||||
|
||||
[lints.rust]
|
||||
deprecated_safe = "warn"
|
||||
|
||||
[lints.clippy]
|
||||
undocumented_unsafe_blocks = "deny"
|
||||
exit = "deny"
|
||||
panic_in_result_fn = "warn"
|
||||
infinite_loop = "warn"
|
||||
mem_forget = "warn"
|
||||
implicit_clone = "warn"
|
||||
format_push_string = "warn"
|
||||
large_include_file = "warn"
|
||||
shadow_unrelated = "warn"
|
||||
struct_field_names = "allow" # annoying
|
||||
|
||||
disallowed_types = "deny"
|
||||
disallowed_methods = "deny"
|
||||
|
||||
allow_attributes_without_reason = "warn"
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
44
crates/uad-cli/examples/usage.sh
Executable file
44
crates/uad-cli/examples/usage.sh
Executable file
|
|
@ -0,0 +1,44 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# Example usage script for UAD CLI
|
||||
|
||||
# Check if uad-cli is available
|
||||
if ! command -v uad &> /dev/null; then
|
||||
echo "uad command not found. Please build and install uad-cli first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== UAD CLI Usage Examples ==="
|
||||
echo
|
||||
|
||||
echo "1. List connected devices:"
|
||||
uad devices
|
||||
echo
|
||||
|
||||
echo "2. List all enabled packages:"
|
||||
uad list --state enabled | head -20
|
||||
echo "... (showing first 20)"
|
||||
echo
|
||||
|
||||
echo "3. Search for Google packages:"
|
||||
uad list --search google | head -10
|
||||
echo "... (showing first 10)"
|
||||
echo
|
||||
|
||||
echo "4. Show info about a package (example):"
|
||||
echo "uad info com.google.android.gms"
|
||||
echo
|
||||
|
||||
echo "5. Dry-run uninstall (safe to test):"
|
||||
echo "uad uninstall com.example.package --dry-run"
|
||||
echo
|
||||
|
||||
echo "6. Start interactive mode:"
|
||||
echo "uad repl"
|
||||
echo
|
||||
|
||||
echo "=== Generate shell completions ==="
|
||||
echo "For bash: uad completions bash > ~/.local/share/bash-completion/completions/uad"
|
||||
echo "For zsh: uad completions zsh > ~/.local/share/zsh/site-functions/_uad"
|
||||
echo "For fish: uad completions fish > ~/.config/fish/completions/uad.fish"
|
||||
|
||||
470
crates/uad-cli/src/commands.rs
Normal file
470
crates/uad-cli/src/commands.rs
Normal file
|
|
@ -0,0 +1,470 @@
|
|||
use clap::CommandFactory;
|
||||
use clap_complete::{Shell, generate};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::io::Write;
|
||||
use uad_core::adb::{ACommand, PmListPacksFlag};
|
||||
use uad_core::sync::{
|
||||
CorePackage, Phone, User, apply_pkg_state_commands, get_devices_list, get_package_state,
|
||||
run_adb_shell_action,
|
||||
};
|
||||
use uad_core::uad_lists::{Package, PackageState, Removal, UadList, load_debloat_lists};
|
||||
use uad_core::utils::{matches_search, truncate_description};
|
||||
|
||||
use crate::device::{NO_DEVICES_FOUND, get_target_device, get_user};
|
||||
use crate::filters::{ListFilter, RemovalFilter, StateFilter};
|
||||
use crate::{Cli, print_or_exit, println_or_exit};
|
||||
|
||||
/// List all connected Android devices
|
||||
pub fn list_devices() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println_or_exit!("Scanning for connected devices...");
|
||||
let devices = get_devices_list();
|
||||
|
||||
if devices.is_empty() {
|
||||
return Err(NO_DEVICES_FOUND.into());
|
||||
}
|
||||
|
||||
println_or_exit!("\nFound {} device(s):\n", devices.len());
|
||||
for device in &devices {
|
||||
println_or_exit!(" Model: {}", device.model);
|
||||
println_or_exit!(" Serial: {}", device.adb_id);
|
||||
println_or_exit!(" Android SDK: {}", device.android_sdk);
|
||||
|
||||
if !device.user_list.is_empty() {
|
||||
println_or_exit!(" Users: {} user(s)", device.user_list.len());
|
||||
for user in &device.user_list {
|
||||
let protected = if user.protected { " (protected)" } else { "" };
|
||||
println_or_exit!(" - User ID: {}{}", user.id, protected);
|
||||
}
|
||||
}
|
||||
println_or_exit!();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Context for package filtering and display
|
||||
pub struct PackageListContext {
|
||||
pub state_filter: Option<StateFilter>,
|
||||
pub removal_filter: Option<RemovalFilter>,
|
||||
pub list_filter: Option<ListFilter>,
|
||||
pub search: Option<String>,
|
||||
}
|
||||
|
||||
impl PackageListContext {
|
||||
/// Check if package passes all filters
|
||||
pub fn filter_package(
|
||||
&self,
|
||||
pkg_name: &str,
|
||||
pkg_info: Option<&Package>,
|
||||
pkg_state: PackageState,
|
||||
) -> bool {
|
||||
// Removal filter
|
||||
if let Some(removal) = self.removal_filter {
|
||||
if !removal.matches(pkg_info) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// State filter
|
||||
if let Some(state) = self.state_filter {
|
||||
if !state.matches(pkg_state) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// List filter
|
||||
if let Some(list) = self.list_filter {
|
||||
if !list.matches(pkg_info) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Search filter
|
||||
if let Some(ref search_term) = self.search {
|
||||
let description = pkg_info.map(|p| p.description.as_str());
|
||||
if !matches_search(pkg_name, search_term, description) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Determine what information to show based on active filters
|
||||
pub fn display_config(&self) -> DisplayConfig {
|
||||
DisplayConfig {
|
||||
show_state: self.state_filter.is_none_or(|f| !f.is_specific()),
|
||||
show_removal: self.removal_filter.is_none_or(|f| !f.is_specific()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DisplayConfig {
|
||||
pub show_state: bool,
|
||||
pub show_removal: bool,
|
||||
}
|
||||
|
||||
/// List packages on a device with filtering
|
||||
pub fn list_packages(
|
||||
device: Option<String>,
|
||||
state_filter: Option<StateFilter>,
|
||||
removal_filter: Option<RemovalFilter>,
|
||||
list_filter: Option<ListFilter>,
|
||||
search: Option<String>,
|
||||
user_id: Option<u16>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let target_device = get_target_device(device)?;
|
||||
let uad_lists = load_debloat_lists(false).unwrap_or_else(|lists| lists);
|
||||
|
||||
println_or_exit!(
|
||||
"Listing packages on: {} ({})\n",
|
||||
target_device.model,
|
||||
target_device.adb_id
|
||||
);
|
||||
|
||||
let context = PackageListContext {
|
||||
state_filter,
|
||||
removal_filter,
|
||||
list_filter,
|
||||
search,
|
||||
};
|
||||
|
||||
let pm_flag = state_filter.and_then(StateFilter::to_pm_flag);
|
||||
let system_packages = ACommand::new()
|
||||
.shell(&target_device.adb_id)
|
||||
.pm()
|
||||
.list_packages_sys(pm_flag, user_id)?;
|
||||
|
||||
let displayed_count = display_package_list(
|
||||
&system_packages,
|
||||
&uad_lists,
|
||||
&target_device.adb_id,
|
||||
user_id,
|
||||
&context,
|
||||
)?;
|
||||
|
||||
if displayed_count == 0 {
|
||||
println_or_exit!(" No packages found matching the specified filters.");
|
||||
} else {
|
||||
println_or_exit!("\nTotal: {} package(s)", displayed_count);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Display a list of packages with their info
|
||||
pub fn display_package_list(
|
||||
packages: &[String],
|
||||
uad_lists: &HashMap<String, Package>,
|
||||
device_serial: &str,
|
||||
user_id: Option<u16>,
|
||||
context: &PackageListContext,
|
||||
) -> Result<usize, Box<dyn std::error::Error>> {
|
||||
let config = context.display_config();
|
||||
let mut displayed_count = 0;
|
||||
|
||||
let enabled_packages: HashSet<String> = ACommand::new()
|
||||
.shell(device_serial)
|
||||
.pm()
|
||||
.list_packages_sys(Some(PmListPacksFlag::OnlyEnabled), user_id)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.collect();
|
||||
let disabled_packages: HashSet<String> = ACommand::new()
|
||||
.shell(device_serial)
|
||||
.pm()
|
||||
.list_packages_sys(Some(PmListPacksFlag::OnlyDisabled), user_id)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
for pkg_name in packages {
|
||||
let pkg_info = uad_lists.get(pkg_name);
|
||||
|
||||
let pkg_state = if enabled_packages.contains(pkg_name) {
|
||||
PackageState::Enabled
|
||||
} else if disabled_packages.contains(pkg_name) {
|
||||
PackageState::Disabled
|
||||
} else {
|
||||
PackageState::Uninstalled
|
||||
};
|
||||
|
||||
if !context.filter_package(pkg_name, pkg_info, pkg_state) {
|
||||
continue;
|
||||
}
|
||||
|
||||
display_package_entry(pkg_name, pkg_info, pkg_state, &config);
|
||||
displayed_count += 1;
|
||||
}
|
||||
|
||||
Ok(displayed_count)
|
||||
}
|
||||
|
||||
/// Display a single package entry
|
||||
pub fn display_package_entry(
|
||||
pkg_name: &str,
|
||||
pkg_info: Option<&Package>,
|
||||
pkg_state: PackageState,
|
||||
config: &DisplayConfig,
|
||||
) {
|
||||
print_or_exit!("[");
|
||||
|
||||
if let Some(info) = pkg_info {
|
||||
if config.show_removal {
|
||||
print_or_exit!("{}", info.removal);
|
||||
if config.show_state {
|
||||
print_or_exit!(" - ");
|
||||
}
|
||||
}
|
||||
if config.show_state {
|
||||
print_or_exit!("{}", pkg_state);
|
||||
}
|
||||
print_or_exit!("] {}", pkg_name);
|
||||
if !info.description.is_empty() {
|
||||
print_or_exit!(" - {}", truncate_description(&info.description, 80));
|
||||
}
|
||||
} else {
|
||||
if config.show_removal {
|
||||
print_or_exit!("Unlisted");
|
||||
if config.show_state {
|
||||
print_or_exit!(" - ");
|
||||
}
|
||||
}
|
||||
if config.show_state {
|
||||
print_or_exit!("{}", pkg_state);
|
||||
}
|
||||
print_or_exit!("] {}", pkg_name);
|
||||
}
|
||||
|
||||
println_or_exit!();
|
||||
}
|
||||
|
||||
/// Change the state of one or more packages
|
||||
pub fn change_package_state(
|
||||
packages: &[String],
|
||||
device: Option<String>,
|
||||
user_id: Option<u16>,
|
||||
dry_run: bool,
|
||||
target_state: PackageState,
|
||||
action_name: &str,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if packages.is_empty() {
|
||||
return Err("No packages specified".into());
|
||||
}
|
||||
|
||||
let target_device = get_target_device(device)?;
|
||||
let user = get_user(&target_device, user_id)?;
|
||||
let uad_lists = load_debloat_lists(false).unwrap_or_else(|lists| lists);
|
||||
|
||||
println!(
|
||||
"{} {} package(s) on: {} ({})\n",
|
||||
action_name,
|
||||
packages.len(),
|
||||
target_device.model,
|
||||
target_device.adb_id
|
||||
);
|
||||
|
||||
if dry_run {
|
||||
println!("DRY RUN - No changes will be made\n");
|
||||
}
|
||||
|
||||
for pkg_name in packages {
|
||||
process_package_state_change(
|
||||
pkg_name,
|
||||
&target_device,
|
||||
user,
|
||||
target_state,
|
||||
dry_run,
|
||||
&uad_lists,
|
||||
)?;
|
||||
println!();
|
||||
}
|
||||
|
||||
if dry_run {
|
||||
println!("Dry run completed. No changes were made.");
|
||||
} else {
|
||||
println!("Operation completed successfully.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Process state change for a single package
|
||||
fn process_package_state_change(
|
||||
pkg_name: &str,
|
||||
device: &Phone,
|
||||
user: User,
|
||||
target_state: PackageState,
|
||||
dry_run: bool,
|
||||
uad_lists: &HashMap<String, Package>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let current_state = get_package_state(&device.adb_id, pkg_name, Some(user.id))
|
||||
.ok_or("Package not found on device")?;
|
||||
|
||||
println!(" {} ({})", pkg_name, current_state);
|
||||
|
||||
if current_state == target_state {
|
||||
println!(" → Already in target state, skipping");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let pkg_info = uad_lists.get(pkg_name);
|
||||
if let Some(info) = pkg_info {
|
||||
if info.removal == Removal::Unsafe {
|
||||
println!(" ⚠ WARNING: This package is marked as UNSAFE to remove!");
|
||||
}
|
||||
}
|
||||
|
||||
let core_pkg = CorePackage {
|
||||
name: pkg_name.to_string(),
|
||||
description: pkg_info.map(|p| p.description.clone()).unwrap_or_default(),
|
||||
removal: pkg_info.map(|p| p.removal).unwrap_or(Removal::Unlisted),
|
||||
state: current_state,
|
||||
list: pkg_info.map(|p| p.list).unwrap_or(UadList::Unlisted),
|
||||
};
|
||||
|
||||
let commands = apply_pkg_state_commands(&core_pkg, target_state, user, device);
|
||||
|
||||
if dry_run {
|
||||
for cmd in &commands {
|
||||
println!(" Would run: {}", cmd);
|
||||
}
|
||||
} else {
|
||||
execute_with_fallback(
|
||||
pkg_name,
|
||||
target_state,
|
||||
&core_pkg,
|
||||
user,
|
||||
device,
|
||||
&commands,
|
||||
" ",
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Execute commands and verify package state with fallback
|
||||
pub fn execute_with_fallback(
|
||||
package: &str,
|
||||
target_state: PackageState,
|
||||
core_pkg: &CorePackage,
|
||||
user: User,
|
||||
device: &Phone,
|
||||
commands: &[String],
|
||||
indent: &str,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Capture the before-state of packages on other users for cross-user detection
|
||||
let before_cross_user_states =
|
||||
uad_core::sync::capture_cross_user_states(package, &device.adb_id, user.id, device);
|
||||
|
||||
// Execute commands
|
||||
for cmd in commands {
|
||||
match run_adb_shell_action(&device.adb_id, cmd.as_str()) {
|
||||
Ok(_) => println!("{}✓ {}", indent, cmd),
|
||||
Err(e) => {
|
||||
return Err(format!("{indent}✗ Failed to execute `{cmd}`: {e:?}").into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify package state and attempt fallback if needed
|
||||
let actual_state = match get_package_state(&device.adb_id, package, Some(user.id)) {
|
||||
Some(state) => state,
|
||||
None if target_state == PackageState::Uninstalled => PackageState::Uninstalled,
|
||||
None => {
|
||||
return Err(format!(
|
||||
"Package '{package}' was not found on device for user {}",
|
||||
user.id
|
||||
)
|
||||
.into());
|
||||
}
|
||||
};
|
||||
|
||||
if actual_state != target_state {
|
||||
println!(
|
||||
"{}⚠ Package state verification failed: expected {:?}, got {:?}",
|
||||
indent, target_state, actual_state
|
||||
);
|
||||
|
||||
// Attempt fallback
|
||||
if let Ok(fallback_action) =
|
||||
uad_core::sync::attempt_fallback(core_pkg, target_state, actual_state, user, device)
|
||||
{
|
||||
println!("{}↻ Fallback: {}", indent, fallback_action);
|
||||
} else {
|
||||
println!("{}✗ No fallback available", indent);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for cross-user behavior if operation succeeded
|
||||
if actual_state == target_state {
|
||||
if let Some(notification) = uad_core::sync::detect_cross_user_behavior(
|
||||
package,
|
||||
device.adb_id.as_str(),
|
||||
user.id,
|
||||
target_state,
|
||||
actual_state,
|
||||
device,
|
||||
&before_cross_user_states,
|
||||
) {
|
||||
println!("{}ℹ {}", indent, notification);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Show detailed information about a package
|
||||
pub fn show_package_info(
|
||||
package: &str,
|
||||
device: Option<String>,
|
||||
user_id: Option<u16>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("Package: {}\n", package);
|
||||
|
||||
let uad_lists = load_debloat_lists(false).unwrap_or_else(|lists| lists);
|
||||
|
||||
if let Some(pkg_info) = uad_lists.get(package) {
|
||||
println!("UAD Information:");
|
||||
println!(" List: {}", pkg_info.list);
|
||||
println!(" Removal: {}", pkg_info.removal);
|
||||
println!(" Description: {}", pkg_info.description);
|
||||
println!();
|
||||
} else {
|
||||
println!(" Not found in UAD lists (unlisted package)\n");
|
||||
}
|
||||
|
||||
if let Some(device_id) = device {
|
||||
let target_device = get_target_device(Some(device_id))?;
|
||||
let user = get_user(&target_device, user_id)?;
|
||||
println!("Device: {} ({})", target_device.model, target_device.adb_id);
|
||||
println!("User: {}", user.id);
|
||||
|
||||
let state = get_package_state(&target_device.adb_id, package, Some(user.id))
|
||||
.ok_or("Package not found on device")?;
|
||||
println!(" State: {}", state);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update UAD package lists from remote repository
|
||||
pub fn update_lists() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("Updating UAD package lists from remote repository...");
|
||||
|
||||
match load_debloat_lists(true) {
|
||||
Ok(_lists) => {
|
||||
println!("✓ Successfully updated package lists");
|
||||
Ok(())
|
||||
}
|
||||
Err(_lists) => Err("Failed to update lists from remote, using cached version".into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate shell completion script
|
||||
pub fn generate_completions(shell: Shell) {
|
||||
let mut cmd = Cli::command();
|
||||
let name = cmd.get_name().to_string();
|
||||
generate(shell, &mut cmd, name, &mut std::io::stdout());
|
||||
}
|
||||
47
crates/uad-cli/src/device.rs
Normal file
47
crates/uad-cli/src/device.rs
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
use uad_core::sync::{Phone, User, get_devices_list};
|
||||
|
||||
pub const NO_DEVICES_FOUND: &str =
|
||||
"No devices found. Make sure ADB is installed and devices are connected.";
|
||||
|
||||
/// Get target device, either by serial or first available
|
||||
pub fn get_target_device(device: Option<String>) -> Result<Phone, Box<dyn std::error::Error>> {
|
||||
let devices = get_devices_list();
|
||||
|
||||
if devices.is_empty() {
|
||||
return Err(NO_DEVICES_FOUND.into());
|
||||
}
|
||||
|
||||
let target_device = if let Some(device_id) = device {
|
||||
devices
|
||||
.iter()
|
||||
.find(|d| d.adb_id == device_id)
|
||||
.ok_or("Device not found")?
|
||||
.clone()
|
||||
} else {
|
||||
if devices.len() > 1 {
|
||||
eprintln!(
|
||||
"Warning: Multiple devices found, using first one: {}",
|
||||
devices[0].adb_id
|
||||
);
|
||||
}
|
||||
devices[0].clone()
|
||||
};
|
||||
|
||||
Ok(target_device)
|
||||
}
|
||||
|
||||
/// Get user from device, creating a basic one if not found
|
||||
pub fn get_user(device: &Phone, user_id: Option<u16>) -> Result<User, Box<dyn std::error::Error>> {
|
||||
let uid = user_id.unwrap_or(0);
|
||||
|
||||
if let Some(user) = device.user_list.iter().find(|u| u.id == uid) {
|
||||
Ok(*user)
|
||||
} else {
|
||||
// Create a basic user if not found in list
|
||||
Ok(User {
|
||||
id: uid,
|
||||
index: 0,
|
||||
protected: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
98
crates/uad-cli/src/filters.rs
Normal file
98
crates/uad-cli/src/filters.rs
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
use clap::ValueEnum;
|
||||
use uad_core::adb::PmListPacksFlag;
|
||||
use uad_core::uad_lists::{Package, PackageState, Removal, UadList};
|
||||
|
||||
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||
pub enum StateFilter {
|
||||
/// Show all packages regardless of state
|
||||
All,
|
||||
/// Show only enabled packages
|
||||
Enabled,
|
||||
/// Show only disabled packages
|
||||
Disabled,
|
||||
/// Show only uninstalled packages
|
||||
Uninstalled,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||
pub enum RemovalFilter {
|
||||
All,
|
||||
Recommended,
|
||||
Advanced,
|
||||
Expert,
|
||||
Unsafe,
|
||||
Unlisted,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||
pub enum ListFilter {
|
||||
All,
|
||||
Aosp,
|
||||
Carrier,
|
||||
Google,
|
||||
Misc,
|
||||
Oem,
|
||||
Pending,
|
||||
Unlisted,
|
||||
}
|
||||
|
||||
impl StateFilter {
|
||||
pub fn to_pm_flag(self) -> Option<PmListPacksFlag> {
|
||||
match self {
|
||||
Self::Enabled => Some(PmListPacksFlag::OnlyEnabled),
|
||||
Self::Disabled => Some(PmListPacksFlag::OnlyDisabled),
|
||||
Self::Uninstalled | Self::All => Some(PmListPacksFlag::IncludeUninstalled),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn matches(self, pkg_state: PackageState) -> bool {
|
||||
match self {
|
||||
Self::All => true,
|
||||
Self::Enabled => pkg_state == PackageState::Enabled,
|
||||
Self::Disabled => pkg_state == PackageState::Disabled,
|
||||
Self::Uninstalled => pkg_state == PackageState::Uninstalled,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_specific(self) -> bool {
|
||||
matches!(self, Self::Enabled | Self::Disabled | Self::Uninstalled)
|
||||
}
|
||||
}
|
||||
|
||||
impl RemovalFilter {
|
||||
pub fn matches(self, pkg_info: Option<&Package>) -> bool {
|
||||
match pkg_info {
|
||||
Some(info) => match self {
|
||||
Self::All => true,
|
||||
Self::Recommended => info.removal == Removal::Recommended,
|
||||
Self::Advanced => info.removal == Removal::Advanced,
|
||||
Self::Expert => info.removal == Removal::Expert,
|
||||
Self::Unsafe => info.removal == Removal::Unsafe,
|
||||
Self::Unlisted => info.removal == Removal::Unlisted,
|
||||
},
|
||||
None => matches!(self, Self::All | Self::Unlisted),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_specific(self) -> bool {
|
||||
!matches!(self, Self::All)
|
||||
}
|
||||
}
|
||||
|
||||
impl ListFilter {
|
||||
pub fn matches(self, pkg_info: Option<&Package>) -> bool {
|
||||
match pkg_info {
|
||||
Some(info) => match self {
|
||||
Self::All => true,
|
||||
Self::Aosp => info.list == UadList::Aosp,
|
||||
Self::Carrier => info.list == UadList::Carrier,
|
||||
Self::Google => info.list == UadList::Google,
|
||||
Self::Misc => info.list == UadList::Misc,
|
||||
Self::Oem => info.list == UadList::Oem,
|
||||
Self::Pending => info.list == UadList::Pending,
|
||||
Self::Unlisted => info.list == UadList::Unlisted,
|
||||
},
|
||||
None => matches!(self, Self::All | Self::Unlisted),
|
||||
}
|
||||
}
|
||||
}
|
||||
251
crates/uad-cli/src/main.rs
Normal file
251
crates/uad-cli/src/main.rs
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
#![allow(
|
||||
clippy::needless_continue,
|
||||
clippy::collapsible_if,
|
||||
clippy::uninlined_format_args,
|
||||
clippy::map_unwrap_or,
|
||||
clippy::unnecessary_wraps,
|
||||
reason = "Suppress non-critical pedantic/style lints to keep build green"
|
||||
)]
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use clap_complete::Shell;
|
||||
use std::process::ExitCode;
|
||||
use uad_core::uad_lists::PackageState;
|
||||
|
||||
mod commands;
|
||||
mod device;
|
||||
mod filters;
|
||||
mod output;
|
||||
mod repl;
|
||||
|
||||
use filters::{ListFilter, RemovalFilter, StateFilter};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "uad")]
|
||||
#[command(about = "Universal Android Debloater - Command Line Interface", long_about = None)]
|
||||
#[command(version)]
|
||||
#[command(propagate_version = true)]
|
||||
pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// List connected Android devices
|
||||
Devices,
|
||||
|
||||
/// List packages on a device
|
||||
#[command(name = "list", visible_alias = "ls")]
|
||||
List {
|
||||
/// Device serial number (optional, uses first device if not specified)
|
||||
#[arg(short, long)]
|
||||
device: Option<String>,
|
||||
|
||||
/// Filter by package state
|
||||
#[arg(short, long, value_enum)]
|
||||
state: Option<StateFilter>,
|
||||
|
||||
/// Filter by removal category
|
||||
#[arg(short, long, value_enum)]
|
||||
removal: Option<RemovalFilter>,
|
||||
|
||||
/// Filter by UAD list
|
||||
#[arg(short, long, value_enum)]
|
||||
list: Option<ListFilter>,
|
||||
|
||||
/// Search pattern (matches package name or description)
|
||||
#[arg(short = 'q', long)]
|
||||
search: Option<String>,
|
||||
|
||||
/// User ID (defaults to 0)
|
||||
#[arg(short, long)]
|
||||
user: Option<u16>,
|
||||
},
|
||||
|
||||
/// Uninstall packages (default removal action)
|
||||
#[command(visible_alias = "rm")]
|
||||
Uninstall {
|
||||
/// Package names to uninstall
|
||||
packages: Vec<String>,
|
||||
|
||||
/// Device serial number (optional, uses first device if not specified)
|
||||
#[arg(short, long)]
|
||||
device: Option<String>,
|
||||
|
||||
/// User ID (defaults to 0)
|
||||
#[arg(short, long)]
|
||||
user: Option<u16>,
|
||||
|
||||
/// Dry run - show what would be done without actually doing it
|
||||
#[arg(long)]
|
||||
dry_run: bool,
|
||||
},
|
||||
|
||||
/// Restore (reinstall) packages
|
||||
#[command(visible_alias = "restore")]
|
||||
Enable {
|
||||
/// Package names to restore/enable
|
||||
packages: Vec<String>,
|
||||
|
||||
/// Device serial number (optional, uses first device if not specified)
|
||||
#[arg(short, long)]
|
||||
device: Option<String>,
|
||||
|
||||
/// User ID (defaults to 0)
|
||||
#[arg(short, long)]
|
||||
user: Option<u16>,
|
||||
|
||||
/// Dry run - show what would be done without actually doing it
|
||||
#[arg(long)]
|
||||
dry_run: bool,
|
||||
},
|
||||
|
||||
/// Disable packages (keeps data but prevents execution)
|
||||
Disable {
|
||||
/// Package names to disable
|
||||
packages: Vec<String>,
|
||||
|
||||
/// Device serial number (optional, uses first device if not specified)
|
||||
#[arg(short, long)]
|
||||
device: Option<String>,
|
||||
|
||||
/// User ID (defaults to 0)
|
||||
#[arg(short, long)]
|
||||
user: Option<u16>,
|
||||
|
||||
/// Dry run - show what would be done without actually doing it
|
||||
#[arg(long)]
|
||||
dry_run: bool,
|
||||
},
|
||||
|
||||
/// Show detailed information about a package
|
||||
Info {
|
||||
/// Package name
|
||||
package: String,
|
||||
|
||||
/// Device serial number (optional, uses first device if not specified)
|
||||
#[arg(short, long)]
|
||||
device: Option<String>,
|
||||
|
||||
/// User ID (defaults to 0)
|
||||
#[arg(short, long)]
|
||||
user: Option<u16>,
|
||||
},
|
||||
|
||||
/// Update UAD package lists from remote repository
|
||||
Update,
|
||||
|
||||
/// Generate shell completion script
|
||||
Completions {
|
||||
/// Shell to generate completions for
|
||||
#[arg(value_enum)]
|
||||
shell: Shell,
|
||||
},
|
||||
|
||||
/// Start interactive REPL mode
|
||||
#[command(visible_alias = "shell")]
|
||||
Repl {
|
||||
/// Device serial number (optional, uses first device if not specified)
|
||||
#[arg(short, long)]
|
||||
device: Option<String>,
|
||||
|
||||
/// User ID (defaults to 0)
|
||||
#[arg(short, long)]
|
||||
user: Option<u16>,
|
||||
},
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> ExitCode {
|
||||
match run() {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
Err(err) => {
|
||||
eprintln!("Error: {err}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
match cli.command {
|
||||
Commands::Devices => {
|
||||
commands::list_devices()?;
|
||||
}
|
||||
Commands::List {
|
||||
device,
|
||||
state,
|
||||
removal,
|
||||
list,
|
||||
search,
|
||||
user,
|
||||
} => {
|
||||
commands::list_packages(device, state, removal, list, search, user)?;
|
||||
}
|
||||
Commands::Uninstall {
|
||||
packages,
|
||||
device,
|
||||
user,
|
||||
dry_run,
|
||||
} => {
|
||||
commands::change_package_state(
|
||||
&packages,
|
||||
device,
|
||||
user,
|
||||
dry_run,
|
||||
PackageState::Uninstalled,
|
||||
"Uninstalling",
|
||||
)?;
|
||||
}
|
||||
Commands::Enable {
|
||||
packages,
|
||||
device,
|
||||
user,
|
||||
dry_run,
|
||||
} => {
|
||||
commands::change_package_state(
|
||||
&packages,
|
||||
device,
|
||||
user,
|
||||
dry_run,
|
||||
PackageState::Enabled,
|
||||
"Enabling",
|
||||
)?;
|
||||
}
|
||||
Commands::Disable {
|
||||
packages,
|
||||
device,
|
||||
user,
|
||||
dry_run,
|
||||
} => {
|
||||
commands::change_package_state(
|
||||
&packages,
|
||||
device,
|
||||
user,
|
||||
dry_run,
|
||||
PackageState::Disabled,
|
||||
"Disabling",
|
||||
)?;
|
||||
}
|
||||
Commands::Info {
|
||||
package,
|
||||
device,
|
||||
user,
|
||||
} => {
|
||||
commands::show_package_info(&package, device, user)?;
|
||||
}
|
||||
Commands::Update => {
|
||||
commands::update_lists()?;
|
||||
}
|
||||
Commands::Completions { shell } => {
|
||||
commands::generate_completions(shell);
|
||||
}
|
||||
Commands::Repl { device, user } => {
|
||||
repl::repl_mode(device, user)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
18
crates/uad-cli/src/output.rs
Normal file
18
crates/uad-cli/src/output.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
/// Helper macro to handle broken pipe errors gracefully
|
||||
/// When piping to commands like `head`, we want to exit cleanly when the pipe closes
|
||||
#[macro_export]
|
||||
macro_rules! println_or_exit {
|
||||
() => {
|
||||
let _ = writeln!(std::io::stdout());
|
||||
};
|
||||
($($arg:tt)*) => {
|
||||
let _ = writeln!(std::io::stdout(), $($arg)*);
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! print_or_exit {
|
||||
($($arg:tt)*) => {
|
||||
let _ = write!(std::io::stdout(), $($arg)*);
|
||||
};
|
||||
}
|
||||
374
crates/uad-cli/src/repl.rs
Normal file
374
crates/uad-cli/src/repl.rs
Normal file
|
|
@ -0,0 +1,374 @@
|
|||
use rustyline::DefaultEditor;
|
||||
use rustyline::error::ReadlineError;
|
||||
use std::collections::HashMap;
|
||||
use std::io::Write;
|
||||
use uad_core::adb::ACommand;
|
||||
use uad_core::sync::{CorePackage, Phone, User, apply_pkg_state_commands, get_package_state};
|
||||
use uad_core::uad_lists::{Package, PackageState, Removal, UadList, load_debloat_lists};
|
||||
|
||||
use crate::commands::{PackageListContext, display_package_list, execute_with_fallback};
|
||||
use crate::device::{get_target_device, get_user};
|
||||
use crate::filters::StateFilter;
|
||||
use crate::println_or_exit;
|
||||
|
||||
enum ReplAction {
|
||||
Continue,
|
||||
Exit,
|
||||
}
|
||||
|
||||
/// Start interactive REPL mode
|
||||
pub fn repl_mode(
|
||||
device: Option<String>,
|
||||
user_id: Option<u16>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("Universal Android Debloater - Interactive Mode");
|
||||
println!("Type 'help' for available commands, 'exit' or 'quit' to leave\n");
|
||||
|
||||
let target_device = get_target_device(device)?;
|
||||
let user = get_user(&target_device, user_id)?;
|
||||
|
||||
println!(
|
||||
"Connected to: {} ({})",
|
||||
target_device.model, target_device.adb_id
|
||||
);
|
||||
println!("User: {}\n", user.id);
|
||||
|
||||
let uad_lists = load_debloat_lists(false).unwrap_or_else(|lists| lists);
|
||||
let mut rl = DefaultEditor::new()?;
|
||||
|
||||
// Try to load history
|
||||
let history_file = dirs::cache_dir().map(|d| d.join("uad").join("cli_history.txt"));
|
||||
if let Some(ref path) = history_file {
|
||||
let _ = rl.load_history(path);
|
||||
}
|
||||
|
||||
loop {
|
||||
let readline = rl.readline("uad> ");
|
||||
match readline {
|
||||
Ok(line) => {
|
||||
match handle_repl_line(&line, &mut rl, &target_device, user, user_id, &uad_lists) {
|
||||
Ok(ReplAction::Continue) => {}
|
||||
Ok(ReplAction::Exit) => break,
|
||||
Err(e) => {
|
||||
eprintln!("Error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(ReadlineError::Interrupted) => {
|
||||
println!("Interrupted (Ctrl-C). Type 'exit' to quit.");
|
||||
continue;
|
||||
}
|
||||
Err(ReadlineError::Eof) => {
|
||||
println!("Goodbye!");
|
||||
break;
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("Error: {:?}", err);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save history
|
||||
if let Some(ref path) = history_file {
|
||||
if let Some(parent) = path.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
let _ = rl.save_history(path);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle a single line of REPL input
|
||||
fn handle_repl_line(
|
||||
line: &str,
|
||||
rl: &mut DefaultEditor,
|
||||
device: &Phone,
|
||||
user: User,
|
||||
user_id: Option<u16>,
|
||||
uad_lists: &HashMap<String, Package>,
|
||||
) -> Result<ReplAction, Box<dyn std::error::Error>> {
|
||||
let line = line.trim();
|
||||
if line.is_empty() {
|
||||
return Ok(ReplAction::Continue);
|
||||
}
|
||||
|
||||
let _ = rl.add_history_entry(line);
|
||||
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if parts.is_empty() {
|
||||
return Ok(ReplAction::Continue);
|
||||
}
|
||||
|
||||
match parts[0] {
|
||||
"help" => print_repl_help(),
|
||||
"exit" | "quit" => {
|
||||
println!("Goodbye!");
|
||||
return Ok(ReplAction::Exit);
|
||||
}
|
||||
"list" | "ls" => {
|
||||
handle_list_command(&parts[1..], device, user_id, uad_lists)?;
|
||||
}
|
||||
"info" => {
|
||||
handle_info_command(&parts[1..], device, user.id, uad_lists)?;
|
||||
}
|
||||
"uninstall" | "rm" => {
|
||||
handle_state_change_command(
|
||||
&parts[1..],
|
||||
device,
|
||||
user,
|
||||
PackageState::Uninstalled,
|
||||
"Uninstalling",
|
||||
uad_lists,
|
||||
)?;
|
||||
}
|
||||
"enable" | "restore" => {
|
||||
handle_state_change_command(
|
||||
&parts[1..],
|
||||
device,
|
||||
user,
|
||||
PackageState::Enabled,
|
||||
"Enabling",
|
||||
uad_lists,
|
||||
)?;
|
||||
}
|
||||
"disable" => {
|
||||
handle_state_change_command(
|
||||
&parts[1..],
|
||||
device,
|
||||
user,
|
||||
PackageState::Disabled,
|
||||
"Disabling",
|
||||
uad_lists,
|
||||
)?;
|
||||
}
|
||||
"device" => {
|
||||
println!(
|
||||
"Device: {} ({}), Android SDK: {}, User: {}",
|
||||
device.model, device.adb_id, device.android_sdk, user.id
|
||||
);
|
||||
}
|
||||
"clear" => {
|
||||
print!("\x1B[2J\x1B[1;1H");
|
||||
}
|
||||
_ => {
|
||||
eprintln!(
|
||||
"Unknown command: '{}'. Type 'help' for available commands.",
|
||||
parts[0]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ReplAction::Continue)
|
||||
}
|
||||
|
||||
/// Parse REPL arguments into filters
|
||||
struct ReplListArgs {
|
||||
state_filter: Option<StateFilter>,
|
||||
search_term: Option<String>,
|
||||
}
|
||||
|
||||
impl ReplListArgs {
|
||||
fn parse(args: &[&str]) -> Result<Self, String> {
|
||||
let mut state_filter = None;
|
||||
let mut search_term = None;
|
||||
let mut i = 0;
|
||||
|
||||
while i < args.len() {
|
||||
match args[i] {
|
||||
"--state" | "-s" => {
|
||||
if i + 1 >= args.len() {
|
||||
return Err("--state requires a value".to_string());
|
||||
}
|
||||
state_filter = match args[i + 1].to_lowercase().as_str() {
|
||||
"enabled" => Some(StateFilter::Enabled),
|
||||
"disabled" => Some(StateFilter::Disabled),
|
||||
"uninstalled" => Some(StateFilter::Uninstalled),
|
||||
"all" => Some(StateFilter::All),
|
||||
_ => return Err(format!("Invalid state: {}", args[i + 1])),
|
||||
};
|
||||
i += 2;
|
||||
}
|
||||
"--search" | "-q" => {
|
||||
if i + 1 >= args.len() {
|
||||
return Err("--search requires a value".to_string());
|
||||
}
|
||||
search_term = Some(args[i + 1].to_string());
|
||||
i += 2;
|
||||
}
|
||||
_ => {
|
||||
return Err(format!("Unknown option: {}", args[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
state_filter,
|
||||
search_term,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle list command in REPL
|
||||
fn handle_list_command(
|
||||
args: &[&str],
|
||||
device: &Phone,
|
||||
user_id: Option<u16>,
|
||||
uad_lists: &HashMap<String, Package>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let parsed = ReplListArgs::parse(args)?;
|
||||
|
||||
let pm_flag = parsed.state_filter.and_then(StateFilter::to_pm_flag);
|
||||
let system_packages = ACommand::new()
|
||||
.shell(&device.adb_id)
|
||||
.pm()
|
||||
.list_packages_sys(pm_flag, user_id)?;
|
||||
|
||||
let context = PackageListContext {
|
||||
state_filter: parsed.state_filter,
|
||||
removal_filter: None,
|
||||
list_filter: None,
|
||||
search: parsed.search_term,
|
||||
};
|
||||
|
||||
let displayed_count = display_package_list(
|
||||
&system_packages,
|
||||
uad_lists,
|
||||
&device.adb_id,
|
||||
user_id,
|
||||
&context,
|
||||
)?;
|
||||
|
||||
if displayed_count == 0 {
|
||||
println_or_exit!(" No packages found.");
|
||||
} else {
|
||||
println_or_exit!("\nTotal: {} package(s)", displayed_count);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle info command in REPL
|
||||
fn handle_info_command(
|
||||
args: &[&str],
|
||||
device: &Phone,
|
||||
user_id: u16,
|
||||
uad_lists: &HashMap<String, Package>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if args.is_empty() {
|
||||
eprintln!("Usage: info <package_name>");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let package = args[0];
|
||||
println!("Package: {}", package);
|
||||
|
||||
if let Some(pkg_info) = uad_lists.get(package) {
|
||||
println!(" List: {}", pkg_info.list);
|
||||
println!(" Removal: {}", pkg_info.removal);
|
||||
println!(" Description: {}", pkg_info.description);
|
||||
} else {
|
||||
println!(" Not found in UAD lists (unlisted package)");
|
||||
}
|
||||
|
||||
let state = get_package_state(&device.adb_id, package, Some(user_id))
|
||||
.ok_or("Package not found on device")?;
|
||||
println!(" State: {}", state);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle state change command in REPL
|
||||
fn handle_state_change_command(
|
||||
args: &[&str],
|
||||
device: &Phone,
|
||||
user: User,
|
||||
target_state: PackageState,
|
||||
action_name: &str,
|
||||
uad_lists: &HashMap<String, Package>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if args.is_empty() {
|
||||
eprintln!(
|
||||
"Usage: {} <package_name> [package_name...]",
|
||||
action_name.to_lowercase()
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for pkg_name in args {
|
||||
process_package_change(pkg_name, device, user, target_state, action_name, uad_lists)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Process state change for a single package in REPL
|
||||
fn process_package_change(
|
||||
pkg_name: &str,
|
||||
device: &Phone,
|
||||
user: User,
|
||||
target_state: PackageState,
|
||||
action_name: &str,
|
||||
uad_lists: &HashMap<String, Package>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let current_state = get_package_state(&device.adb_id, pkg_name, Some(user.id))
|
||||
.ok_or("Package not found on device")?;
|
||||
|
||||
println!("{} {} (current: {})", action_name, pkg_name, current_state);
|
||||
|
||||
if current_state == target_state {
|
||||
println!(" → Already in target state, skipping");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let pkg_info = uad_lists.get(pkg_name);
|
||||
if let Some(info) = pkg_info {
|
||||
if info.removal == Removal::Unsafe {
|
||||
println!(" ⚠ WARNING: This package is marked as UNSAFE to remove!");
|
||||
}
|
||||
}
|
||||
|
||||
let core_pkg = CorePackage {
|
||||
name: pkg_name.to_string(),
|
||||
description: pkg_info.map(|p| p.description.clone()).unwrap_or_default(),
|
||||
removal: pkg_info.map(|p| p.removal).unwrap_or(Removal::Unlisted),
|
||||
state: current_state,
|
||||
list: pkg_info.map(|p| p.list).unwrap_or(UadList::Unlisted),
|
||||
};
|
||||
|
||||
let commands = apply_pkg_state_commands(&core_pkg, target_state, user, device);
|
||||
|
||||
execute_with_fallback(
|
||||
pkg_name,
|
||||
target_state,
|
||||
&core_pkg,
|
||||
user,
|
||||
device,
|
||||
&commands,
|
||||
" ",
|
||||
)
|
||||
}
|
||||
|
||||
/// Print REPL help message
|
||||
fn print_repl_help() {
|
||||
println!("Available commands:");
|
||||
println!(" list [--state <state>] [--search <term>]");
|
||||
println!(" List packages with optional filters");
|
||||
println!(" info <package_name>");
|
||||
println!(" Show information about a package");
|
||||
println!(" uninstall <package_name> [package_name...]");
|
||||
println!(" Uninstall one or more packages");
|
||||
println!(" enable <package_name> [package_name...]");
|
||||
println!(" Enable/restore one or more packages");
|
||||
println!(" disable <package_name> [package_name...]");
|
||||
println!(" Disable one or more packages");
|
||||
println!(" device");
|
||||
println!(" Show current device information");
|
||||
println!(" clear");
|
||||
println!(" Clear the screen");
|
||||
println!(" help");
|
||||
println!(" Show this help message");
|
||||
println!(" exit, quit");
|
||||
println!(" Exit the interactive mode");
|
||||
}
|
||||
52
crates/uad-core/Cargo.toml
Normal file
52
crates/uad-core/Cargo.toml
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
[package]
|
||||
name = "uad-core"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
readme.workspace = true
|
||||
keywords.workspace = true
|
||||
categories = ["command-line-utilities"]
|
||||
edition.workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
self-update = ["dep:flate2", "dep:tar"]
|
||||
|
||||
[dependencies]
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
log.workspace = true
|
||||
chrono.workspace = true
|
||||
toml.workspace = true
|
||||
dirs.workspace = true
|
||||
ureq.workspace = true
|
||||
retry.workspace = true
|
||||
csv.workspace = true
|
||||
flate2 = { workspace = true, optional = true }
|
||||
tar = { workspace = true, optional = true }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
win32console.workspace = true
|
||||
|
||||
[lints.rust]
|
||||
deprecated_safe = "warn"
|
||||
|
||||
[lints.clippy]
|
||||
undocumented_unsafe_blocks = "forbid"
|
||||
exit = "deny"
|
||||
panic_in_result_fn = "warn"
|
||||
infinite_loop = "warn"
|
||||
mem_forget = "warn"
|
||||
implicit_clone = "warn"
|
||||
format_push_string = "warn"
|
||||
large_include_file = "warn"
|
||||
shadow_unrelated = "warn"
|
||||
struct_field_names = "allow" # annoying
|
||||
|
||||
disallowed_types = "deny"
|
||||
disallowed_methods = "deny"
|
||||
|
||||
allow_attributes_without_reason = "warn"
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
503
crates/uad-core/src/adb.rs
Normal file
503
crates/uad-core/src/adb.rs
Normal file
|
|
@ -0,0 +1,503 @@
|
|||
#![deny(clippy::unwrap_used)]
|
||||
|
||||
//! This module is intended to group everything that's "intrinsic" of ADB.
|
||||
//!
|
||||
//! Following the design philosophy of most of Rust `std`,
|
||||
//! `*Command` are intended to be "thin wrappers" (low-overhead abstractions)
|
||||
//! around the ADB CLI or `adb_client`
|
||||
//! ([in the future](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/issues/700) ),
|
||||
//! which implies:
|
||||
//! - no "magic"
|
||||
//! - no custom commands
|
||||
//! - no chaining ("piping") of existing commands
|
||||
//!
|
||||
//! This guarantees a 1-to-1 mapping between methods and cmds,
|
||||
//! thereby reducing surprises such as:
|
||||
//! - Non-atomic operations: consider what happens if a pack changes state
|
||||
//! in the middle of listing enabled and disabled packs!
|
||||
//! - Non-standard semantics: what would happen if a new ADB version
|
||||
//! supports a feature we already defined,
|
||||
//! but has _slightly_ different behavior?
|
||||
//!
|
||||
//! Despite being "low-level", we can still "have cake and eat it too";
|
||||
//! After all, what's the point of an abstraction if it doesn't come with goodies?:
|
||||
//! We can reserve some artistic license, such as:
|
||||
//! - pre-parsing or validanting output, to provide types with invariants
|
||||
//! - strongly-typed rather than "stringly-typed" APIs
|
||||
//! - nicer IDE support
|
||||
//! - compile-time prevention of malformed cmds
|
||||
//! - implicit enforcement of a narrow set of operations
|
||||
//!
|
||||
//! About that last point, if there's ever a need for an ADB feature
|
||||
//! which these APIs don't expose,
|
||||
//! please, **PLEASE** refrain from falling-back to any `Command`-like API.
|
||||
//! Rather, please extend these APIs in a consistent way.
|
||||
//!
|
||||
//! Thank you! ❤️
|
||||
//!
|
||||
//! For comprehensive info about ADB,
|
||||
//! [see this](https://android.googlesource.com/platform/packages/modules/adb/+/refs/heads/master/docs/)
|
||||
|
||||
use std::rc::Rc;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::os::windows::process::CommandExt;
|
||||
|
||||
use crate::utils::is_all_w_c;
|
||||
use log::{error, info, warn};
|
||||
|
||||
/// Convert ADB output bytes to a trimmed UTF-8 string.
|
||||
/// Uses lossy conversion to prevent panics on non-UTF8 output from certain OEMs.
|
||||
#[must_use]
|
||||
pub fn to_trimmed_utf8(v: &[u8]) -> String {
|
||||
String::from_utf8_lossy(v).trim_end().to_string()
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
#[must_use]
|
||||
fn is_version_triple(s: &str) -> bool {
|
||||
let mut components = s.split('.');
|
||||
for _ in 0..3 {
|
||||
if !components
|
||||
.next()
|
||||
.is_some_and(|comp| comp.as_bytes().iter().all(u8::is_ascii_digit))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if components.next().is_some() {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Builder object for an Android Debug Bridge CLI command,
|
||||
/// using the type-state and new-type patterns.
|
||||
///
|
||||
/// This is not intended to model the entire ADB API.
|
||||
/// It only models the subset that concerns UADNG.
|
||||
///
|
||||
/// [More info here](https://developer.android.com/tools/adb)
|
||||
#[derive(Debug)]
|
||||
pub struct ACommand(std::process::Command);
|
||||
impl ACommand {
|
||||
/// `adb` command builder
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self(std::process::Command::new("adb"))
|
||||
}
|
||||
|
||||
/// `shell` sub-command builder.
|
||||
///
|
||||
/// If `device_serial` is empty, it lets ADB choose the default device.
|
||||
#[must_use]
|
||||
pub fn shell<S: AsRef<str>>(mut self, device_serial: S) -> ShellCommand {
|
||||
let serial = device_serial.as_ref();
|
||||
if !serial.is_empty() {
|
||||
self.0.args(["-s", serial]);
|
||||
}
|
||||
self.0.arg("shell");
|
||||
ShellCommand(self)
|
||||
}
|
||||
|
||||
/// Header-less list of attached devices (as serials) and their statuses:
|
||||
/// - USB
|
||||
/// - TCP/IP: WIFI, Ethernet, etc...
|
||||
/// - Local emulators
|
||||
///
|
||||
/// Status can be (but not limited to):
|
||||
/// - "unauthorized"
|
||||
/// - "device"
|
||||
pub fn devices(mut self) -> Result<Vec<(String, String)>, String> {
|
||||
self.0.arg("devices");
|
||||
Ok(self
|
||||
.run()?
|
||||
.lines()
|
||||
.skip(1) // header
|
||||
.map(|dev_stat| {
|
||||
let tab_idx = dev_stat
|
||||
// OS-specific?
|
||||
.find('\t')
|
||||
// True on Linux,
|
||||
// no matter if ADB is piped or connected to terminal
|
||||
.expect("There must be 1 tab after serial");
|
||||
(
|
||||
// serial
|
||||
dev_stat[..tab_idx].to_string(),
|
||||
// status
|
||||
dev_stat[(tab_idx + 1)..].to_string(),
|
||||
)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// `version` sub-command
|
||||
///
|
||||
/// ## Format
|
||||
/// This is just a sample,
|
||||
/// we don't know which guarantees are stable (yet):
|
||||
/// ```txt
|
||||
/// Android Debug Bridge version 1.0.41
|
||||
/// Version 34.0.5-debian
|
||||
/// Installed as /usr/lib/android-sdk/platform-tools/adb
|
||||
/// Running on Linux 6.12.12-amd64 (x86_64)
|
||||
/// ```
|
||||
///
|
||||
/// The expected format should be like:
|
||||
/// ```txt
|
||||
/// Android Debug Bridge version <num>.<num>.<num>
|
||||
/// Version <num>.<num>.<num>-<no spaces>
|
||||
/// Installed as <ANDROID_SDK_HOME>/platform-tools/adb[.exe]
|
||||
/// Running on <OS/kernel version> (<CPU arch>)
|
||||
/// ```
|
||||
#[expect(clippy::panic_in_result_fn, reason = "Assertions are fine")]
|
||||
pub fn version(mut self) -> Result<String, String> {
|
||||
self.0.arg("version");
|
||||
let out = self.run()?;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
const ADBV: &str = "Android Debug Bridge version ";
|
||||
const V: &str = "Version ";
|
||||
|
||||
let mut lns = out.lines();
|
||||
|
||||
assert!(
|
||||
lns.next()
|
||||
.is_some_and(|ln| ln.starts_with(ADBV) && is_version_triple(&ln[ADBV.len()..]))
|
||||
);
|
||||
assert!(lns.next().is_some_and(|ln| ln.starts_with(V)
|
||||
&& is_version_triple(&ln[V.len()..ln.find('-').unwrap_or(ln.len())])));
|
||||
// missing test for valid path
|
||||
assert!(lns.next().is_some_and(|ln| ln.starts_with("Installed as ")
|
||||
&& (ln.ends_with("adb") || ln.ends_with("adb.exe"))));
|
||||
// missing test for x86/ARM (both 64b)
|
||||
assert!(lns.next().is_some_and(|ln| ln.starts_with("Running on ")));
|
||||
if lns.next().is_some() {
|
||||
unreachable!("Expected < 5 lines")
|
||||
}
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// General executor
|
||||
fn run(self) -> Result<String, String> {
|
||||
let mut cmd = self.0;
|
||||
#[cfg(target_os = "windows")]
|
||||
let cmd = cmd.creation_flags(0x0800_0000); // do not open a cmd window
|
||||
|
||||
info!(
|
||||
"Ran command: adb {}",
|
||||
cmd.get_args()
|
||||
.map(|s| s.to_str().unwrap_or_else(|| unreachable!()))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
);
|
||||
match cmd.output() {
|
||||
Err(e) => {
|
||||
error!("ADB: {e}");
|
||||
Err("Cannot run ADB, likely not found".to_string())
|
||||
}
|
||||
Ok(o) => {
|
||||
let stdout = to_trimmed_utf8(&o.stdout);
|
||||
if o.status.success() {
|
||||
Ok(stdout)
|
||||
} else {
|
||||
let stderr = to_trimmed_utf8(&o.stderr);
|
||||
// ADB does really weird things:
|
||||
// Some errors are not redirected to `stderr`
|
||||
let err = if stdout.is_empty() { stderr } else { stdout };
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ACommand {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder object for a command that runs on the device's default `sh` implementation.
|
||||
/// Typically MKSH, but could be Ash.
|
||||
///
|
||||
/// [More info](https://chromium.googlesource.com/aosp/platform/system/core/+/refs/heads/upstream/shell_and_utilities).
|
||||
#[derive(Debug)]
|
||||
pub struct ShellCommand(ACommand);
|
||||
impl ShellCommand {
|
||||
/// `pm` command builder
|
||||
pub fn pm(mut self) -> PmCommand {
|
||||
self.0.0.arg("pm");
|
||||
PmCommand(self)
|
||||
}
|
||||
/// Query a device property value, by its key.
|
||||
/// These can be of any type:
|
||||
/// - `boolean`
|
||||
/// - `int`
|
||||
/// - chars
|
||||
/// - etc...
|
||||
///
|
||||
/// So to avoid lossy conversions, we return strs
|
||||
pub fn getprop(mut self, key: &str) -> Result<String, String> {
|
||||
self.0.0.args(["getprop", key]);
|
||||
self.0.run()
|
||||
}
|
||||
/// Reboots device
|
||||
pub fn reboot(mut self) -> Result<String, String> {
|
||||
self.0.0.arg("reboot");
|
||||
self.0.run()
|
||||
}
|
||||
|
||||
/// Execute an arbitrary shell action string on the device's default shell.
|
||||
/// The action string is passed as a single argument to `adb shell` and
|
||||
/// interpreted by the remote shell (which splits on spaces).
|
||||
pub fn raw(mut self, action: &str) -> Result<String, String> {
|
||||
self.0.0.arg(action);
|
||||
self.0.run()
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn is_pkg_component(s: &[u8]) -> bool {
|
||||
if s.is_empty() {
|
||||
return false;
|
||||
}
|
||||
s[0].is_ascii_alphabetic()
|
||||
&& if s.len() > 1 {
|
||||
is_all_w_c(s.split_at(1).1)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// String with the invariant of being a valid package-name.
|
||||
/// See [`PackageId::new`] for validation details.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct PackageId(Rc<str>);
|
||||
impl PackageId {
|
||||
/// Creates a package-ID if it's `"android"` or valid according to:
|
||||
/// - <https://developer.android.com/guide/topics/manifest/manifest-element.html#package>
|
||||
/// - <https://developer.android.com/build/configure-app-module#set-application-id>
|
||||
#[must_use]
|
||||
pub fn new(p_id: &str) -> Option<Self> {
|
||||
if p_id == "android" {
|
||||
return Some(Self(p_id.into()));
|
||||
}
|
||||
let mut components = p_id.split('.');
|
||||
for _ in 0..2 {
|
||||
if !components
|
||||
.next()
|
||||
.is_some_and(|comp| is_pkg_component(comp.as_bytes()))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
}
|
||||
if components.all(|comp| is_pkg_component(comp.as_bytes())) {
|
||||
Some(Self(p_id.into()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `pm list packages` flag/state/type
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PmListPacksFlag {
|
||||
/// `-u`, not to be confused with `-a`
|
||||
IncludeUninstalled,
|
||||
/// `-e`
|
||||
OnlyEnabled,
|
||||
/// `-d`
|
||||
OnlyDisabled,
|
||||
}
|
||||
impl PmListPacksFlag {
|
||||
// is there a trait for this?
|
||||
fn to_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::IncludeUninstalled => "-u",
|
||||
Self::OnlyEnabled => "-e",
|
||||
Self::OnlyDisabled => "-d",
|
||||
}
|
||||
}
|
||||
}
|
||||
#[expect(clippy::to_string_trait_impl, reason = "This is not user-facing")]
|
||||
impl ToString for PmListPacksFlag {
|
||||
fn to_string(&self) -> String {
|
||||
self.to_str().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
const PACK_PREFIX: &str = "package:";
|
||||
|
||||
pub const PM_CLEAR_PACK: &str = "pm clear";
|
||||
|
||||
/// Builder object for an Android Package Manager command.
|
||||
/// <https://developer.android.com/tools/adb#pm>
|
||||
#[derive(Debug)]
|
||||
#[must_use]
|
||||
pub struct PmCommand(ShellCommand);
|
||||
impl PmCommand {
|
||||
/// `list packages -s` sub-command, [`PACK_PREFIX`] stripped from each element.
|
||||
///
|
||||
/// `Ok` variant:
|
||||
/// - isn't 100% guaranteed to contain valid pack-IDs
|
||||
/// but you can assume it does (except in `unsafe {}` blocks)
|
||||
/// - isn't sorted
|
||||
/// - duplicates never _seem_ to happen, but don't assume uniqueness
|
||||
pub fn list_packages_sys(
|
||||
mut self,
|
||||
f: Option<PmListPacksFlag>,
|
||||
user_id: Option<u16>,
|
||||
) -> Result<Vec<String>, String> {
|
||||
let cmd = &mut self.0.0.0;
|
||||
|
||||
cmd.args(["list", "packages", "-s"]);
|
||||
if let Some(s) = f {
|
||||
cmd.arg(s.to_str());
|
||||
}
|
||||
if let Some(u) = user_id {
|
||||
cmd.arg("--user");
|
||||
cmd.arg(u.to_string());
|
||||
}
|
||||
|
||||
self.0.0.run().map(|pack_ls| {
|
||||
pack_ls
|
||||
.lines()
|
||||
.filter_map(|p_ln| {
|
||||
debug_assert!(p_ln.starts_with(PACK_PREFIX));
|
||||
let p = &p_ln[PACK_PREFIX.len()..];
|
||||
if PackageId::new(p).is_some() {
|
||||
Some(String::from(p))
|
||||
} else {
|
||||
warn!("skipping nonstandard package name: {p:?}");
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
/// `list users` sub-command, deserialized/parsed.
|
||||
///
|
||||
/// - <https://source.android.com/docs/devices/admin/multi-user-testing>
|
||||
/// - <https://stackoverflow.com/questions/37495126/android-get-list-of-users-and-profile-name>
|
||||
pub fn list_users(mut self) -> Result<Box<[UserInfo]>, String> {
|
||||
self.0.0.0.args(["list", "users"]);
|
||||
Ok(self
|
||||
.0
|
||||
.0
|
||||
.run()?
|
||||
.lines()
|
||||
.skip(1) // omit header
|
||||
.map(|ln| {
|
||||
// this could be optimized by making more API-stability assumptions
|
||||
let ln = ln.trim_ascii_start();
|
||||
let ln = ln.strip_prefix("UserInfo").unwrap_or(ln).trim_ascii_start();
|
||||
let ln = ln.strip_prefix('{').unwrap_or(ln).trim_ascii();
|
||||
//let run;
|
||||
let ln = if let Some(l) = ln.strip_suffix("running") {
|
||||
//run = true;
|
||||
l.trim_ascii_end()
|
||||
} else {
|
||||
//run = false;
|
||||
ln
|
||||
};
|
||||
let ln = ln.strip_suffix('}').unwrap_or(ln).trim_ascii_end();
|
||||
// https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/core/java/android/content/pm/UserInfo.java
|
||||
// The format looks stable today, but google may change it in future Android versions
|
||||
// (and very old Androids might differ). Keep parsing defensive.
|
||||
// Expected shape: "UserInfo{<id>:<name>:<flags>}[ running]"
|
||||
|
||||
let mut comps = ln.split(':');
|
||||
|
||||
let id = comps
|
||||
.next()
|
||||
.expect("There must be at least 1 ':'-separated component")
|
||||
.parse()
|
||||
.expect("string assumed to be UID numeral");
|
||||
//let name = comps
|
||||
// .next()
|
||||
// .expect("There must be at least 2 ':'-separated components. 2nd is user-name");
|
||||
//let flags = u32::from_str_radix(
|
||||
// comps.next().expect(
|
||||
// "There must be at least 3 ':'-separated components. 3rd is user bit-flags",
|
||||
// ),
|
||||
// 16,
|
||||
//)
|
||||
//.expect("string assumed to be hexadecimal bit-flags");
|
||||
UserInfo {
|
||||
id,
|
||||
//name: name.into(),
|
||||
//flags,
|
||||
//running: run,
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
/// Mirror of AOSP `UserInfo` Java Class, with an extra field
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UserInfo {
|
||||
id: u16,
|
||||
//name: Box<str>,
|
||||
//flags: u32,
|
||||
//running: bool,
|
||||
}
|
||||
impl UserInfo {
|
||||
#[must_use]
|
||||
pub const fn get_id(&self) -> u16 {
|
||||
self.id
|
||||
}
|
||||
/*
|
||||
/// Check if the user was logged-in at the time `pm list users` was invoked
|
||||
#[must_use]
|
||||
#[allow(dead_code, reason = "Currently unused by UI; kept for future features")]
|
||||
pub const fn was_running(&self) -> bool {
|
||||
self.running
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn invalid_pack_ids() {
|
||||
for p_id in [
|
||||
"",
|
||||
" ",
|
||||
".",
|
||||
"nodots",
|
||||
"com..example",
|
||||
"net.hello.",
|
||||
"org.0example",
|
||||
"org._foobar",
|
||||
"the.🎂.is.a.lie",
|
||||
"EXCLAMATION!!!!",
|
||||
] {
|
||||
assert_eq!(PackageId::new(p_id), None);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_pack_ids() {
|
||||
for p_id in [
|
||||
"A.a",
|
||||
"x.X",
|
||||
"org.example",
|
||||
"net.hello",
|
||||
"uwu.owo",
|
||||
"Am0Gu5.Zuz",
|
||||
"net.net.net.net.net.net.net.net.net.net.net",
|
||||
"com.github.w1nst0n",
|
||||
"this_.String_.is_.not_.real_",
|
||||
] {
|
||||
assert_ne!(PackageId::new(p_id), None);
|
||||
}
|
||||
}
|
||||
}
|
||||
150
crates/uad-core/src/config.rs
Normal file
150
crates/uad-core/src/config.rs
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
use crate::CACHE_DIR;
|
||||
use crate::CONFIG_DIR;
|
||||
use crate::sync::User;
|
||||
use crate::utils::DisplayablePath;
|
||||
use log::error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
const DEFAULT_THEME: &str = "Auto (follow system theme)";
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Config {
|
||||
pub general: GeneralSettings,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
|
||||
pub devices: Vec<DeviceSettings>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct GeneralSettings {
|
||||
pub theme: String,
|
||||
pub expert_mode: bool,
|
||||
pub backup_folder: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct BackupSettings {
|
||||
pub backups: Vec<DisplayablePath>,
|
||||
pub selected: Option<DisplayablePath>,
|
||||
pub users: Vec<User>,
|
||||
pub selected_user: Option<User>,
|
||||
pub backup_state: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
|
||||
pub struct DeviceSettings {
|
||||
/// Unique serial identifier
|
||||
pub device_id: String,
|
||||
pub disable_mode: bool, // should be `enum RemovalMode`
|
||||
pub multi_user_mode: bool,
|
||||
#[serde(skip)]
|
||||
pub backup: BackupSettings,
|
||||
}
|
||||
|
||||
impl Default for GeneralSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
theme: DEFAULT_THEME.to_string(),
|
||||
expert_mode: false,
|
||||
backup_folder: CACHE_DIR.join("backups"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static CONFIG_FILE: LazyLock<PathBuf> = LazyLock::new(|| CONFIG_DIR.join("config.toml"));
|
||||
|
||||
impl Config {
|
||||
pub fn save_device_settings(
|
||||
&mut self,
|
||||
device_settings: DeviceSettings,
|
||||
general: GeneralSettings,
|
||||
) {
|
||||
if let Some(device) = self
|
||||
.devices
|
||||
.iter_mut()
|
||||
.find(|x| x.device_id == device_settings.device_id)
|
||||
{
|
||||
*device = device_settings;
|
||||
} else {
|
||||
self.devices.push(device_settings);
|
||||
}
|
||||
self.general = general;
|
||||
let toml = toml::to_string(&self).unwrap();
|
||||
fs::write(&*CONFIG_FILE, toml).expect("Could not write config file to disk!");
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn load_configuration_file() -> Self {
|
||||
match fs::read_to_string(&*CONFIG_FILE) {
|
||||
Ok(s) => match toml::from_str(&s) {
|
||||
Ok(config) => return config,
|
||||
Err(e) => error!("Invalid config file: `{e}`"),
|
||||
},
|
||||
Err(e) => error!("Failed to read config file: `{e}`"),
|
||||
}
|
||||
error!("Restoring default config file");
|
||||
let toml = toml::to_string(&Self::default()).unwrap();
|
||||
fs::write(&*CONFIG_FILE, toml).expect("Could not write config file to disk!");
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
//! Unit tests
|
||||
|
||||
use super::*;
|
||||
use std::path::Path;
|
||||
|
||||
// create a clean default config file for testing
|
||||
fn create_default_config_file() {
|
||||
let toml = toml::to_string(&Config::default()).unwrap();
|
||||
fs::write(&*CONFIG_FILE, toml).expect("Could not write config file to disk!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_default_config_file() {
|
||||
create_default_config_file();
|
||||
assert!(CONFIG_FILE.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_configuration_file() {
|
||||
create_default_config_file();
|
||||
let config = Config::load_configuration_file();
|
||||
// non-deterministic
|
||||
//assert_eq!(config.devices.len(), 0);
|
||||
assert_eq!(config.general.theme, DEFAULT_THEME);
|
||||
assert!(!config.general.expert_mode);
|
||||
assert_eq!(config.general.backup_folder, CACHE_DIR.join("backups"));
|
||||
}
|
||||
|
||||
// non-deterministic
|
||||
/*
|
||||
#[test]
|
||||
fn test_save_changes() {
|
||||
let mut settings = Settings::default();
|
||||
let device_id = "test_device".to_string();
|
||||
settings.device.device_id = device_id.clone();
|
||||
Config::save_changes(&settings, &device_id);
|
||||
let config = Config::load_configuration_file();
|
||||
assert_eq!(config.devices[0].device_id, device_id);
|
||||
}
|
||||
*/
|
||||
|
||||
#[test]
|
||||
fn test_default_config() {
|
||||
let config = Config::default();
|
||||
assert_eq!(config.devices.len(), 0);
|
||||
assert_eq!(config.general.theme, DEFAULT_THEME);
|
||||
assert!(!config.general.expert_mode);
|
||||
assert_eq!(config.general.backup_folder, CACHE_DIR.join("backups"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_file_path() {
|
||||
assert_eq!(&*CONFIG_FILE, Path::new(&*CONFIG_DIR.join("config.toml")));
|
||||
}
|
||||
}
|
||||
23
crates/uad-core/src/lib.rs
Normal file
23
crates/uad-core/src/lib.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
#![allow(
|
||||
clippy::missing_panics_doc,
|
||||
clippy::missing_errors_doc,
|
||||
clippy::collapsible_if,
|
||||
clippy::uninlined_format_args,
|
||||
clippy::result_unit_err,
|
||||
reason = "Doc+style pedantic lints are out-of-scope for this pass"
|
||||
)]
|
||||
|
||||
pub mod adb;
|
||||
pub mod config;
|
||||
pub mod save;
|
||||
pub mod sync;
|
||||
pub mod uad_lists;
|
||||
pub mod update;
|
||||
pub mod utils;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::LazyLock;
|
||||
pub static CONFIG_DIR: LazyLock<PathBuf> =
|
||||
LazyLock::new(|| utils::setup_uad_dir(&dirs::config_dir().expect("Can't detect config dir")));
|
||||
pub static CACHE_DIR: LazyLock<PathBuf> =
|
||||
LazyLock::new(|| utils::setup_uad_dir(&dirs::cache_dir().expect("Can't detect cache dir")));
|
||||
|
|
@ -1,34 +1,31 @@
|
|||
use crate::core::config::DeviceSettings;
|
||||
use crate::core::sync::{apply_pkg_state_commands, CorePackage, Phone, User};
|
||||
use crate::core::utils::DisplayablePath;
|
||||
use crate::gui::widgets::package_row::PackageRow;
|
||||
use crate::CACHE_DIR;
|
||||
use crate::config::{Config, DeviceSettings};
|
||||
use crate::sync::{CorePackage, Phone, User, apply_pkg_state_commands};
|
||||
use crate::utils::DisplayablePath;
|
||||
use log::{error, info, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use static_init::dynamic;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[dynamic]
|
||||
pub static BACKUP_DIR: PathBuf = CACHE_DIR.join("backups");
|
||||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
#[derive(Default, Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
|
||||
struct PhoneBackup {
|
||||
device_id: String,
|
||||
users: Vec<UserBackup>,
|
||||
pub struct PhoneBackup {
|
||||
pub device_id: String,
|
||||
pub users: Vec<UserBackup>,
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
|
||||
struct UserBackup {
|
||||
id: u16,
|
||||
packages: Vec<CorePackage>,
|
||||
pub struct UserBackup {
|
||||
pub id: u16,
|
||||
pub packages: Vec<CorePackage>,
|
||||
}
|
||||
|
||||
// Backup all `Uninstalled` and `Disabled` packages
|
||||
pub async fn backup_phone(
|
||||
/// Backup all `Uninstalled` and `Disabled` packages
|
||||
pub fn backup_phone(
|
||||
users: Vec<User>,
|
||||
device_id: String,
|
||||
phone_packages: Vec<Vec<PackageRow>>,
|
||||
) -> Result<(), String> {
|
||||
phone_packages: &[Vec<CorePackage>],
|
||||
) -> Result<bool, String> {
|
||||
let mut backup = PhoneBackup {
|
||||
device_id: device_id.clone(),
|
||||
..PhoneBackup::default()
|
||||
|
|
@ -40,29 +37,27 @@ pub async fn backup_phone(
|
|||
..UserBackup::default()
|
||||
};
|
||||
|
||||
for p in phone_packages[u.index].clone() {
|
||||
user_backup.packages.push(CorePackage {
|
||||
name: p.name.clone(),
|
||||
state: p.state,
|
||||
});
|
||||
for p in phone_packages[u.index].iter().cloned() {
|
||||
user_backup.packages.push(p);
|
||||
}
|
||||
backup.users.push(user_backup);
|
||||
}
|
||||
|
||||
match serde_json::to_string_pretty(&backup) {
|
||||
Ok(json) => {
|
||||
let backup_path = &*BACKUP_DIR.join(device_id);
|
||||
let backup_dir: PathBuf = Config::load_configuration_file().general.backup_folder;
|
||||
let backup_path = &*backup_dir.join(device_id);
|
||||
|
||||
if let Err(e) = fs::create_dir_all(backup_path) {
|
||||
error!("BACKUP: could not create backup dir: {}", e);
|
||||
error!("BACKUP: could not create backup dir: {e}");
|
||||
return Err(e.to_string());
|
||||
};
|
||||
}
|
||||
|
||||
let backup_filename =
|
||||
format!("{}.json", chrono::Local::now().format("%Y-%m-%d_%H-%M-%S"));
|
||||
|
||||
match fs::write(backup_path.join(backup_filename), json) {
|
||||
Ok(_) => Ok(()),
|
||||
Ok(()) => Ok(true),
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
}
|
||||
|
|
@ -71,34 +66,30 @@ pub async fn backup_phone(
|
|||
}
|
||||
|
||||
pub fn list_available_backups(dir: &Path) -> Vec<DisplayablePath> {
|
||||
#[allow(clippy::option_if_let_else)]
|
||||
match fs::read_dir(dir) {
|
||||
Ok(files) => files
|
||||
.filter_map(|e| e.ok())
|
||||
.filter_map(Result::ok)
|
||||
.map(|e| DisplayablePath { path: e.path() })
|
||||
.collect::<Vec<_>>(),
|
||||
Err(_) => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn list_available_backup_user(backup: DisplayablePath) -> Vec<User> {
|
||||
match fs::read_to_string(backup.path) {
|
||||
Ok(data) => {
|
||||
let phone_backup: PhoneBackup =
|
||||
serde_json::from_str(&data).expect("Unable to parse backup file");
|
||||
|
||||
let mut users = vec![];
|
||||
for u in phone_backup.users {
|
||||
users.push(User {
|
||||
id: u.id,
|
||||
index: 0,
|
||||
protected: false,
|
||||
});
|
||||
}
|
||||
users
|
||||
}
|
||||
Ok(data) => serde_json::from_str::<PhoneBackup>(&data)
|
||||
.expect("Unable to parse backup file")
|
||||
.users
|
||||
.into_iter()
|
||||
.map(|u| User {
|
||||
id: u.id,
|
||||
index: 0,
|
||||
protected: false,
|
||||
})
|
||||
.collect(),
|
||||
Err(e) => {
|
||||
error!("[BACKUP]: Selected backup file not found: {}", e);
|
||||
error!("[BACKUP]: Selected backup file not found: {e}");
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
|
@ -106,15 +97,22 @@ pub fn list_available_backup_user(backup: DisplayablePath) -> Vec<User> {
|
|||
|
||||
#[derive(Debug)]
|
||||
pub struct BackupPackage {
|
||||
pub i_user: usize,
|
||||
pub index: usize,
|
||||
pub commands: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RestoreResult {
|
||||
pub packages: Vec<BackupPackage>,
|
||||
pub skipped_count: usize,
|
||||
}
|
||||
|
||||
pub fn restore_backup(
|
||||
selected_device: &Phone,
|
||||
packages: &[Vec<PackageRow>],
|
||||
packages: &[Vec<CorePackage>],
|
||||
settings: &DeviceSettings,
|
||||
) -> Result<Vec<BackupPackage>, String> {
|
||||
) -> Result<RestoreResult, String> {
|
||||
match fs::read_to_string(
|
||||
settings
|
||||
.backup
|
||||
|
|
@ -129,30 +127,31 @@ pub fn restore_backup(
|
|||
serde_json::from_str(&data).expect("Unable to parse backup file");
|
||||
|
||||
let mut commands = vec![];
|
||||
let mut skipped_packages = 0;
|
||||
for u in phone_backup.users {
|
||||
let index = match selected_device.user_list.iter().find(|x| x.id == u.id) {
|
||||
let i_user = match selected_device.user_list.iter().find(|x| x.id == u.id) {
|
||||
Some(i) => i.index,
|
||||
None => return Err(format!("user {} doesn't exist", u.id)),
|
||||
};
|
||||
|
||||
for (i, backup_package) in u.packages.iter().enumerate() {
|
||||
let package: CorePackage;
|
||||
match packages[index]
|
||||
let package: CorePackage = if let Some(p) = packages[i_user]
|
||||
.iter()
|
||||
.find(|x| x.name == backup_package.name)
|
||||
{
|
||||
Some(p) => package = p.into(),
|
||||
None => {
|
||||
return Err(format!(
|
||||
"{} not found for user {}",
|
||||
backup_package.name, u.id
|
||||
))
|
||||
}
|
||||
}
|
||||
p.clone()
|
||||
} else {
|
||||
skipped_packages += 1;
|
||||
warn!(
|
||||
"{} not found for user {} - skipping package during restore",
|
||||
backup_package.name, u.id
|
||||
);
|
||||
continue;
|
||||
};
|
||||
let p_commands = apply_pkg_state_commands(
|
||||
&package,
|
||||
backup_package.state,
|
||||
&settings
|
||||
settings
|
||||
.backup
|
||||
.selected_user
|
||||
.ok_or("field should be Some type")?,
|
||||
|
|
@ -160,19 +159,29 @@ pub fn restore_backup(
|
|||
);
|
||||
if !p_commands.is_empty() {
|
||||
commands.push(BackupPackage {
|
||||
i_user,
|
||||
index: i,
|
||||
commands: p_commands,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if skipped_packages > 0 {
|
||||
info!(
|
||||
"Restore completed with {skipped_packages} packages skipped (not found on device)"
|
||||
);
|
||||
}
|
||||
if !commands.is_empty() {
|
||||
commands.push(BackupPackage {
|
||||
i_user: 0,
|
||||
index: 0,
|
||||
commands: vec![],
|
||||
});
|
||||
}
|
||||
Ok(commands)
|
||||
Ok(RestoreResult {
|
||||
packages: commands,
|
||||
skipped_count: skipped_packages,
|
||||
})
|
||||
}
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
663
crates/uad-core/src/sync.rs
Normal file
663
crates/uad-core/src/sync.rs
Normal file
|
|
@ -0,0 +1,663 @@
|
|||
use crate::{
|
||||
adb::{ACommand as AdbCommand, PM_CLEAR_PACK, PackageId},
|
||||
uad_lists::PackageState,
|
||||
};
|
||||
use log::{error, info};
|
||||
use retry::{OperationResult, delay::Fixed, retry};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// An Android device, typically a phone
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Phone {
|
||||
/// Non-market name
|
||||
pub model: String, // could be `Copy`
|
||||
/// Android API level version
|
||||
pub android_sdk: u8,
|
||||
/// In theory, `len < u16::MAX` _should_ always be `true`.
|
||||
/// In practice, `len <= u8::MAX`.
|
||||
pub user_list: Vec<User>,
|
||||
/// Unique serial identifier
|
||||
pub adb_id: String, // could be `Copy`
|
||||
}
|
||||
|
||||
impl Default for Phone {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
model: "fetching devices...".to_string(),
|
||||
android_sdk: 0,
|
||||
user_list: vec![],
|
||||
adb_id: String::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Phone {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.model)
|
||||
}
|
||||
}
|
||||
|
||||
/// `UserInfo` but relevant to UAD
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct User {
|
||||
pub id: u16,
|
||||
pub index: usize,
|
||||
pub protected: bool,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for User {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "user {}", self.id)
|
||||
}
|
||||
}
|
||||
|
||||
/// An enum to contain different variants for errors yielded by ADB.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AdbError {
|
||||
Generic(String),
|
||||
}
|
||||
|
||||
/// Run an arbitrary shell action via the typed ADB wrapper.
|
||||
/// This replaces the deprecated `adb_shell_command`.
|
||||
///
|
||||
/// If `serial` is empty, it lets ADB choose the default device.
|
||||
pub fn run_adb_shell_action<S: AsRef<str>>(
|
||||
device_serial: S,
|
||||
action: &str,
|
||||
) -> Result<String, AdbError> {
|
||||
let serial = device_serial.as_ref();
|
||||
|
||||
match AdbCommand::new().shell(serial).raw(action) {
|
||||
Ok(o) => {
|
||||
if ["Error", "Failure"].iter().any(|&e| o.contains(e)) {
|
||||
let friendly_msg = make_friendly_error_message(&o, action);
|
||||
return Err(AdbError::Generic(friendly_msg));
|
||||
}
|
||||
info!("{action} -> {o}");
|
||||
Ok(o)
|
||||
}
|
||||
Err(err) => {
|
||||
if !err.contains("[not installed for") {
|
||||
let friendly_msg = make_friendly_error_message(&err, action);
|
||||
return Err(AdbError::Generic(friendly_msg));
|
||||
}
|
||||
Err(AdbError::Generic(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert common OEM-specific ADB error messages into user-friendly explanations.
|
||||
fn make_friendly_error_message(error_output: &str, action: &str) -> String {
|
||||
// Common Samsung errors
|
||||
if error_output.contains("DELETE_FAILED_USER_RESTRICTED") {
|
||||
return format!(
|
||||
"Cannot uninstall: This package is restricted by the device manufacturer (Samsung Knox or similar).\n\
|
||||
Error: {error_output}\n\
|
||||
Tip: Try disabling the package instead, or check device settings for Knox/security restrictions."
|
||||
);
|
||||
}
|
||||
|
||||
if error_output.contains("NOT_INSTALLED_FOR_USER") {
|
||||
return format!(
|
||||
"Package is not installed for the current user.\n\
|
||||
Error: {error_output}\n\
|
||||
Tip: The package may be installed for a different user profile or work profile."
|
||||
);
|
||||
}
|
||||
|
||||
// Empty package name error
|
||||
if error_output.contains("Shell cannot change component state for null") {
|
||||
return format!(
|
||||
"Invalid package: Empty package name detected.\n\
|
||||
Error: {error_output}\n\
|
||||
Tip: Please refresh the package list and try again."
|
||||
);
|
||||
}
|
||||
|
||||
// Generic permission errors
|
||||
if error_output.contains("Permission denied")
|
||||
|| error_output.contains("INSTALL_FAILED_PERMISSION_MODEL_DOWNGRADE")
|
||||
{
|
||||
return format!(
|
||||
"Permission denied: Insufficient privileges to perform this action.\n\
|
||||
Error: {error_output}\n\
|
||||
Tip: This may require root access or the package is protected by the system."
|
||||
);
|
||||
}
|
||||
|
||||
// Work profile / managed device errors
|
||||
if error_output.contains("DELETE_FAILED_DEVICE_POLICY_MANAGER") {
|
||||
return format!(
|
||||
"Cannot modify: Package is managed by device policy (MDM/EMM).\n\
|
||||
Error: {error_output}\n\
|
||||
Tip: Contact your IT administrator if this is a work device."
|
||||
);
|
||||
}
|
||||
|
||||
// Generic failure with context
|
||||
format!("{action} -> {error_output}")
|
||||
}
|
||||
|
||||
/// If `None`, returns an empty String, not " --user 0"
|
||||
#[must_use]
|
||||
pub fn user_flag(user_id: Option<User>) -> String {
|
||||
user_id
|
||||
.map(|user| format!(" --user {}", user.id))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
// Minimum information for processing adb commands
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
|
||||
pub struct CorePackage {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
#[serde(default)]
|
||||
pub removal: crate::uad_lists::Removal,
|
||||
#[serde(default)]
|
||||
pub state: PackageState,
|
||||
#[serde(default)]
|
||||
pub list: crate::uad_lists::UadList,
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn apply_pkg_state_commands(
|
||||
package: &CorePackage,
|
||||
wanted_state: PackageState,
|
||||
selected_user: User,
|
||||
phone: &Phone,
|
||||
) -> Vec<String> {
|
||||
// https://github.com/Universal-Debloater-Alliance/universal-android-debloater/wiki/ADB-reference
|
||||
// ALWAYS PUT THE COMMAND THAT CHANGES THE PACKAGE STATE FIRST!
|
||||
let commands = match wanted_state {
|
||||
PackageState::Enabled => match package.state {
|
||||
PackageState::Disabled => vec!["pm enable"],
|
||||
PackageState::Uninstalled => match phone.android_sdk {
|
||||
i if i >= 23 => vec!["cmd package install-existing"],
|
||||
21 | 22 => vec!["pm unhide"],
|
||||
19 | 20 => vec!["pm unblock", PM_CLEAR_PACK],
|
||||
_ => unreachable!("already prevented by the GUI"),
|
||||
},
|
||||
_ => vec![],
|
||||
},
|
||||
PackageState::Disabled => match package.state {
|
||||
PackageState::Uninstalled | PackageState::Enabled => {
|
||||
if supports_disabling(phone) {
|
||||
vec!["pm disable-user", "am force-stop", PM_CLEAR_PACK]
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
_ => vec![],
|
||||
},
|
||||
PackageState::Uninstalled => match package.state {
|
||||
PackageState::Enabled | PackageState::Disabled => match phone.android_sdk {
|
||||
sdk if sdk >= 23 => vec!["pm uninstall"], // > Marshmallow (6.0)
|
||||
21 | 22 => vec!["pm hide", PM_CLEAR_PACK], // Lollipop (5.x)
|
||||
_ => vec!["pm block", PM_CLEAR_PACK], // Disable mode is unavailable on older devices because the specific ADB commands need root
|
||||
},
|
||||
_ => vec![],
|
||||
},
|
||||
PackageState::All => vec![],
|
||||
}; // this should be a `tinyvec`, as `len <= 4`
|
||||
|
||||
let user = supports_multi_user(phone).then_some(selected_user);
|
||||
request_builder(&commands, &package.name, user)
|
||||
}
|
||||
|
||||
/// Build a command request to be sent via ADB to a device.
|
||||
/// `commands` accepts one or more ADB shell commands
|
||||
/// which act on a common `package` and `user`.
|
||||
#[must_use]
|
||||
pub fn request_builder(commands: &[&str], package: &str, user: Option<User>) -> Vec<String> {
|
||||
// Defense-in-depth: `package` is interpolated verbatim into a device-shell
|
||||
// action, so refuse any invalid name/ID. Every current caller already
|
||||
// reconciles the name against the live device package list before reaching
|
||||
// here, so this rejects nothing legitimate — it just keeps the no-injection
|
||||
// guarantee local to the sink instead of relying on each caller to sanitise.
|
||||
// Fail closed: emit no command for a malformed name rather than an injectable
|
||||
// device-shell string.
|
||||
if PackageId::new(package).is_none() {
|
||||
error!("request_builder: refusing invalid package name: {package:?}");
|
||||
return Vec::new();
|
||||
}
|
||||
let maybe_user_flag = user_flag(user);
|
||||
commands
|
||||
.iter()
|
||||
.map(|c| format!("{c}{maybe_user_flag} {package}"))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get the model by querying the `ro.product.model` property.
|
||||
///
|
||||
/// If `serial` is empty, it lets ADB choose the default device.
|
||||
#[must_use]
|
||||
pub fn get_device_model(serial: &str) -> String {
|
||||
AdbCommand::new()
|
||||
.shell(serial)
|
||||
.getprop("ro.product.model")
|
||||
.unwrap_or_else(|err| {
|
||||
eprintln!("ERROR: {err}");
|
||||
error!("{err}");
|
||||
if err.contains("adb: no devices/emulators found") {
|
||||
"no devices/emulators found".to_string()
|
||||
} else {
|
||||
err
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the brand by querying the `ro.product.brand` property.
|
||||
///
|
||||
/// If `serial` is empty, it lets ADB choose the default device.
|
||||
#[must_use]
|
||||
pub fn get_device_brand(serial: &str) -> String {
|
||||
AdbCommand::new()
|
||||
.shell(serial)
|
||||
.getprop("ro.product.brand")
|
||||
// `trim` is just-in-case
|
||||
.map(|s| s.trim().to_string())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Get Android SDK version by querying the
|
||||
// `ro.build.version.sdk` property or defaulting to 0.
|
||||
///
|
||||
/// If `device_serial` is empty, it lets ADB choose the default device.
|
||||
#[must_use]
|
||||
pub fn get_android_sdk(device_serial: &str) -> u8 {
|
||||
AdbCommand::new()
|
||||
.shell(device_serial)
|
||||
.getprop("ro.build.version.sdk")
|
||||
.map_or(0, |sdk| {
|
||||
sdk.parse().expect("SDK version numeral must be valid")
|
||||
})
|
||||
}
|
||||
|
||||
/// Capture the current state of a package across all non-protected users.
|
||||
/// This is used to detect cross-user behavior by comparing before and after states.
|
||||
///
|
||||
/// Only includes users where the package exists (Some state). Users where the package
|
||||
/// doesn't exist (None) are not tracked.
|
||||
#[must_use]
|
||||
pub fn capture_cross_user_states(
|
||||
package_name: &str,
|
||||
device_serial: &str,
|
||||
target_user_id: u16,
|
||||
phone: &Phone,
|
||||
) -> Vec<(u16, PackageState)> {
|
||||
phone
|
||||
.user_list
|
||||
.iter()
|
||||
.filter(|u| !u.protected && u.id != target_user_id)
|
||||
.filter_map(|u| {
|
||||
verify_package_state(package_name, device_serial, Some(u.id)).map(|state| (u.id, state))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Detect cross-user behavior and return appropriate notification message.
|
||||
/// This handles unexpected cross-user behavior:
|
||||
/// - Case A: Uninstall → Restore (package appears on other users)
|
||||
/// - Case B: Uninstall → Uninstall (package disappears from other users that previously had it)
|
||||
/// - Case C: Restore → Restore (package appears on other users)
|
||||
#[must_use]
|
||||
pub fn detect_cross_user_behavior(
|
||||
package_name: &str,
|
||||
device_serial: &str,
|
||||
target_user_id: u16,
|
||||
wanted_state: PackageState,
|
||||
actual_state: PackageState,
|
||||
phone: &Phone,
|
||||
before_states: &[(u16, PackageState)],
|
||||
) -> Option<String> {
|
||||
// Only check if operation was successful on target user
|
||||
if actual_state != wanted_state {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Only check if we have multiple users
|
||||
if phone.user_list.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let after_states =
|
||||
check_cross_user_package_existence(package_name, device_serial, target_user_id, phone);
|
||||
|
||||
match wanted_state {
|
||||
PackageState::Uninstalled => {
|
||||
if after_states.is_empty() {
|
||||
// Case B: Uninstall → Uninstall (check if all users lost package)
|
||||
let affected_users: Vec<_> = before_states
|
||||
.iter()
|
||||
.filter(|(uid, before_state)| {
|
||||
// Only flag if the package was installed/enabled/disabled before
|
||||
*before_state != PackageState::Uninstalled
|
||||
// And is NOT in after_states (doesn't exist in usable state anymore)
|
||||
&& !after_states.iter().any(|(after_uid, _)| after_uid == uid)
|
||||
})
|
||||
.map(|(uid, _)| uid)
|
||||
.collect();
|
||||
|
||||
if affected_users.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let user_list = affected_users
|
||||
.iter()
|
||||
.map(|uid| format!("user {uid}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
|
||||
Some(format!(
|
||||
"Detected cross-user uninstall: package was also uninstalled from {user_list} after uninstalling from user {target_user_id}"
|
||||
))
|
||||
}
|
||||
} else {
|
||||
// Case A: Uninstall → Restore (package appears on other users)
|
||||
let user_list = after_states
|
||||
.iter()
|
||||
.map(|(uid, state)| format!("user {uid} ({state:?})"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
|
||||
Some(format!(
|
||||
"Detected cross-user restoration: package exists on {user_list} after uninstalling from user {target_user_id}"
|
||||
))
|
||||
}
|
||||
}
|
||||
PackageState::Enabled | PackageState::Disabled => {
|
||||
// Case C: Restore → Restore (package appears on other users)
|
||||
|
||||
// Check if a user didn't have the package before (not tracked or package didn't exist).
|
||||
// Detects packages that appear on users where they didn't exist previously (OEM cross-user restoration).
|
||||
let was_package_absent_before = |uid: &u16| {
|
||||
before_states
|
||||
.iter()
|
||||
.find(|(before_uid, _)| before_uid == uid)
|
||||
.is_none_or(|(_, before_state)| *before_state == PackageState::Uninstalled)
|
||||
};
|
||||
|
||||
let newly_appeared: Vec<_> = after_states
|
||||
.iter()
|
||||
.filter(|(uid, _after_state)| was_package_absent_before(uid))
|
||||
.collect();
|
||||
|
||||
if newly_appeared.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let user_list = newly_appeared
|
||||
.iter()
|
||||
.map(|(uid, state)| format!("user {uid} ({state:?})"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
|
||||
Some(format!(
|
||||
"Detected cross-user restoration: package exists on {user_list} after {} from user {target_user_id}",
|
||||
if wanted_state == PackageState::Enabled {
|
||||
"enabling"
|
||||
} else {
|
||||
"disabling"
|
||||
}
|
||||
))
|
||||
}
|
||||
}
|
||||
PackageState::All => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn supports_disabling(dev: &Phone) -> bool {
|
||||
// >= Marshmallow (6.0)
|
||||
dev.android_sdk >= 23
|
||||
}
|
||||
|
||||
/// Check if it might support multi-user mode,
|
||||
/// by simply comparing SDK version.
|
||||
/// `true` isn't reliable, you can only trust `false`.
|
||||
///
|
||||
/// See:
|
||||
/// - <https://source.android.com/docs/devices/admin/multi-user#applying_the_overlay>
|
||||
/// - <https://developer.android.com/reference/android/os/UserManager#supportsMultipleUsers()>
|
||||
#[must_use]
|
||||
pub const fn supports_multi_user(dev: &Phone) -> bool {
|
||||
// >= Lollipop (5.0)
|
||||
dev.android_sdk >= 21
|
||||
}
|
||||
|
||||
/// Check if a `user_id` is protected on a device by trying
|
||||
/// to list associated packages.
|
||||
///
|
||||
/// If `device_serial` is empty, it lets ADB choose the default device.
|
||||
pub fn is_protected_user<S: AsRef<str>>(user_id: u16, device_serial: S) -> bool {
|
||||
AdbCommand::new()
|
||||
.shell(device_serial)
|
||||
.pm()
|
||||
.list_packages_sys(None, Some(user_id))
|
||||
.is_err()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn list_users_idx_prot(device_serial: &str) -> Vec<User> {
|
||||
AdbCommand::new()
|
||||
.shell(device_serial)
|
||||
.pm()
|
||||
.list_users()
|
||||
.map(|out| {
|
||||
out.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, user)| {
|
||||
let id = user.get_id();
|
||||
User {
|
||||
id,
|
||||
index: i,
|
||||
protected: is_protected_user(id, device_serial),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// This matches serials (`getprop ro.serialno`)
|
||||
/// that are authorized by the user.
|
||||
#[must_use]
|
||||
pub fn get_devices_list() -> Vec<Phone> {
|
||||
retry(
|
||||
Fixed::from_millis(500).take(if cfg!(debug_assertions) { 3 } else { 10 }),
|
||||
|| match AdbCommand::new().devices() {
|
||||
Ok(devices) => {
|
||||
let mut device_list: Vec<Phone> = vec![];
|
||||
if devices.iter().all(|(_, stat)| stat != "device") {
|
||||
return OperationResult::Retry(vec![]);
|
||||
}
|
||||
for device in devices {
|
||||
let serial = &device.0;
|
||||
device_list.push(Phone {
|
||||
model: format!("{} {}", get_device_brand(serial), get_device_model(serial)),
|
||||
android_sdk: get_android_sdk(serial),
|
||||
user_list: list_users_idx_prot(serial),
|
||||
adb_id: serial.clone(),
|
||||
});
|
||||
}
|
||||
OperationResult::Ok(device_list)
|
||||
}
|
||||
Err(err) => {
|
||||
error!("get_devices_list() -> {err}");
|
||||
let test: Vec<Phone> = vec![];
|
||||
OperationResult::Retry(test)
|
||||
}
|
||||
},
|
||||
)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn initial_load() -> bool {
|
||||
match AdbCommand::new().devices() {
|
||||
Ok(_devices) => true,
|
||||
Err(_err) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current state of a package on a device
|
||||
#[must_use]
|
||||
pub fn get_package_state(
|
||||
device_serial: &str,
|
||||
package_name: &str,
|
||||
user_id: Option<u16>,
|
||||
) -> Option<PackageState> {
|
||||
use crate::adb::{ACommand as AdbCommand, PmListPacksFlag};
|
||||
|
||||
// Check if package is enabled
|
||||
if let Ok(enabled_packages) = AdbCommand::new()
|
||||
.shell(device_serial)
|
||||
.pm()
|
||||
.list_packages_sys(Some(PmListPacksFlag::OnlyEnabled), user_id)
|
||||
&& enabled_packages.contains(&package_name.to_string())
|
||||
{
|
||||
return Some(PackageState::Enabled);
|
||||
}
|
||||
|
||||
// Check if package is disabled
|
||||
if let Ok(disabled_packages) = AdbCommand::new()
|
||||
.shell(device_serial)
|
||||
.pm()
|
||||
.list_packages_sys(Some(PmListPacksFlag::OnlyDisabled), user_id)
|
||||
&& disabled_packages.contains(&package_name.to_string())
|
||||
{
|
||||
return Some(PackageState::Disabled);
|
||||
}
|
||||
|
||||
// Check if package exists at all (including uninstalled)
|
||||
if let Ok(all_packages) = AdbCommand::new()
|
||||
.shell(device_serial)
|
||||
.pm()
|
||||
.list_packages_sys(Some(PmListPacksFlag::IncludeUninstalled), user_id)
|
||||
&& all_packages.contains(&package_name.to_string())
|
||||
{
|
||||
return Some(PackageState::Uninstalled);
|
||||
}
|
||||
|
||||
// Package not found at all - it doesn't exist on this device/user
|
||||
None
|
||||
}
|
||||
|
||||
/// Verify the current state of a package on a device.
|
||||
/// Returns the package state, or `None` if the package doesn't exist.
|
||||
#[must_use]
|
||||
pub fn verify_package_state(
|
||||
package_name: &str,
|
||||
device_serial: &str,
|
||||
user_id: Option<u16>,
|
||||
) -> Option<PackageState> {
|
||||
get_package_state(device_serial, package_name, user_id)
|
||||
}
|
||||
|
||||
/// Check if a package exists on any other users besides the target user.
|
||||
/// This helps detect OEM-specific cross-user restoration behavior.
|
||||
///
|
||||
/// Only includes users where the package exists in a non-uninstalled state
|
||||
/// (i.e., Enabled or Disabled).
|
||||
#[must_use]
|
||||
pub fn check_cross_user_package_existence(
|
||||
package_name: &str,
|
||||
device_serial: &str,
|
||||
target_user_id: u16,
|
||||
phone: &Phone,
|
||||
) -> Vec<(u16, PackageState)> {
|
||||
let mut other_user_states = Vec::new();
|
||||
|
||||
for user in &phone.user_list {
|
||||
if user.id != target_user_id
|
||||
&& !user.protected
|
||||
&& let Some(state) = verify_package_state(package_name, device_serial, Some(user.id))
|
||||
&& state != PackageState::Uninstalled
|
||||
{
|
||||
other_user_states.push((user.id, state));
|
||||
}
|
||||
}
|
||||
|
||||
other_user_states
|
||||
}
|
||||
|
||||
/// Creates a `CorePackage` with the specified state, preserving other fields from the original.
|
||||
fn package_with_state(package: &CorePackage, state: PackageState) -> CorePackage {
|
||||
CorePackage {
|
||||
state,
|
||||
..package.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes the first available command and returns the result.
|
||||
fn execute_first_command(commands: &[String], phone: &Phone) -> Result<(), String> {
|
||||
if commands.is_empty() {
|
||||
return Err("No command available".to_string());
|
||||
}
|
||||
AdbCommand::new()
|
||||
.shell(&phone.adb_id)
|
||||
.raw(&commands[0])
|
||||
.map(|_| ())
|
||||
.map_err(|err| err.clone())
|
||||
}
|
||||
|
||||
/// Attempt fallback action when package state verification fails
|
||||
pub fn attempt_fallback(
|
||||
package: &CorePackage,
|
||||
wanted_state: PackageState,
|
||||
actual_state: PackageState,
|
||||
user: User,
|
||||
phone: &Phone,
|
||||
) -> Result<String, String> {
|
||||
match (wanted_state, actual_state) {
|
||||
// Case 1: Tried to uninstall but package was reinstalled -> disable it
|
||||
(PackageState::Uninstalled, PackageState::Enabled) => {
|
||||
let pkg = package_with_state(package, PackageState::Enabled);
|
||||
let commands = apply_pkg_state_commands(&pkg, PackageState::Disabled, user, phone);
|
||||
execute_first_command(&commands, phone)
|
||||
.map(|()| "disabled package instead of uninstalling".to_string())
|
||||
.map_err(|e| format!("Failed to disable package: {e}"))
|
||||
}
|
||||
|
||||
// Case 2: Tried to disable but package re-enabled itself -> try uninstall
|
||||
(PackageState::Disabled, PackageState::Enabled) => {
|
||||
let pkg = package_with_state(package, PackageState::Enabled);
|
||||
let commands = apply_pkg_state_commands(&pkg, PackageState::Uninstalled, user, phone);
|
||||
execute_first_command(&commands, phone)
|
||||
.map_err(|e| format!("Failed to uninstall: {e}"))?;
|
||||
|
||||
// Verify the package was actually uninstalled
|
||||
match verify_package_state(&package.name, &phone.adb_id, Some(user.id)) {
|
||||
Some(PackageState::Uninstalled) | None => {
|
||||
Ok("uninstalled package instead of disabling".to_string())
|
||||
}
|
||||
_ => Err("Package still exists after uninstall attempt".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
// Case 3: Tried to enable but package was disabled -> try uninstall then reinstall
|
||||
(PackageState::Enabled, PackageState::Disabled) => {
|
||||
let pkg = package_with_state(package, PackageState::Disabled);
|
||||
let uninstall_cmds =
|
||||
apply_pkg_state_commands(&pkg, PackageState::Uninstalled, user, phone);
|
||||
execute_first_command(&uninstall_cmds, phone)
|
||||
.map_err(|e| format!("Failed to uninstall for reinstall: {e}"))?;
|
||||
|
||||
// Now try to reinstall/enable
|
||||
let pkg_uninstalled = package_with_state(package, PackageState::Uninstalled);
|
||||
let enable_cmds =
|
||||
apply_pkg_state_commands(&pkg_uninstalled, PackageState::Enabled, user, phone);
|
||||
|
||||
if enable_cmds.is_empty() {
|
||||
return Ok("uninstalled package but couldn't reinstall".to_string());
|
||||
}
|
||||
|
||||
execute_first_command(&enable_cmds, phone)
|
||||
.map(|()| "uninstalled and reinstalled package to enable it".to_string())
|
||||
.map_err(|e| format!("Failed to reinstall package: {e}"))
|
||||
}
|
||||
|
||||
// Other cases - no fallback available
|
||||
_ => Err(format!(
|
||||
"No fallback available for wanted state {wanted_state:?} and actual state {actual_state:?}"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,26 @@
|
|||
use crate::core::utils::{format_diff_time_from_now, last_modified_date};
|
||||
use crate::CACHE_DIR;
|
||||
use retry::{delay::Fixed, retry, OperationResult};
|
||||
use crate::utils::{format_diff_time_from_now, last_modified_date};
|
||||
use log::warn;
|
||||
use retry::{OperationResult, delay::Fixed, retry};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json;
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub const LIST_FNAME: &str = "uad_lists.json";
|
||||
|
||||
#[allow(
|
||||
clippy::large_include_file,
|
||||
reason = "https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/discussions/608"
|
||||
)]
|
||||
// not `const`, because it's too big
|
||||
pub static DATA: &str = include_str!("../../../resources/assets/uad_lists.json");
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, PartialEq, Hash, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Package {
|
||||
id: String,
|
||||
pub list: UadList,
|
||||
pub description: String,
|
||||
dependencies: Vec<String>,
|
||||
|
|
@ -19,7 +29,7 @@ pub struct Package {
|
|||
pub removal: Removal,
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[derive(Default, Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum UadList {
|
||||
#[default]
|
||||
All,
|
||||
|
|
@ -42,7 +52,7 @@ pub enum UadListState {
|
|||
|
||||
impl std::fmt::Display for UadListState {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let date = last_modified_date(CACHE_DIR.join("uad_lists.json"));
|
||||
let date = last_modified_date(CACHE_DIR.join(LIST_FNAME));
|
||||
let s = match self {
|
||||
Self::Downloading => "Checking updates...".to_string(),
|
||||
Self::Done => format!("Done (last was {})", format_diff_time_from_now(date)),
|
||||
|
|
@ -63,24 +73,31 @@ impl UadList {
|
|||
Self::Pending,
|
||||
Self::Unlisted,
|
||||
];
|
||||
|
||||
#[must_use]
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::All => "All lists",
|
||||
Self::Aosp => "aosp",
|
||||
Self::Carrier => "carrier",
|
||||
Self::Google => "google",
|
||||
Self::Misc => "misc",
|
||||
Self::Oem => "oem",
|
||||
Self::Pending => "pending",
|
||||
Self::Unlisted => "unlisted",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for UadList {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::All => "All lists",
|
||||
Self::Aosp => "aosp",
|
||||
Self::Carrier => "carrier",
|
||||
Self::Google => "google",
|
||||
Self::Misc => "misc",
|
||||
Self::Oem => "oem",
|
||||
Self::Pending => "pending",
|
||||
Self::Unlisted => "unlisted",
|
||||
}
|
||||
)
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UadList> for Cow<'_, str> {
|
||||
fn from(list: UadList) -> Self {
|
||||
Cow::Borrowed(list.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -103,7 +120,7 @@ impl std::fmt::Display for PackageState {
|
|||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::All => "All packages",
|
||||
Self::All => "All states",
|
||||
Self::Enabled => "Enabled",
|
||||
Self::Uninstalled => "Uninstalled",
|
||||
Self::Disabled => "Disabled",
|
||||
|
|
@ -133,15 +150,15 @@ impl Opposite for PackageState {
|
|||
}
|
||||
|
||||
// Bad names. To be changed!
|
||||
#[derive(Default, Debug, Deserialize, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[derive(Default, Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum Removal {
|
||||
All,
|
||||
#[default]
|
||||
Recommended,
|
||||
Advanced,
|
||||
Expert,
|
||||
Unsafe,
|
||||
Unlisted,
|
||||
All,
|
||||
}
|
||||
|
||||
impl Removal {
|
||||
|
|
@ -153,79 +170,93 @@ impl Removal {
|
|||
Self::Unsafe,
|
||||
Self::Unlisted,
|
||||
];
|
||||
pub const CATEGORIES: [Self; 5] = [
|
||||
Self::Recommended,
|
||||
Self::Advanced,
|
||||
Self::Expert,
|
||||
Self::Unsafe,
|
||||
Self::Unlisted,
|
||||
];
|
||||
|
||||
#[must_use]
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::All => "All removals",
|
||||
Self::Recommended => "Recommended",
|
||||
Self::Advanced => "Advanced",
|
||||
Self::Expert => "Expert",
|
||||
Self::Unsafe => "Unsafe",
|
||||
Self::Unlisted => "Unlisted",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Removal {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::All => "All",
|
||||
Self::Recommended => "Recommended",
|
||||
Self::Advanced => "Advanced",
|
||||
Self::Expert => "Expert",
|
||||
Self::Unsafe => "Unsafe",
|
||||
Self::Unlisted => "Unlisted",
|
||||
}
|
||||
)
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
type PackageHashMap = HashMap<String, Package>;
|
||||
pub fn load_debloat_lists(remote: bool) -> (Result<PackageHashMap, PackageHashMap>, bool) {
|
||||
let cached_uad_lists: PathBuf = CACHE_DIR.join("uad_lists.json");
|
||||
impl From<Removal> for Cow<'_, str> {
|
||||
fn from(list: Removal) -> Self {
|
||||
Cow::Borrowed(list.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
pub type PackageHashMap = HashMap<String, Package>;
|
||||
pub fn load_debloat_lists(remote: bool) -> Result<PackageHashMap, PackageHashMap> {
|
||||
let cached_uad_lists: PathBuf = CACHE_DIR.join(LIST_FNAME);
|
||||
let mut error = false;
|
||||
let list: Vec<Package> = if remote {
|
||||
let list: PackageHashMap = if remote {
|
||||
retry(Fixed::from_millis(1000).take(60), || {
|
||||
match ureq::get(
|
||||
"https://raw.githubusercontent.com/Universal-Debloater-Alliance/universal-android-debloater/\
|
||||
main/resources/assets/uad_lists.json",
|
||||
)
|
||||
match ureq::get(format!(
|
||||
"https://raw.githubusercontent.com\
|
||||
/Universal-Debloater-Alliance\
|
||||
/universal-android-debloater\
|
||||
/main\
|
||||
/resources\
|
||||
/assets\
|
||||
/{LIST_FNAME}"
|
||||
))
|
||||
.call()
|
||||
{
|
||||
Ok(data) => {
|
||||
let text = data.into_string().expect("response should be Ok type");
|
||||
Ok(mut data) => {
|
||||
// https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/discussions/608
|
||||
let text = data
|
||||
.body_mut()
|
||||
.with_config()
|
||||
.limit(1 << (3 + 10 + 10))
|
||||
.read_to_string()
|
||||
.expect("remote list is bigger than 8MiB");
|
||||
fs::write(cached_uad_lists.clone(), &text).expect("Unable to write file");
|
||||
let list = serde_json::from_str(&text).expect("Unable to parse");
|
||||
let list: PackageHashMap =
|
||||
serde_json::from_str(&text).expect("Unable to parse");
|
||||
OperationResult::Ok(list)
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Could not load remote debloat list: {}", e);
|
||||
warn!("Could not load remote debloat list: {e}");
|
||||
error = true;
|
||||
OperationResult::Retry(Vec::<Package>::new())
|
||||
OperationResult::Retry(PackageHashMap::new())
|
||||
}
|
||||
}
|
||||
})
|
||||
.map_or_else(|_| get_local_lists(), |list| list)
|
||||
.unwrap_or_else(|_| get_local_lists())
|
||||
} else {
|
||||
warn!("Could not load remote debloat list");
|
||||
get_local_lists()
|
||||
};
|
||||
|
||||
// TODO: Do it without intermediary Vec?
|
||||
let mut package_lists = HashMap::new();
|
||||
for p in list {
|
||||
let name = p.id.clone();
|
||||
package_lists.insert(name, p);
|
||||
}
|
||||
if error {
|
||||
(Err(package_lists), remote)
|
||||
} else {
|
||||
(Ok(package_lists), remote)
|
||||
}
|
||||
(if error { Err } else { Ok })(list)
|
||||
}
|
||||
|
||||
fn get_local_lists() -> Vec<Package> {
|
||||
const DATA: &str = include_str!("../../resources/assets/uad_lists.json");
|
||||
let cached_uad_lists = CACHE_DIR.join("uad_lists.json");
|
||||
|
||||
if Path::new(&cached_uad_lists).exists() {
|
||||
let data = fs::read_to_string(cached_uad_lists).unwrap();
|
||||
serde_json::from_str(&data).expect("Unable to parse")
|
||||
} else {
|
||||
serde_json::from_str(DATA).expect("Unable to parse")
|
||||
}
|
||||
fn get_local_lists() -> PackageHashMap {
|
||||
let cached_uad_lists = CACHE_DIR.join(LIST_FNAME);
|
||||
serde_json::from_str(
|
||||
fs::read_to_string(cached_uad_lists)
|
||||
.as_deref()
|
||||
.unwrap_or(DATA),
|
||||
)
|
||||
.expect("Unable to parse")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -233,7 +264,6 @@ mod tests {
|
|||
use super::*;
|
||||
#[test]
|
||||
fn test_parse_json() {
|
||||
const DATA: &str = include_str!("../../resources/assets/uad_lists.json");
|
||||
let _: Vec<Package> = serde_json::from_str(DATA).expect("Unable to parse");
|
||||
let _: PackageHashMap = serde_json::from_str(DATA).expect("Unable to parse");
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,9 @@ use serde::Deserialize;
|
|||
|
||||
#[cfg(feature = "self-update")]
|
||||
use {
|
||||
retry::{delay::Fibonacci, retry, OperationResult},
|
||||
crate::utils::NAME,
|
||||
log::{debug, error},
|
||||
retry::{OperationResult, delay::Fibonacci, retry},
|
||||
std::fs,
|
||||
std::io,
|
||||
std::io::copy,
|
||||
|
|
@ -52,15 +54,15 @@ impl std::fmt::Display for SelfUpdateStatus {
|
|||
|
||||
/// Download a file from the internet
|
||||
#[cfg(feature = "self-update")]
|
||||
pub async fn download_file<T: ToString + Send>(url: T, dest_file: PathBuf) -> Result<(), String> {
|
||||
let url = url.to_string();
|
||||
debug!("downloading file from {}", &url);
|
||||
#[allow(clippy::unused_async, reason = "`.call` is equivalent to `.await`")]
|
||||
pub async fn download_file(url: &str, dest_file: PathBuf) -> Result<(), String> {
|
||||
debug!("downloading file from {url}");
|
||||
|
||||
match ureq::get(&url).call() {
|
||||
Ok(res) => {
|
||||
match ureq::get(url).call() {
|
||||
Ok(mut res) => {
|
||||
let mut file = fs::File::create(dest_file).map_err(|e| e.to_string())?;
|
||||
|
||||
if let Err(e) = copy(&mut res.into_reader(), &mut file) {
|
||||
if let Err(e) = copy(&mut res.body_mut().as_reader(), &mut file) {
|
||||
return Err(e.to_string());
|
||||
}
|
||||
}
|
||||
|
|
@ -74,7 +76,7 @@ pub async fn download_file<T: ToString + Send>(url: T, dest_file: PathBuf) -> Re
|
|||
/// then returns both the original file name (new version) and temp path (old version)
|
||||
#[cfg(feature = "self-update")]
|
||||
pub async fn download_update_to_temp_file(
|
||||
bin_name: String,
|
||||
bin_name: &str,
|
||||
release: Release,
|
||||
) -> Result<(PathBuf, PathBuf), ()> {
|
||||
let current_bin_path = std::env::current_exe().map_err(|_| ())?;
|
||||
|
|
@ -107,13 +109,13 @@ pub async fn download_update_to_temp_file(
|
|||
|
||||
let archive_path = current_bin_path.parent().ok_or(())?.join(&asset_name);
|
||||
|
||||
if let Err(e) = download_file(asset.download_url, archive_path.clone()).await {
|
||||
error!("Couldn't download UAD update: {}", e);
|
||||
if let Err(e) = download_file(&asset.download_url, archive_path.clone()).await {
|
||||
error!("Couldn't download {NAME} update: {e}");
|
||||
return Err(());
|
||||
}
|
||||
|
||||
if extract_binary_from_tar(&archive_path, &download_path).is_err() {
|
||||
error!("Couldn't extract UAD release tarball");
|
||||
error!("Couldn't extract {NAME} release tarball");
|
||||
return Err(());
|
||||
}
|
||||
|
||||
|
|
@ -130,8 +132,8 @@ pub async fn download_update_to_temp_file(
|
|||
.cloned()
|
||||
.ok_or(())?;
|
||||
|
||||
if let Err(e) = download_file(asset.download_url, download_path.clone()).await {
|
||||
error!("Couldn't download UAD update: {}", e);
|
||||
if let Err(e) = download_file(&asset.download_url, download_path.clone()).await {
|
||||
error!("Couldn't download {NAME} update: {}", e);
|
||||
return Err(());
|
||||
}
|
||||
}
|
||||
|
|
@ -144,17 +146,17 @@ pub async fn download_update_to_temp_file(
|
|||
let mut permissions = fs::metadata(&download_path).map_err(|_| ())?.permissions();
|
||||
permissions.set_mode(0o755);
|
||||
if let Err(e) = fs::set_permissions(&download_path, permissions) {
|
||||
error!("[SelfUpdate] Couldn't set permission to temp file: {}", e);
|
||||
error!("[SelfUpdate] Couldn't set permission to temp file: {e}");
|
||||
return Err(());
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = rename(¤t_bin_path, &tmp_path) {
|
||||
error!("[SelfUpdate] Couldn't rename binary path: {}", e);
|
||||
error!("[SelfUpdate] Couldn't rename from current to temporary binary path: {e}");
|
||||
return Err(());
|
||||
}
|
||||
if let Err(e) = rename(&download_path, ¤t_bin_path) {
|
||||
error!("[SelfUpdate] Couldn't rename binary path: {}", e);
|
||||
error!("[SelfUpdate] Couldn't rename from downloaded to current binary path: {e}");
|
||||
return Err(());
|
||||
}
|
||||
|
||||
|
|
@ -166,37 +168,34 @@ pub fn get_latest_release() -> Result<Option<Release>, ()> {
|
|||
Ok(None)
|
||||
}
|
||||
|
||||
// UAD only has pre-releases so we can't use
|
||||
// UAD-ng only has pre-releases so we can't use
|
||||
// https://api.github.com/repos/Universal-Debloater-Alliance/universal-android-debloater/releases/latest
|
||||
// to only get the latest release
|
||||
#[cfg(feature = "self-update")]
|
||||
pub fn get_latest_release() -> Result<Option<Release>, ()> {
|
||||
debug!("Checking for UAD update");
|
||||
debug!("Checking for {NAME} update");
|
||||
|
||||
match ureq::get("https://api.github.com/repos/Universal-Debloater-Alliance/universal-android-debloater/releases")
|
||||
.call()
|
||||
{
|
||||
Ok(res) => {
|
||||
let release: Release = serde_json::from_value(
|
||||
res.into_json::<serde_json::Value>()
|
||||
.map_err(|_| ())?
|
||||
.get(0)
|
||||
.ok_or(())?
|
||||
.clone(),
|
||||
)
|
||||
.map_err(|_| ())?;
|
||||
if release.tag_name.as_str() != "dev-build"
|
||||
&& release.tag_name.as_str() > env!("CARGO_PKG_VERSION")
|
||||
{
|
||||
Ok(Some(release))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
debug!("Failed to check UAD update");
|
||||
Err(())
|
||||
if let Ok(mut res) = ureq::get("https://api.github.com/repos/Universal-Debloater-Alliance/universal-android-debloater/releases/latest")
|
||||
.call() {
|
||||
let release: Release = serde_json::from_value(
|
||||
res.body_mut().read_json::<serde_json::Value>()
|
||||
.map_err(|_| ())?
|
||||
.clone(),
|
||||
)
|
||||
.map_err(|_| ())?;
|
||||
|
||||
let release_version = release.tag_name.strip_prefix('v').unwrap_or(&release.tag_name);
|
||||
|
||||
if release_version != "dev-build"
|
||||
&& release_version > env!("CARGO_PKG_VERSION")
|
||||
{
|
||||
Ok(Some(release))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
} else {
|
||||
debug!("Failed to check {NAME} update");
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -224,24 +223,29 @@ pub fn extract_binary_from_tar(archive_path: &Path, temp_file: &Path) -> io::Res
|
|||
}
|
||||
|
||||
/// Hardcoded binary names for each compilation target
|
||||
/// that gets published to the Github Release
|
||||
/// that gets published to the GitHub Release
|
||||
#[cfg(feature = "self-update")]
|
||||
pub const fn bin_name() -> &'static str {
|
||||
pub const BIN_NAME: &str = {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
"uad_gui.exe"
|
||||
"uad-ng-windows.exe"
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[cfg(all(target_os = "macos", any(target_arch = "x86_64", target_arch = "x86")))]
|
||||
{
|
||||
"uad_gui-macos"
|
||||
"uad-ng-macos-intel"
|
||||
}
|
||||
|
||||
#[cfg(all(target_os = "macos", any(target_arch = "arm", target_arch = "aarch64")))]
|
||||
{
|
||||
"uad-ng-macos"
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
|
||||
{
|
||||
"uad_gui-linux"
|
||||
"uad-ng-linux"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// Rename a file or directory to a new name, retrying if the operation fails because of permissions
|
||||
///
|
||||
|
|
@ -261,7 +265,7 @@ where
|
|||
|
||||
retry(Fibonacci::from_millis(1).take(21), || {
|
||||
match fs::rename(from, to) {
|
||||
Ok(_) => OperationResult::Ok(()),
|
||||
Ok(()) => OperationResult::Ok(()),
|
||||
Err(e) => match e.kind() {
|
||||
io::ErrorKind::PermissionDenied => OperationResult::Retry(e),
|
||||
_ => OperationResult::Err(e),
|
||||
|
|
@ -288,7 +292,7 @@ where
|
|||
retry(
|
||||
Fibonacci::from_millis(1).take(21),
|
||||
|| match fs::remove_file(path) {
|
||||
Ok(_) => OperationResult::Ok(()),
|
||||
Ok(()) => OperationResult::Ok(()),
|
||||
Err(e) => match e.kind() {
|
||||
io::ErrorKind::PermissionDenied => OperationResult::Retry(e),
|
||||
_ => OperationResult::Err(e),
|
||||
299
crates/uad-core/src/utils.rs
Normal file
299
crates/uad-core/src/utils.rs
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
#![warn(clippy::unwrap_used)]
|
||||
|
||||
use crate::{
|
||||
adb::{ACommand as AdbCommand, PmListPacksFlag},
|
||||
sync::{CorePackage, User},
|
||||
uad_lists::{PackageHashMap, PackageState, Removal, UadList},
|
||||
};
|
||||
use chrono::{DateTime, offset::Utc};
|
||||
use csv::Writer;
|
||||
use log::error;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fmt, fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
/// Canonical shortened name of the application
|
||||
pub const NAME: &str = "UAD-ng";
|
||||
/// Full name of the application
|
||||
pub const FULL_NAME: &str = "Universal Android Debloater Next Generation";
|
||||
pub const EXPORT_FILE_NAME: &str = "selection_export.txt";
|
||||
|
||||
/// Returns `true` if `c` matches the regex `\w`
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub const fn is_w(c: u8) -> bool {
|
||||
// https://github.com/rust-lang/rust/issues/93279
|
||||
// https://github.com/rust-lang/rust/issues/83623
|
||||
(c == b'_') | c.is_ascii_alphanumeric()
|
||||
}
|
||||
|
||||
/// Returns `true` if `s` matches the regex `^\w+$`
|
||||
#[must_use]
|
||||
pub const fn is_all_w_c(s: &[u8]) -> bool {
|
||||
let mut i = 0;
|
||||
while i < s.len() {
|
||||
if !is_w(s[i]) {
|
||||
return false;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
// Takes a time-stamp parameter,
|
||||
// for purity and testability.
|
||||
//
|
||||
// The TZ is generic, because testing requires UTC,
|
||||
// while users get the local-aware version.
|
||||
#[expect(
|
||||
clippy::needless_pass_by_value,
|
||||
reason = "Timestamps should be fresh, no need to borrow"
|
||||
)]
|
||||
#[must_use]
|
||||
pub fn generate_backup_name<T>(t: DateTime<T>) -> String
|
||||
where
|
||||
T: chrono::TimeZone,
|
||||
T::Offset: std::fmt::Display,
|
||||
{
|
||||
t.format("uninstalled_packages_%Y%m%d.csv").to_string()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Error {
|
||||
DialogClosed,
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn fetch_packages(
|
||||
uad_lists: &PackageHashMap,
|
||||
device_serial: &str,
|
||||
user_id: Option<u16>,
|
||||
) -> Vec<CorePackage> {
|
||||
let all_sys_packs = AdbCommand::new()
|
||||
.shell(device_serial)
|
||||
.pm()
|
||||
.list_packages_sys(Some(PmListPacksFlag::IncludeUninstalled), user_id)
|
||||
.unwrap_or_default();
|
||||
let enabled_sys_packs: HashSet<String> = AdbCommand::new()
|
||||
.shell(device_serial)
|
||||
.pm()
|
||||
.list_packages_sys(Some(PmListPacksFlag::OnlyEnabled), user_id)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.collect();
|
||||
let disabled_sys_packs: HashSet<String> = AdbCommand::new()
|
||||
.shell(device_serial)
|
||||
.pm()
|
||||
.list_packages_sys(Some(PmListPacksFlag::OnlyDisabled), user_id)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
let mut description;
|
||||
let mut removal;
|
||||
let mut state;
|
||||
let mut list;
|
||||
let mut user_package: Vec<CorePackage> = Vec::new();
|
||||
|
||||
for pack_name in all_sys_packs {
|
||||
let p_name = &pack_name;
|
||||
description = String::from("[No description]: CONTRIBUTION WELCOMED");
|
||||
removal = Removal::Unlisted;
|
||||
state = PackageState::Uninstalled;
|
||||
list = UadList::Unlisted;
|
||||
|
||||
if let Some(package) = uad_lists.get(p_name) {
|
||||
if !package.description.is_empty() {
|
||||
description = package.description.clone();
|
||||
}
|
||||
removal = package.removal;
|
||||
list = package.list;
|
||||
}
|
||||
|
||||
if enabled_sys_packs.contains(p_name) {
|
||||
state = PackageState::Enabled;
|
||||
} else if disabled_sys_packs.contains(p_name) {
|
||||
state = PackageState::Disabled;
|
||||
}
|
||||
|
||||
let package = CorePackage {
|
||||
name: p_name.clone(),
|
||||
description,
|
||||
removal,
|
||||
state,
|
||||
list,
|
||||
};
|
||||
user_package.push(package);
|
||||
}
|
||||
user_package.sort_by_key(|package| package.name.to_lowercase());
|
||||
user_package
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn setup_uad_dir(dir: &Path) -> PathBuf {
|
||||
let dir = dir.join("uad");
|
||||
if let Err(e) = fs::create_dir_all(&dir) {
|
||||
error!("Can't create directory: {}", dir.display());
|
||||
panic!("{e}");
|
||||
}
|
||||
dir
|
||||
}
|
||||
|
||||
/// Open a directory or file with the system's default file manager.
|
||||
pub fn open_url(dir: PathBuf) {
|
||||
const OPENER: &str = match std::env::consts::OS.as_bytes() {
|
||||
b"windows" => "explorer",
|
||||
b"macos" => "open",
|
||||
// "linux"
|
||||
_ => "xdg-open",
|
||||
};
|
||||
match std::process::Command::new(OPENER).arg(dir).output() {
|
||||
Ok(o) => {
|
||||
if !o.status.success() {
|
||||
// Use lossy conversion for stderr - some systems (like Windows)
|
||||
// may output non-UTF8 characters in error messages
|
||||
let stderr = String::from_utf8_lossy(&o.stderr);
|
||||
let stderr_trimmed = stderr.trim_end();
|
||||
if stderr_trimmed.is_empty() {
|
||||
error!("Can't open URL: command failed with no error message");
|
||||
} else {
|
||||
error!("Can't open the following URL: {stderr_trimmed}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => error!("Failed to run command to open the file explorer: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
#[must_use]
|
||||
pub fn last_modified_date(file: PathBuf) -> DateTime<Utc> {
|
||||
fs::metadata(file).map_or_else(|_| Utc::now(), |metadata| match metadata.modified() {
|
||||
Ok(time) => time.into(),
|
||||
Err(_) => Utc::now(),
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn format_diff_time_from_now(date: DateTime<Utc>) -> String {
|
||||
let now: DateTime<Utc> = Utc::now();
|
||||
let last_update = now - date;
|
||||
if last_update.num_days() == 0 {
|
||||
if last_update.num_hours() == 0 {
|
||||
last_update.num_minutes().to_string() + " min(s) ago"
|
||||
} else {
|
||||
last_update.num_hours().to_string() + " hour(s) ago"
|
||||
}
|
||||
} else {
|
||||
last_update.num_days().to_string() + " day(s) ago"
|
||||
}
|
||||
}
|
||||
|
||||
/// Export selected package names.
|
||||
/// File will be saved in same directory where UAD-ng is located.
|
||||
pub fn export_selection(package_names: &[String]) -> Result<bool, String> {
|
||||
let selected = package_names.join("\n");
|
||||
|
||||
match fs::write(EXPORT_FILE_NAME, selected) {
|
||||
Ok(()) => Ok(true),
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct DisplayablePath {
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
impl fmt::Display for DisplayablePath {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let stem = self.path.file_stem().map_or_else(
|
||||
|| {
|
||||
error!("[PATH STEM]: No file stem found");
|
||||
"[File steam not found]".to_string()
|
||||
},
|
||||
|p| {
|
||||
if let Ok(stem) = p.to_os_string().into_string() {
|
||||
stem
|
||||
} else {
|
||||
error!("[PATH ENCODING]: {}", self.path.display());
|
||||
"[PATH ENCODING ERROR]".to_string()
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
write!(f, "{stem}")
|
||||
}
|
||||
}
|
||||
|
||||
/// Export uninstalled packages in a csv file.
|
||||
/// Exported information will contain package name and description.
|
||||
pub fn export_packages(user: User, phone_packages: &[Vec<CorePackage>]) -> Result<bool, String> {
|
||||
let backup_file = generate_backup_name(chrono::Local::now());
|
||||
|
||||
let file = fs::File::create(backup_file).map_err(|err| err.to_string())?;
|
||||
let mut wtr = Writer::from_writer(file);
|
||||
|
||||
wtr.write_record(["Package Name", "Description"])
|
||||
.map_err(|err| err.to_string())?;
|
||||
|
||||
let uninstalled_packages: Vec<&CorePackage> = phone_packages[user.index]
|
||||
.iter()
|
||||
.filter(|p| p.state == PackageState::Uninstalled)
|
||||
.collect();
|
||||
|
||||
for package in uninstalled_packages {
|
||||
wtr.write_record([&package.name, &package.description.replace('\n', " ")])
|
||||
.map_err(|err| err.to_string())?;
|
||||
}
|
||||
|
||||
wtr.flush().map_err(|err| err.to_string())?;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Truncate description to fit within max length, taking only the first line
|
||||
#[must_use]
|
||||
pub fn truncate_description(desc: &str, max_len: usize) -> String {
|
||||
let first_line = desc.split('\n').next().unwrap_or("").trim_end();
|
||||
|
||||
if first_line.len() <= max_len {
|
||||
first_line.to_string()
|
||||
} else {
|
||||
format!("{}...", &first_line[..max_len.saturating_sub(3)])
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if package matches search term (checks name and description)
|
||||
#[must_use]
|
||||
pub fn matches_search(pkg_name: &str, search_term: &str, pkg_description: Option<&str>) -> bool {
|
||||
let search_lower = search_term.to_lowercase();
|
||||
|
||||
if pkg_name.to_lowercase().contains(&search_lower) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some(description) = pkg_description {
|
||||
if description.to_lowercase().contains(&search_lower) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use chrono::TimeZone;
|
||||
|
||||
#[test]
|
||||
fn backup_name() {
|
||||
assert_eq!(
|
||||
generate_backup_name(chrono::Utc.timestamp_millis_opt(0).unwrap()),
|
||||
"uninstalled_packages_19700101.csv".to_string()
|
||||
);
|
||||
}
|
||||
}
|
||||
62
crates/uad-gui/Cargo.toml
Normal file
62
crates/uad-gui/Cargo.toml
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
[package]
|
||||
name = "uad-gui"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
readme.workspace = true
|
||||
keywords.workspace = true
|
||||
categories = ["gui"]
|
||||
edition.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "uad-ng"
|
||||
path = "src/main.rs"
|
||||
|
||||
[features]
|
||||
default = ["wgpu", "self-update", "img"]
|
||||
wgpu = [] # Iced/wgpu is default
|
||||
self-update = ["flate2", "tar"]
|
||||
no-self-update = []
|
||||
img = ["image", "iced/image"]
|
||||
|
||||
[dependencies]
|
||||
uad-core = { path = "../uad-core", features = ["self-update"] }
|
||||
iced.workspace = true
|
||||
image = { workspace = true, optional = true }
|
||||
rfd.workspace = true
|
||||
dark-light.workspace = true
|
||||
fern.workspace = true
|
||||
chrono.workspace = true
|
||||
dirs.workspace = true
|
||||
log.workspace = true
|
||||
flate2 = { workspace = true, optional = true }
|
||||
tar = { workspace = true, optional = true }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
win32console.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
embed-resource = "3"
|
||||
|
||||
[lints.rust]
|
||||
deprecated_safe = "warn"
|
||||
|
||||
[lints.clippy]
|
||||
undocumented_unsafe_blocks = "forbid"
|
||||
exit = "deny"
|
||||
panic_in_result_fn = "warn"
|
||||
infinite_loop = "warn"
|
||||
mem_forget = "warn"
|
||||
implicit_clone = "warn"
|
||||
format_push_string = "warn"
|
||||
large_include_file = "warn"
|
||||
shadow_unrelated = "warn"
|
||||
struct_field_names = "allow" # annoying
|
||||
|
||||
disallowed_types = "deny"
|
||||
disallowed_methods = "deny"
|
||||
|
||||
allow_attributes_without_reason = "warn"
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
475
crates/uad-gui/src/gui.rs
Normal file
475
crates/uad-gui/src/gui.rs
Normal file
|
|
@ -0,0 +1,475 @@
|
|||
#[cfg(feature = "img")]
|
||||
use crate::theme::OS_COLOR_SCHEME;
|
||||
use crate::theme::Theme;
|
||||
use crate::theme::string_to_theme;
|
||||
use crate::views::about::{About as AboutView, Message as AboutMessage};
|
||||
use crate::views::list::{
|
||||
List as AppsView, LoadingState as ListLoadingState, Message as AppsMessage,
|
||||
};
|
||||
use crate::views::settings::{Message as SettingsMessage, Settings as SettingsView};
|
||||
use crate::widgets::navigation_menu::nav_menu;
|
||||
use iced::font;
|
||||
use iced::widget::column;
|
||||
#[cfg(feature = "img")]
|
||||
use iced::window::icon;
|
||||
use iced::{Alignment, Element, Length, Settings, Task, window::Settings as Window};
|
||||
use iced::{Subscription, event, keyboard};
|
||||
#[cfg(feature = "img")]
|
||||
use image::ImageFormat;
|
||||
use log::{debug, error, info};
|
||||
#[cfg(feature = "self-update")]
|
||||
use std::path::PathBuf;
|
||||
use uad_core::adb;
|
||||
use uad_core::sync::{Phone, get_devices_list, initial_load};
|
||||
use uad_core::uad_lists::UadListState;
|
||||
use uad_core::update::{Release, SelfUpdateState, SelfUpdateStatus, get_latest_release};
|
||||
use uad_core::utils::{FULL_NAME, NAME};
|
||||
|
||||
#[cfg(feature = "self-update")]
|
||||
use uad_core::update::{BIN_NAME, download_update_to_temp_file, remove_file};
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
enum View {
|
||||
#[default]
|
||||
List,
|
||||
About,
|
||||
Settings,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct UpdateState {
|
||||
pub self_update: SelfUpdateState,
|
||||
pub uad_list: UadListState,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct UadGui {
|
||||
view: View,
|
||||
apps_view: AppsView,
|
||||
about_view: AboutView,
|
||||
settings_view: SettingsView,
|
||||
devices_list: Vec<Phone>,
|
||||
/// index of `devices_list`
|
||||
selected_device: Option<Phone>,
|
||||
update_state: UpdateState,
|
||||
nb_running_async_adb_commands: u32,
|
||||
adb_satisfied: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Message {
|
||||
/// Navigation Panel
|
||||
AboutPressed,
|
||||
SettingsPressed,
|
||||
AppsPress,
|
||||
DeviceSelected(Phone),
|
||||
AboutAction(AboutMessage),
|
||||
AppsAction(AppsMessage),
|
||||
SettingsAction(SettingsMessage),
|
||||
RefreshButtonPressed,
|
||||
RebootButtonPressed,
|
||||
LoadDevices(Vec<Phone>),
|
||||
#[cfg(feature = "self-update")]
|
||||
_NewReleaseDownloaded(Result<(PathBuf, PathBuf), ()>),
|
||||
GetLatestRelease(Result<Option<Release>, ()>),
|
||||
FontLoaded(Result<(), iced::font::Error>),
|
||||
Nothing,
|
||||
ADBSatisfied(bool),
|
||||
}
|
||||
|
||||
impl UadGui {
|
||||
#[allow(
|
||||
clippy::unused_self,
|
||||
reason = "required by iced's Application trait interface"
|
||||
)]
|
||||
fn title(&self) -> String {
|
||||
self.selected_device.as_ref().map_or_else(
|
||||
|| FULL_NAME.to_string(),
|
||||
|device| format!("{FULL_NAME} - {}", device.model),
|
||||
)
|
||||
}
|
||||
|
||||
fn new() -> (Self, Task<Message>) {
|
||||
(
|
||||
Self::default(),
|
||||
Task::batch([
|
||||
// Used in crate::widgets::navigation_menu::ICONS. Name is `icomoon`.
|
||||
font::load(include_bytes!("../../../resources/assets/icons.ttf").as_slice())
|
||||
.map(Message::FontLoaded),
|
||||
Task::perform(async { initial_load() }, Message::ADBSatisfied),
|
||||
Task::perform(async { get_devices_list() }, Message::LoadDevices),
|
||||
Task::perform(
|
||||
async move { get_latest_release() },
|
||||
Message::GetLatestRelease,
|
||||
),
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(
|
||||
clippy::unused_self,
|
||||
reason = "required by iced's Application trait interface"
|
||||
)]
|
||||
fn subscription(&self) -> Subscription<Message> {
|
||||
if self.selected_device.is_some() {
|
||||
event::listen_with(Self::keyboard_shortcuts_with_device)
|
||||
} else {
|
||||
event::listen_with(Self::keyboard_shortcuts_without_device)
|
||||
}
|
||||
}
|
||||
|
||||
fn keyboard_shortcuts_with_device(
|
||||
event: iced::Event,
|
||||
_status: iced::event::Status,
|
||||
_window: iced::window::Id,
|
||||
) -> Option<Message> {
|
||||
Self::keyboard_shortcuts(event, true)
|
||||
}
|
||||
|
||||
fn keyboard_shortcuts_without_device(
|
||||
event: iced::Event,
|
||||
_status: iced::event::Status,
|
||||
_window: iced::window::Id,
|
||||
) -> Option<Message> {
|
||||
Self::keyboard_shortcuts(event, false)
|
||||
}
|
||||
|
||||
fn keyboard_shortcuts(event: iced::Event, can_control_device: bool) -> Option<Message> {
|
||||
match event {
|
||||
iced::Event::Keyboard(keyboard::Event::KeyPressed {
|
||||
key: keyboard::Key::Character(c),
|
||||
modifiers,
|
||||
..
|
||||
}) if modifiers.control() && modifiers.shift() => match c.as_str() {
|
||||
"r" if can_control_device => Some(Message::RebootButtonPressed),
|
||||
"5" if can_control_device => Some(Message::RefreshButtonPressed),
|
||||
"a" => Some(Message::AppsPress),
|
||||
"i" => Some(Message::AboutPressed),
|
||||
"s" => Some(Message::SettingsPressed),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(
|
||||
clippy::too_many_lines,
|
||||
reason = "Root GUI update orchestrates many flows"
|
||||
)]
|
||||
fn update(&mut self, msg: Message) -> Task<Message> {
|
||||
match msg {
|
||||
Message::LoadDevices(devices_list) => {
|
||||
self.selected_device = match &self.selected_device {
|
||||
Some(s_device) => {
|
||||
// Try to reload last selected phone
|
||||
devices_list
|
||||
.iter()
|
||||
.find(|phone| phone.adb_id == s_device.adb_id)
|
||||
.cloned()
|
||||
}
|
||||
None => devices_list.first().cloned(),
|
||||
};
|
||||
self.devices_list = devices_list;
|
||||
|
||||
#[expect(unused_must_use, reason = "side-effect")]
|
||||
{
|
||||
self.update(Message::SettingsAction(SettingsMessage::LoadDeviceSettings));
|
||||
}
|
||||
|
||||
self.update(Message::AppsAction(AppsMessage::LoadUadList(true)))
|
||||
}
|
||||
Message::AppsPress => {
|
||||
self.view = View::List;
|
||||
Task::none()
|
||||
}
|
||||
Message::AboutPressed => {
|
||||
self.view = View::About;
|
||||
self.update_state.self_update = SelfUpdateState::default();
|
||||
Task::perform(
|
||||
async move { get_latest_release() },
|
||||
Message::GetLatestRelease,
|
||||
)
|
||||
}
|
||||
Message::SettingsPressed => {
|
||||
self.view = View::Settings;
|
||||
Task::none()
|
||||
}
|
||||
Message::RefreshButtonPressed => {
|
||||
self.apps_view = AppsView::default();
|
||||
#[expect(unused_must_use, reason = "side-effect")]
|
||||
{
|
||||
self.update(Message::AppsAction(AppsMessage::ADBSatisfied(
|
||||
self.adb_satisfied,
|
||||
)));
|
||||
}
|
||||
Task::perform(async { get_devices_list() }, Message::LoadDevices)
|
||||
}
|
||||
Message::RebootButtonPressed => {
|
||||
self.apps_view = AppsView::default();
|
||||
let serial = match &self.selected_device {
|
||||
Some(d) => d.adb_id.clone(),
|
||||
_ => String::default(),
|
||||
};
|
||||
self.selected_device = None;
|
||||
self.devices_list = vec![];
|
||||
Task::perform(
|
||||
async { adb::ACommand::new().shell(serial).reboot() },
|
||||
|_| Message::Nothing,
|
||||
)
|
||||
}
|
||||
Message::AppsAction(msg) => self
|
||||
.apps_view
|
||||
.update(
|
||||
&mut self.settings_view,
|
||||
&mut self.selected_device.clone().unwrap_or_default(),
|
||||
&mut self.update_state.uad_list,
|
||||
msg,
|
||||
)
|
||||
.map(Message::AppsAction),
|
||||
Message::SettingsAction(msg) => {
|
||||
match msg {
|
||||
SettingsMessage::RestoringDevice(ref output) => {
|
||||
self.nb_running_async_adb_commands -= 1;
|
||||
self.view = View::List;
|
||||
|
||||
#[expect(unused_must_use, reason = "side-effect")]
|
||||
{
|
||||
self.apps_view.update(
|
||||
&mut self.settings_view,
|
||||
&mut self.selected_device.clone().unwrap_or_default(),
|
||||
&mut self.update_state.uad_list,
|
||||
AppsMessage::RestoringDevice(output.clone()),
|
||||
);
|
||||
}
|
||||
if self.nb_running_async_adb_commands == 0 {
|
||||
return self.update(Message::RefreshButtonPressed);
|
||||
}
|
||||
}
|
||||
SettingsMessage::MultiUserMode(toggled) if toggled => {
|
||||
for user in self.apps_view.phone_packages.clone() {
|
||||
for (i, _) in user.iter().filter(|&pkg| pkg.selected).enumerate() {
|
||||
for u in self
|
||||
.selected_device
|
||||
.as_ref()
|
||||
.expect("Device should be selected")
|
||||
.user_list
|
||||
.iter()
|
||||
.filter(|&u| !u.protected)
|
||||
{
|
||||
self.apps_view.phone_packages[u.index][i].selected = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
self.settings_view
|
||||
.update(
|
||||
&self.selected_device.clone().unwrap_or_default(),
|
||||
&self.apps_view.phone_packages,
|
||||
&mut self.nb_running_async_adb_commands,
|
||||
msg,
|
||||
self.apps_view.selected_user,
|
||||
)
|
||||
.map(Message::SettingsAction)
|
||||
}
|
||||
Message::AboutAction(msg) => {
|
||||
self.about_view.update(msg.clone());
|
||||
|
||||
match msg {
|
||||
AboutMessage::UpdateUadLists => {
|
||||
self.update_state.uad_list = UadListState::Downloading;
|
||||
self.apps_view.loading_state = ListLoadingState::DownloadingList;
|
||||
self.update(Message::AppsAction(AppsMessage::LoadUadList(true)))
|
||||
}
|
||||
AboutMessage::DoSelfUpdate => {
|
||||
#[cfg(feature = "self-update")]
|
||||
if let Some(release) = self.update_state.self_update.latest_release.as_ref()
|
||||
{
|
||||
self.update_state.self_update.status = SelfUpdateStatus::Updating;
|
||||
self.apps_view.loading_state = ListLoadingState::_UpdatingUad;
|
||||
Task::perform(
|
||||
download_update_to_temp_file(BIN_NAME, release.clone()),
|
||||
Message::_NewReleaseDownloaded,
|
||||
)
|
||||
} else {
|
||||
Task::none()
|
||||
}
|
||||
#[cfg(not(feature = "self-update"))]
|
||||
Task::none()
|
||||
}
|
||||
AboutMessage::UrlPressed(_) => Task::none(),
|
||||
}
|
||||
}
|
||||
Message::DeviceSelected(s_device) => {
|
||||
self.selected_device = Some(s_device.clone());
|
||||
self.view = View::List;
|
||||
info!("{:-^65}", "-");
|
||||
info!(
|
||||
"ANDROID_SDK: {} | DEVICE: {}",
|
||||
s_device.android_sdk, s_device.model
|
||||
);
|
||||
info!("{:-^65}", "-");
|
||||
self.apps_view.loading_state = ListLoadingState::FindingPhones;
|
||||
|
||||
#[expect(unused_must_use, reason = "side-effects")]
|
||||
{
|
||||
self.update(Message::SettingsAction(SettingsMessage::LoadDeviceSettings));
|
||||
self.update(Message::AppsAction(AppsMessage::ToggleAllSelected(false)));
|
||||
self.update(Message::AppsAction(AppsMessage::ClearSelectedPackages));
|
||||
}
|
||||
self.update(Message::AppsAction(AppsMessage::LoadPhonePackages((
|
||||
self.apps_view.uad_lists.clone(),
|
||||
UadListState::Done,
|
||||
))))
|
||||
}
|
||||
#[cfg(feature = "self-update")]
|
||||
Message::_NewReleaseDownloaded(res) => {
|
||||
debug!("{NAME} update has been downloaded!");
|
||||
|
||||
if let Ok((relaunch_path, cleanup_path)) = res {
|
||||
let mut args: Vec<_> = std::env::args().skip(1).collect();
|
||||
|
||||
// Remove the `--self-update-temp` arg from args if it exists,
|
||||
// since we need to pass it cleanly. Otherwise new process will
|
||||
// fail during arg parsing.
|
||||
if let Some(idx) = args.iter().position(|a| a == "--self-update-temp") {
|
||||
args.remove(idx);
|
||||
// Remove path passed after this arg
|
||||
args.remove(idx);
|
||||
}
|
||||
|
||||
match std::process::Command::new(relaunch_path)
|
||||
.args(args)
|
||||
.arg("--self-update-temp")
|
||||
.arg(&cleanup_path)
|
||||
.spawn()
|
||||
{
|
||||
Ok(_) => {
|
||||
if let Err(e) = remove_file(cleanup_path) {
|
||||
error!("Could not remove temp update file: {e}");
|
||||
}
|
||||
std::process::exit(0)
|
||||
}
|
||||
Err(error) => {
|
||||
if let Err(e) = remove_file(cleanup_path) {
|
||||
error!("Could not remove temp update file: {e}");
|
||||
}
|
||||
error!("Failed to update {NAME}: {error}");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error!("Failed to update {NAME}!");
|
||||
#[expect(unused_must_use, reason = "side-effect")]
|
||||
{
|
||||
self.update(Message::AppsAction(AppsMessage::UpdateFailed));
|
||||
self.update_state.self_update.status = SelfUpdateStatus::Failed;
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::GetLatestRelease(release) => {
|
||||
match release {
|
||||
Ok(r) => {
|
||||
self.update_state.self_update.status = SelfUpdateStatus::Done;
|
||||
self.update_state.self_update.latest_release = r;
|
||||
}
|
||||
Err(()) => self.update_state.self_update.status = SelfUpdateStatus::Failed,
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::FontLoaded(result) => {
|
||||
if let Err(error) = result {
|
||||
error!("Couldn't load font: {error:?}");
|
||||
}
|
||||
|
||||
Task::none()
|
||||
}
|
||||
Message::ADBSatisfied(result) => {
|
||||
self.adb_satisfied = result;
|
||||
self.update(Message::AppsAction(AppsMessage::ADBSatisfied(
|
||||
self.adb_satisfied,
|
||||
)))
|
||||
}
|
||||
Message::Nothing => Task::none(),
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self) -> Element<'_, Message, Theme> {
|
||||
let navigation_container = nav_menu(
|
||||
&self.devices_list,
|
||||
self.selected_device.clone(),
|
||||
&self.apps_view,
|
||||
&self.update_state.self_update,
|
||||
);
|
||||
|
||||
let selected_device = self.selected_device.clone().unwrap_or_default();
|
||||
let main_container = match self.view {
|
||||
View::List => self
|
||||
.apps_view
|
||||
.view(&self.settings_view, &selected_device)
|
||||
.map(Message::AppsAction),
|
||||
View::About => self
|
||||
.about_view
|
||||
.view(&self.update_state)
|
||||
.map(Message::AboutAction),
|
||||
View::Settings => self
|
||||
.settings_view
|
||||
.view(&selected_device, &self.apps_view)
|
||||
.map(Message::SettingsAction),
|
||||
};
|
||||
|
||||
column![navigation_container, main_container]
|
||||
.width(Length::Fill)
|
||||
.align_x(Alignment::Center)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn theme(&self) -> Theme {
|
||||
string_to_theme(&self.settings_view.general.theme)
|
||||
}
|
||||
}
|
||||
|
||||
impl UadGui {
|
||||
pub fn start() -> iced::Result {
|
||||
#[cfg(feature = "img")]
|
||||
let logo: &[u8] = match *OS_COLOR_SCHEME {
|
||||
// remember to keep `Unspecified` in sync with `src/theme`
|
||||
dark_light::Mode::Dark | dark_light::Mode::Unspecified => {
|
||||
include_bytes!("../../../resources/assets/logo-dark.png")
|
||||
}
|
||||
dark_light::Mode::Light => {
|
||||
include_bytes!("../../../resources/assets/logo-light.png")
|
||||
}
|
||||
};
|
||||
|
||||
iced::application(UadGui::new, UadGui::update, UadGui::view)
|
||||
.title(UadGui::title)
|
||||
.theme(UadGui::theme)
|
||||
.subscription(UadGui::subscription)
|
||||
.settings(Settings {
|
||||
id: Some(String::from(NAME)),
|
||||
default_text_size: iced::Pixels(16.0),
|
||||
// HACK: avoid glyph clipping on some systems. Details:
|
||||
// https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/pull/1337
|
||||
#[cfg(target_os = "linux")]
|
||||
default_font: iced::Font {
|
||||
family: iced::font::Family::Name("Adwaita Sans"),
|
||||
..Default::default()
|
||||
},
|
||||
..Settings::default()
|
||||
})
|
||||
.window(Window {
|
||||
size: iced::Size {
|
||||
width: 950.0,
|
||||
height: 700.0,
|
||||
},
|
||||
resizable: true,
|
||||
decorations: true,
|
||||
#[cfg(feature = "img")]
|
||||
icon: icon::from_file_data(logo, Some(ImageFormat::Png)).ok(),
|
||||
..iced::window::Settings::default()
|
||||
})
|
||||
.run()
|
||||
}
|
||||
}
|
||||
23
crates/uad-gui/src/helpers.rs
Normal file
23
crates/uad-gui/src/helpers.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
use crate::style;
|
||||
use crate::theme::Theme;
|
||||
use iced::Element;
|
||||
use iced::widget::button::Button;
|
||||
|
||||
/// Wrapper function for `iced::widget::button` with padding and style applied
|
||||
pub fn button_primary<'a, Message>(
|
||||
content: impl Into<Element<'a, Message, Theme>>,
|
||||
) -> Button<'a, Message, Theme> {
|
||||
iced::widget::button(content)
|
||||
.padding([5, 10])
|
||||
.style(style::Button::Primary)
|
||||
}
|
||||
|
||||
/// Opens a file picker dialog to select a folder
|
||||
pub async fn open_folder() -> Result<std::path::PathBuf, uad_core::utils::Error> {
|
||||
rfd::AsyncFileDialog::new()
|
||||
.set_title("Choose a backup location")
|
||||
.pick_folder()
|
||||
.await
|
||||
.ok_or(uad_core::utils::Error::DialogClosed)
|
||||
.map(|f| f.path().to_path_buf())
|
||||
}
|
||||
11
crates/uad-gui/src/lib.rs
Normal file
11
crates/uad-gui/src/lib.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
#![allow(
|
||||
clippy::missing_errors_doc,
|
||||
reason = "Short-circuit doc lint to keep CI green"
|
||||
)]
|
||||
|
||||
pub mod gui;
|
||||
pub mod helpers;
|
||||
pub mod style;
|
||||
pub mod theme;
|
||||
pub mod views;
|
||||
pub mod widgets;
|
||||
112
crates/uad-gui/src/main.rs
Normal file
112
crates/uad-gui/src/main.rs
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
#![windows_subsystem = "windows"]
|
||||
|
||||
use fern::{
|
||||
FormatCallback,
|
||||
colors::{Color, ColoredLevelConfig},
|
||||
};
|
||||
use log::Record;
|
||||
use std::sync::LazyLock;
|
||||
use std::{fmt::Arguments, fs::OpenOptions, path::PathBuf};
|
||||
use uad_core::utils::setup_uad_dir;
|
||||
|
||||
use uad_gui::gui::UadGui;
|
||||
|
||||
static CACHE_DIR: LazyLock<PathBuf> =
|
||||
LazyLock::new(|| setup_uad_dir(&dirs::cache_dir().expect("Can't detect cache dir")));
|
||||
|
||||
fn main() -> iced::Result {
|
||||
// Safety: This function is safe to call in a single-threaded program.
|
||||
// The exact requirement is: you must ensure that there are no other threads concurrently writing or
|
||||
// reading(!) the environment through functions or global variables other than the ones in this module.
|
||||
unsafe {
|
||||
// Force WGPU/Iced to use discrete GPU to prevent crashes on PCs with two GPUs.
|
||||
// See #848 and related pull 850.
|
||||
std::env::set_var("WGPU_POWER_PREF", "high");
|
||||
}
|
||||
|
||||
setup_logger().expect("setup logging");
|
||||
UadGui::start()
|
||||
}
|
||||
|
||||
/// Sets up logging to a new file in `CACHE_DIR"/uadng.log"`
|
||||
/// Also attaches the terminal on Windows machines
|
||||
/// '''
|
||||
/// match `setup_logger().expect("Error` setting up logger")
|
||||
/// '''
|
||||
fn setup_logger() -> Result<(), fern::InitError> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
attach_windows_console();
|
||||
}
|
||||
|
||||
let colors = ColoredLevelConfig::new().info(Color::Green);
|
||||
|
||||
let make_formatter = |use_colors: bool| {
|
||||
move |out: FormatCallback, message: &Arguments, record: &Record| {
|
||||
out.finish(format_args!(
|
||||
"{} {} [{}:{}] {}",
|
||||
chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
|
||||
if use_colors {
|
||||
format!("{:5}", colors.color(record.level()))
|
||||
} else {
|
||||
format!("{:5}", record.level().to_string())
|
||||
},
|
||||
record.file().unwrap_or("?"),
|
||||
record.line().map(|l| l.to_string()).unwrap_or_default(),
|
||||
message
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let default_log_level = log::LevelFilter::Warn;
|
||||
let log_file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.truncate(false)
|
||||
.open(CACHE_DIR.join("uadng.log"))?;
|
||||
|
||||
let file_dispatcher = fern::Dispatch::new()
|
||||
.format(make_formatter(false))
|
||||
.level(default_log_level)
|
||||
// Rust compiler makes module names use _ instead of -
|
||||
.level_for("uad_gui", log::LevelFilter::Debug)
|
||||
.chain(log_file);
|
||||
|
||||
let stdout_dispatcher = fern::Dispatch::new()
|
||||
.format(make_formatter(true))
|
||||
.level(default_log_level)
|
||||
// Rust compiler makes module names use _ instead of -
|
||||
.level_for("uad_gui", log::LevelFilter::Warn)
|
||||
.chain(std::io::stdout());
|
||||
|
||||
fern::Dispatch::new()
|
||||
.chain(stdout_dispatcher)
|
||||
.chain(file_dispatcher)
|
||||
.apply()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// (Windows) Allow the application to display logs to the terminal
|
||||
/// regardless if it was compiled with `windows_subsystem = "windows"`.
|
||||
///
|
||||
/// This is excluded on non-Windows targets.
|
||||
#[cfg(target_os = "windows")]
|
||||
fn attach_windows_console() {
|
||||
use win32console::console::WinConsole;
|
||||
|
||||
const ATTACH_PARENT_PROCESS: u32 = 0xFFFFFFFF;
|
||||
let _ = WinConsole::attach_console(ATTACH_PARENT_PROCESS);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn init_logger() {
|
||||
match setup_logger() {
|
||||
Ok(()) => (),
|
||||
Err(error) => panic!("Error: {error}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
776
crates/uad-gui/src/style.rs
Normal file
776
crates/uad-gui/src/style.rs
Normal file
|
|
@ -0,0 +1,776 @@
|
|||
#![allow(
|
||||
non_snake_case,
|
||||
clippy::trivially_copy_pass_by_ref,
|
||||
clippy::wildcard_imports,
|
||||
reason = "Iced style modules use PascalCase and &Theme; wildcard for local convenience"
|
||||
)]
|
||||
use crate::theme::{ColorPalette, Theme};
|
||||
use iced::widget::{
|
||||
button, checkbox, container, overlay, pick_list, radio, scrollable, text, text_editor,
|
||||
text_input,
|
||||
};
|
||||
use iced::{Background, Border, Color, Shadow};
|
||||
|
||||
// Implement theming catalogs for our custom `Theme` so generic widgets
|
||||
// like `Button<'_, Message, Theme, Renderer>` compile under iced 0.13.
|
||||
|
||||
impl button::Catalog for Theme {
|
||||
type Class<'a> = button::StyleFn<'a, Theme>;
|
||||
|
||||
fn default<'a>() -> <Self as button::Catalog>::Class<'a> {
|
||||
Box::new(|t: &Theme, s: button::Status| Button::Primary(t, s))
|
||||
}
|
||||
|
||||
fn style(
|
||||
&self,
|
||||
class: &<Self as button::Catalog>::Class<'_>,
|
||||
status: button::Status,
|
||||
) -> button::Style {
|
||||
(class)(self, status)
|
||||
}
|
||||
}
|
||||
|
||||
impl checkbox::Catalog for Theme {
|
||||
type Class<'a> = checkbox::StyleFn<'a, Theme>;
|
||||
|
||||
fn default<'a>() -> <Self as checkbox::Catalog>::Class<'a> {
|
||||
Box::new(|t: &Theme, s: checkbox::Status| CheckBox::SettingsEnabled(t, s))
|
||||
}
|
||||
|
||||
fn style(
|
||||
&self,
|
||||
class: &<Self as checkbox::Catalog>::Class<'_>,
|
||||
status: checkbox::Status,
|
||||
) -> checkbox::Style {
|
||||
(class)(self, status)
|
||||
}
|
||||
}
|
||||
|
||||
impl container::Catalog for Theme {
|
||||
type Class<'a> = container::StyleFn<'a, Theme>;
|
||||
|
||||
fn default<'a>() -> <Self as container::Catalog>::Class<'a> {
|
||||
Box::new(|t: &Theme| Container::Background(t))
|
||||
}
|
||||
|
||||
fn style(&self, class: &<Self as container::Catalog>::Class<'_>) -> container::Style {
|
||||
(class)(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl scrollable::Catalog for Theme {
|
||||
type Class<'a> = scrollable::StyleFn<'a, Theme>;
|
||||
|
||||
fn default<'a>() -> <Self as scrollable::Catalog>::Class<'a> {
|
||||
Box::new(|t: &Theme, s: scrollable::Status| Scrollable::Description(t, s))
|
||||
}
|
||||
|
||||
fn style(
|
||||
&self,
|
||||
class: &<Self as scrollable::Catalog>::Class<'_>,
|
||||
status: scrollable::Status,
|
||||
) -> scrollable::Style {
|
||||
(class)(self, status)
|
||||
}
|
||||
}
|
||||
|
||||
impl text::Catalog for Theme {
|
||||
type Class<'a> = text::StyleFn<'a, Theme>;
|
||||
|
||||
fn default<'a>() -> <Self as text::Catalog>::Class<'a> {
|
||||
Box::new(|t: &Theme| Text::Default(t))
|
||||
}
|
||||
|
||||
fn style(&self, class: &<Self as text::Catalog>::Class<'_>) -> text::Style {
|
||||
(class)(self)
|
||||
}
|
||||
}
|
||||
|
||||
// Additional catalogs required by Iced 0.13 generic theming
|
||||
|
||||
impl text_input::Catalog for Theme {
|
||||
type Class<'a> = text_input::StyleFn<'a, Theme>;
|
||||
|
||||
fn default<'a>() -> <Self as text_input::Catalog>::Class<'a> {
|
||||
Box::new(|t: &Theme, s: text_input::Status| {
|
||||
let p = t.palette();
|
||||
|
||||
let active = text_input::Style {
|
||||
background: Background::Color(p.base.foreground),
|
||||
border: Border {
|
||||
color: p.base.foreground,
|
||||
width: 0.0,
|
||||
radius: 5.0.into(),
|
||||
},
|
||||
icon: Color {
|
||||
a: 0.5,
|
||||
..p.normal.primary
|
||||
},
|
||||
placeholder: p.normal.surface,
|
||||
value: p.bright.primary,
|
||||
selection: p.normal.primary,
|
||||
};
|
||||
|
||||
let focused = text_input::Style {
|
||||
background: Background::Color(p.base.foreground),
|
||||
border: Border {
|
||||
color: Color {
|
||||
a: 0.5,
|
||||
..p.normal.primary
|
||||
},
|
||||
width: 1.0,
|
||||
radius: 2.0.into(),
|
||||
},
|
||||
icon: Color {
|
||||
a: 0.5,
|
||||
..p.normal.primary
|
||||
},
|
||||
placeholder: p.normal.surface,
|
||||
value: p.bright.primary,
|
||||
selection: p.normal.primary,
|
||||
};
|
||||
|
||||
let disabled = text_input::Style {
|
||||
background: Background::Color(p.base.background),
|
||||
border: Border {
|
||||
color: Color {
|
||||
a: 0.5,
|
||||
..p.base.foreground
|
||||
},
|
||||
width: 1.0,
|
||||
radius: 2.0.into(),
|
||||
},
|
||||
icon: Color {
|
||||
a: 0.5,
|
||||
..p.base.foreground
|
||||
},
|
||||
placeholder: p.normal.surface,
|
||||
value: p.bright.primary,
|
||||
selection: p.normal.primary,
|
||||
};
|
||||
|
||||
match s {
|
||||
text_input::Status::Active => active,
|
||||
text_input::Status::Focused { .. } | text_input::Status::Hovered => focused,
|
||||
text_input::Status::Disabled => disabled,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn style(
|
||||
&self,
|
||||
class: &<Self as text_input::Catalog>::Class<'_>,
|
||||
status: text_input::Status,
|
||||
) -> text_input::Style {
|
||||
(class)(self, status)
|
||||
}
|
||||
}
|
||||
|
||||
impl pick_list::Catalog for Theme {
|
||||
type Class<'a> = pick_list::StyleFn<'a, Theme>;
|
||||
|
||||
fn default<'a>() -> <Self as pick_list::Catalog>::Class<'a> {
|
||||
Box::new(|t: &Theme, s: pick_list::Status| {
|
||||
let p = t.palette();
|
||||
let border_color = match s {
|
||||
pick_list::Status::Hovered => p.normal.primary,
|
||||
_ => Color {
|
||||
a: 0.5,
|
||||
..p.normal.primary
|
||||
},
|
||||
};
|
||||
pick_list::Style {
|
||||
text_color: p.bright.surface,
|
||||
placeholder_color: p.bright.surface,
|
||||
handle_color: p.bright.surface,
|
||||
background: Background::Color(p.base.background),
|
||||
border: Border {
|
||||
color: border_color,
|
||||
width: 1.0,
|
||||
radius: 2.0.into(),
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn style(
|
||||
&self,
|
||||
class: &<Self as pick_list::Catalog>::Class<'_>,
|
||||
status: pick_list::Status,
|
||||
) -> pick_list::Style {
|
||||
(class)(self, status)
|
||||
}
|
||||
}
|
||||
|
||||
impl overlay::menu::Catalog for Theme {
|
||||
type Class<'a> = overlay::menu::StyleFn<'a, Theme>;
|
||||
|
||||
fn default<'a>() -> <Self as overlay::menu::Catalog>::Class<'a> {
|
||||
Box::new(|t: &Theme| {
|
||||
let p = t.palette();
|
||||
overlay::menu::Style {
|
||||
text_color: p.bright.surface,
|
||||
background: p.base.background.into(),
|
||||
border: Border {
|
||||
color: p.base.background,
|
||||
width: 1.0,
|
||||
radius: 2.0.into(),
|
||||
},
|
||||
selected_text_color: p.bright.surface,
|
||||
selected_background: p.normal.primary.into(),
|
||||
shadow: Shadow::default(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn style(&self, class: &<Self as overlay::menu::Catalog>::Class<'_>) -> overlay::menu::Style {
|
||||
(class)(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl radio::Catalog for Theme {
|
||||
type Class<'a> = radio::StyleFn<'a, Theme>;
|
||||
|
||||
fn default<'a>() -> <Self as radio::Catalog>::Class<'a> {
|
||||
Box::new(|t: &Theme, s: radio::Status| {
|
||||
let p = t.palette();
|
||||
let active = radio::Style {
|
||||
background: Color::TRANSPARENT.into(),
|
||||
dot_color: p.bright.primary,
|
||||
border_width: 1.0,
|
||||
border_color: p.bright.primary,
|
||||
text_color: None,
|
||||
};
|
||||
|
||||
match s {
|
||||
radio::Status::Active { .. } => active,
|
||||
radio::Status::Hovered { .. } => radio::Style {
|
||||
border_width: 2.0,
|
||||
..active
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn style(
|
||||
&self,
|
||||
class: &<Self as radio::Catalog>::Class<'_>,
|
||||
status: radio::Status,
|
||||
) -> radio::Style {
|
||||
(class)(self, status)
|
||||
}
|
||||
}
|
||||
|
||||
impl text_editor::Catalog for Theme {
|
||||
type Class<'a> = text_editor::StyleFn<'a, Theme>;
|
||||
|
||||
fn default<'a>() -> <Self as text_editor::Catalog>::Class<'a> {
|
||||
Box::new(|t: &Theme, _s: text_editor::Status| {
|
||||
let p = t.palette();
|
||||
text_editor::Style {
|
||||
background: Background::Color(p.base.foreground),
|
||||
border: Border {
|
||||
color: Color::TRANSPARENT,
|
||||
width: 0.0,
|
||||
radius: 0.0.into(),
|
||||
},
|
||||
placeholder: p.normal.surface,
|
||||
value: p.bright.surface,
|
||||
selection: Color {
|
||||
a: 0.3,
|
||||
..p.normal.primary
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn style(
|
||||
&self,
|
||||
class: &<Self as text_editor::Catalog>::Class<'_>,
|
||||
status: text_editor::Status,
|
||||
) -> text_editor::Style {
|
||||
(class)(self, status)
|
||||
}
|
||||
}
|
||||
|
||||
// Rule styling for custom Theme (needed by vertical_rule)
|
||||
impl iced::widget::rule::Catalog for Theme {
|
||||
type Class<'a> = iced::widget::rule::StyleFn<'a, Theme>;
|
||||
|
||||
fn default<'a>() -> <Self as iced::widget::rule::Catalog>::Class<'a> {
|
||||
Box::new(|t: &Theme| {
|
||||
let p = t.palette();
|
||||
iced::widget::rule::Style {
|
||||
color: p.bright.surface,
|
||||
radius: 2.0.into(),
|
||||
fill_mode: iced::widget::rule::FillMode::Full,
|
||||
snap: true,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn style(
|
||||
&self,
|
||||
class: &<Self as iced::widget::rule::Catalog>::Class<'_>,
|
||||
) -> iced::widget::rule::Style {
|
||||
(class)(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub mod Container {
|
||||
use super::*;
|
||||
|
||||
#[allow(dead_code, reason = "Used by other themes or future styles")]
|
||||
#[must_use]
|
||||
pub fn Invisible(_: &Theme) -> container::Style {
|
||||
container::Style::default()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn Frame(theme: &Theme) -> container::Style {
|
||||
let p = theme.palette();
|
||||
container::Style {
|
||||
background: Some(Background::Color(p.base.foreground)),
|
||||
text_color: Some(p.bright.surface),
|
||||
border: Border {
|
||||
color: Color::TRANSPARENT,
|
||||
width: 0.0,
|
||||
radius: 5.0.into(),
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
snap: true,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn BorderedFrame(theme: &Theme) -> container::Style {
|
||||
let p = theme.palette();
|
||||
container::Style {
|
||||
background: Some(Background::Color(p.base.foreground)),
|
||||
text_color: Some(p.bright.surface),
|
||||
border: Border {
|
||||
color: p.normal.error,
|
||||
width: 1.0,
|
||||
radius: 5.0.into(),
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
snap: true,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code, reason = "Currently unused in some views")]
|
||||
#[must_use]
|
||||
pub fn Tooltip(theme: &Theme) -> container::Style {
|
||||
let p = theme.palette();
|
||||
container::Style {
|
||||
background: Some(Background::Color(p.base.foreground)),
|
||||
text_color: Some(p.bright.surface),
|
||||
border: Border {
|
||||
color: p.normal.primary,
|
||||
width: 1.0,
|
||||
radius: 8.0.into(),
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
snap: true,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn Background(theme: &Theme) -> container::Style {
|
||||
let p = theme.palette();
|
||||
container::Style {
|
||||
background: Some(Background::Color(p.base.background)),
|
||||
text_color: Some(p.bright.surface),
|
||||
border: Border {
|
||||
color: Color::TRANSPARENT,
|
||||
width: 0.0,
|
||||
radius: 5.0.into(),
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
snap: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod Button {
|
||||
use super::*;
|
||||
|
||||
#[allow(
|
||||
dead_code,
|
||||
reason = "Helper used by multiple styles; may be inlined by compiler"
|
||||
)]
|
||||
fn base(border_color: Color) -> button::Style {
|
||||
button::Style {
|
||||
background: None,
|
||||
text_color: Color::WHITE,
|
||||
border: Border {
|
||||
color: border_color,
|
||||
width: 1.0,
|
||||
radius: 2.0.into(),
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
snap: true,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn Primary(theme: &Theme, status: button::Status) -> button::Style {
|
||||
let p = theme.palette();
|
||||
let mut style = style_active_hover_disabled(p.bright.primary, p.bright.primary, status);
|
||||
if matches!(status, button::Status::Active | button::Status::Pressed) {
|
||||
style.background = Some(Background::Color(p.base.foreground));
|
||||
}
|
||||
style
|
||||
}
|
||||
|
||||
#[allow(
|
||||
dead_code,
|
||||
reason = "Alias kept for semantic clarity in some call-sites"
|
||||
)]
|
||||
#[must_use]
|
||||
pub fn SelfUpdate(theme: &Theme, status: button::Status) -> button::Style {
|
||||
Primary(theme, status)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn RestorePackage(theme: &Theme, status: button::Status) -> button::Style {
|
||||
let p = theme.palette();
|
||||
let mut style = style_active_hover_disabled(p.bright.secondary, p.bright.secondary, status);
|
||||
if matches!(status, button::Status::Active | button::Status::Pressed) {
|
||||
style.background = Some(Background::Color(p.base.foreground));
|
||||
}
|
||||
if matches!(status, button::Status::Disabled) {
|
||||
style.background = Some(Background::Color(Color {
|
||||
a: 0.05,
|
||||
..p.normal.primary
|
||||
}));
|
||||
style.text_color = p.bright.primary;
|
||||
}
|
||||
style
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn UninstallPackage(theme: &Theme, status: button::Status) -> button::Style {
|
||||
let p = theme.palette();
|
||||
let mut style = style_active_hover_disabled(p.bright.error, p.bright.error, status);
|
||||
if matches!(status, button::Status::Active | button::Status::Pressed) {
|
||||
style.background = Some(Background::Color(p.base.foreground));
|
||||
}
|
||||
style
|
||||
}
|
||||
|
||||
#[allow(
|
||||
dead_code,
|
||||
reason = "Style exposed for disabled state buttons in some contexts"
|
||||
)]
|
||||
#[must_use]
|
||||
pub fn Unavailable(theme: &Theme, status: button::Status) -> button::Style {
|
||||
UninstallPackage(theme, status)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn NormalPackage(theme: &Theme, status: button::Status) -> button::Style {
|
||||
let p = theme.palette();
|
||||
match status {
|
||||
button::Status::Hovered => button::Style {
|
||||
background: Some(Background::Color(Color {
|
||||
a: 0.25,
|
||||
..p.normal.primary
|
||||
})),
|
||||
text_color: p.bright.surface,
|
||||
border: Border {
|
||||
color: p.base.background,
|
||||
width: 0.0,
|
||||
radius: 5.0.into(),
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
snap: true,
|
||||
},
|
||||
_ => button::Style {
|
||||
background: Some(Background::Color(p.base.foreground)),
|
||||
text_color: p.bright.surface,
|
||||
border: Border {
|
||||
color: p.base.background,
|
||||
width: 0.0,
|
||||
radius: 5.0.into(),
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
snap: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn SelectedPackage(theme: &Theme, _status: button::Status) -> button::Style {
|
||||
let p = theme.palette();
|
||||
button::Style {
|
||||
background: Some(Background::Color(Color {
|
||||
a: 0.25,
|
||||
..p.normal.primary
|
||||
})),
|
||||
text_color: p.bright.primary,
|
||||
border: Border {
|
||||
color: p.normal.primary,
|
||||
width: 0.0,
|
||||
radius: 5.0.into(),
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
snap: true,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code, reason = "Used in views where buttons must be invisible")]
|
||||
#[must_use]
|
||||
pub fn Hidden(_: &Theme, _: button::Status) -> button::Style {
|
||||
button::Style {
|
||||
background: Some(Background::Color(Color::TRANSPARENT)),
|
||||
text_color: Color::TRANSPARENT,
|
||||
border: Border {
|
||||
color: Color::TRANSPARENT,
|
||||
width: 0.0,
|
||||
radius: 5.0.into(),
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
snap: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn style_active_hover_disabled(
|
||||
main: Color,
|
||||
text: Color,
|
||||
status: button::Status,
|
||||
) -> button::Style {
|
||||
match status {
|
||||
button::Status::Active | button::Status::Pressed => button::Style {
|
||||
background: Some(Background::Color(main)),
|
||||
text_color: text,
|
||||
border: Border {
|
||||
color: Color { a: 0.5, ..main },
|
||||
width: 1.0,
|
||||
radius: 2.0.into(),
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
snap: true,
|
||||
},
|
||||
button::Status::Hovered => button::Style {
|
||||
background: Some(Background::Color(Color { a: 0.25, ..main })),
|
||||
text_color: text,
|
||||
border: Border {
|
||||
color: Color { a: 0.5, ..main },
|
||||
width: 1.0,
|
||||
radius: 2.0.into(),
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
snap: true,
|
||||
},
|
||||
button::Status::Disabled => button::Style {
|
||||
background: Some(Background::Color(Color { a: 0.05, ..main })),
|
||||
text_color: Color { a: 0.5, ..text },
|
||||
border: Border {
|
||||
color: Color { a: 0.5, ..main },
|
||||
width: 1.0,
|
||||
radius: 2.0.into(),
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
snap: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod Scrollable {
|
||||
use super::*;
|
||||
|
||||
#[allow(dead_code, reason = "Kept for future custom rails variations")]
|
||||
fn rails(scroller_color: Color) -> (scrollable::Rail, scrollable::Rail) {
|
||||
let rail = scrollable::Rail {
|
||||
background: Some(Background::Color(Color::TRANSPARENT)),
|
||||
border: Border {
|
||||
color: Color::TRANSPARENT,
|
||||
width: 0.0,
|
||||
radius: 5.0.into(),
|
||||
},
|
||||
scroller: scrollable::Scroller {
|
||||
background: Background::Color(scroller_color),
|
||||
border: Border {
|
||||
color: Color::TRANSPARENT,
|
||||
width: 1.0,
|
||||
radius: 5.0.into(),
|
||||
},
|
||||
},
|
||||
};
|
||||
(rail, rail)
|
||||
}
|
||||
|
||||
fn autoscroll(p: ColorPalette) -> scrollable::AutoScroll {
|
||||
scrollable::AutoScroll {
|
||||
background: Background::Color(Color {
|
||||
a: 0.05,
|
||||
..p.base.background
|
||||
}),
|
||||
border: Border {
|
||||
color: Color::TRANSPARENT,
|
||||
width: 0.0,
|
||||
radius: 5.0.into(),
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
icon: p.bright.surface,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn Description(theme: &Theme, _status: scrollable::Status) -> scrollable::Style {
|
||||
let p = theme.palette();
|
||||
let (v, h) = rails(p.normal.surface);
|
||||
scrollable::Style {
|
||||
container: container::Style::default(),
|
||||
vertical_rail: v,
|
||||
horizontal_rail: h,
|
||||
gap: Some(Background::Color(Color::TRANSPARENT)),
|
||||
auto_scroll: autoscroll(p),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn Packages(theme: &Theme, _status: scrollable::Status) -> scrollable::Style {
|
||||
let p = theme.palette();
|
||||
let (v, h) = rails(p.base.foreground);
|
||||
scrollable::Style {
|
||||
container: container::Style::default(),
|
||||
vertical_rail: v,
|
||||
horizontal_rail: h,
|
||||
gap: Some(Background::Color(Color::TRANSPARENT)),
|
||||
auto_scroll: autoscroll(p),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod CheckBox {
|
||||
use super::*;
|
||||
|
||||
#[must_use]
|
||||
pub fn PackageEnabled(theme: &Theme, _status: checkbox::Status) -> checkbox::Style {
|
||||
let p = theme.palette();
|
||||
checkbox::Style {
|
||||
background: Background::Color(p.base.background),
|
||||
icon_color: p.bright.primary,
|
||||
border: Border {
|
||||
color: p.base.background,
|
||||
width: 1.0,
|
||||
radius: 5.0.into(),
|
||||
},
|
||||
text_color: Some(p.bright.surface),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn PackageDisabled(theme: &Theme, _status: checkbox::Status) -> checkbox::Style {
|
||||
let p = theme.palette();
|
||||
checkbox::Style {
|
||||
background: Background::Color(Color {
|
||||
a: 0.55,
|
||||
..p.base.background
|
||||
}),
|
||||
icon_color: p.bright.primary,
|
||||
border: Border {
|
||||
color: p.normal.primary,
|
||||
width: 1.0,
|
||||
radius: 5.0.into(),
|
||||
},
|
||||
text_color: Some(p.normal.primary),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn SettingsEnabled(theme: &Theme, _status: checkbox::Status) -> checkbox::Style {
|
||||
let p = theme.palette();
|
||||
checkbox::Style {
|
||||
background: Background::Color(p.base.background),
|
||||
icon_color: p.bright.primary,
|
||||
border: Border {
|
||||
color: p.bright.primary,
|
||||
width: 1.0,
|
||||
radius: 5.0.into(),
|
||||
},
|
||||
text_color: Some(p.bright.surface),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn SettingsDisabled(theme: &Theme, _status: checkbox::Status) -> checkbox::Style {
|
||||
let p = theme.palette();
|
||||
checkbox::Style {
|
||||
background: Background::Color(p.base.foreground),
|
||||
icon_color: p.bright.primary,
|
||||
border: Border {
|
||||
color: p.normal.primary,
|
||||
width: 1.0,
|
||||
radius: 5.0.into(),
|
||||
},
|
||||
text_color: Some(p.bright.surface),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod Text {
|
||||
use super::*;
|
||||
|
||||
#[must_use]
|
||||
pub fn Default(theme: &Theme) -> text::Style {
|
||||
let _ = theme;
|
||||
text::Style::default()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn Ok(theme: &Theme) -> text::Style {
|
||||
let p = theme.palette();
|
||||
text::Style {
|
||||
color: Some(p.bright.secondary),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn Danger(theme: &Theme) -> text::Style {
|
||||
let p = theme.palette();
|
||||
text::Style {
|
||||
color: Some(p.bright.error),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn Commentary(theme: &Theme) -> text::Style {
|
||||
let p = theme.palette();
|
||||
text::Style {
|
||||
color: Some(p.normal.surface),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(
|
||||
dead_code,
|
||||
reason = "Convenience factory used by dynamic text coloring"
|
||||
)]
|
||||
pub fn Color(c: Color) -> impl Fn(&Theme) -> text::Style {
|
||||
move |_t: &Theme| text::Style { color: Some(c) }
|
||||
}
|
||||
}
|
||||
|
||||
// Unit tests
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_palette() {
|
||||
let palette = Theme::Dark.palette();
|
||||
|
||||
assert_ne!(palette.base.background, palette.base.foreground);
|
||||
assert_ne!(palette.normal.primary, Color::BLACK);
|
||||
assert_ne!(palette.normal.surface, Color::BLACK);
|
||||
assert_ne!(palette.bright.primary, Color::BLACK);
|
||||
// if `LIGHT` then this can be `BLACK`
|
||||
// https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/pull/730#issuecomment-2525405134
|
||||
//assert_ne!(palette.bright.surface, Color::BLACK);
|
||||
assert_ne!(palette.normal.error, Color::BLACK);
|
||||
assert_ne!(palette.bright.error, Color::BLACK);
|
||||
}
|
||||
}
|
||||
219
crates/uad-gui/src/theme.rs
Normal file
219
crates/uad-gui/src/theme.rs
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
use iced::theme::{self, Mode, Palette, Style};
|
||||
use iced::{Color, color};
|
||||
use std::sync::LazyLock;
|
||||
|
||||
/*
|
||||
In-memory caching.
|
||||
This fixes the perf bug
|
||||
caused by Iced repeatedly calling `palette`.
|
||||
|
||||
Coincidentally, this also ensures consistent colors across the GUI,
|
||||
at the cost of requiring a restart to update the palette.
|
||||
(this is just a patch, not a fix)
|
||||
*/
|
||||
pub static OS_COLOR_SCHEME: LazyLock<dark_light::Mode> =
|
||||
LazyLock::new(|| dark_light::detect().unwrap_or(dark_light::Mode::Unspecified));
|
||||
|
||||
#[derive(Default, Debug, PartialEq, Eq, Copy, Clone)]
|
||||
/// Color scheme
|
||||
pub enum Theme {
|
||||
#[default]
|
||||
/// `Dark` or `Light`, according to `dark_light`
|
||||
Auto,
|
||||
/// `Dark`-ish and purple
|
||||
Lupin,
|
||||
/// white on black
|
||||
Dark,
|
||||
/// black on white
|
||||
Light,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct BaseColors {
|
||||
pub background: Color,
|
||||
pub foreground: Color,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct NormalColors {
|
||||
pub primary: Color,
|
||||
#[allow(dead_code, reason = "Reserved for future palette updates")]
|
||||
pub secondary: Color,
|
||||
pub surface: Color,
|
||||
pub error: Color,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct BrightColors {
|
||||
pub primary: Color,
|
||||
pub secondary: Color,
|
||||
pub surface: Color,
|
||||
pub error: Color,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ColorPalette {
|
||||
pub base: BaseColors,
|
||||
pub normal: NormalColors,
|
||||
pub bright: BrightColors,
|
||||
}
|
||||
|
||||
impl Theme {
|
||||
pub const ALL: [Self; 4] = [Self::Auto, Self::Lupin, Self::Dark, Self::Light];
|
||||
|
||||
#[allow(
|
||||
clippy::unreadable_literal,
|
||||
reason = "https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/pull/578#discussion_r1759653408"
|
||||
)]
|
||||
/// This `fn` _could_ be `const`,
|
||||
/// but `deref`ing a lazy-`static` is non-`const`.
|
||||
#[must_use]
|
||||
pub fn palette(self) -> ColorPalette {
|
||||
const DARK: ColorPalette = ColorPalette {
|
||||
base: BaseColors {
|
||||
background: color!(0x111111),
|
||||
foreground: color!(0x1C1C1C),
|
||||
},
|
||||
normal: NormalColors {
|
||||
primary: color!(0x5E4266),
|
||||
secondary: color!(0x386E50),
|
||||
surface: color!(0x828282),
|
||||
error: color!(0x992B2B),
|
||||
},
|
||||
bright: BrightColors {
|
||||
primary: color!(0xBA84FC),
|
||||
secondary: color!(0x49EB7A),
|
||||
surface: color!(0xE0E0E0),
|
||||
error: color!(0xC13047),
|
||||
},
|
||||
};
|
||||
const LIGHT: ColorPalette = ColorPalette {
|
||||
base: BaseColors {
|
||||
background: color!(0xEEEEEE),
|
||||
foreground: color!(0xE0E0E0),
|
||||
},
|
||||
normal: NormalColors {
|
||||
primary: color!(0x818181),
|
||||
secondary: color!(0xF9D659),
|
||||
surface: color!(0x818181),
|
||||
error: color!(0x992B2B),
|
||||
},
|
||||
bright: BrightColors {
|
||||
primary: color!(0x673AB7),
|
||||
secondary: color!(0x3797A4),
|
||||
surface: color!(0x000000),
|
||||
error: color!(0xC13047),
|
||||
},
|
||||
};
|
||||
const LUPIN: ColorPalette = ColorPalette {
|
||||
base: BaseColors {
|
||||
background: color!(0x282A36),
|
||||
foreground: color!(0x353746),
|
||||
},
|
||||
normal: NormalColors {
|
||||
primary: color!(0x58406F),
|
||||
secondary: color!(0x386E50),
|
||||
surface: color!(0xA2A4A3),
|
||||
error: color!(0xA13034),
|
||||
},
|
||||
bright: BrightColors {
|
||||
primary: color!(0xBD94F9),
|
||||
secondary: color!(0x49EB7A),
|
||||
surface: color!(0xF4F8F3),
|
||||
error: color!(0xE63E6D),
|
||||
},
|
||||
};
|
||||
match self {
|
||||
Self::Dark => DARK,
|
||||
Self::Light => LIGHT,
|
||||
Self::Lupin => LUPIN,
|
||||
Self::Auto => match *OS_COLOR_SCHEME {
|
||||
dark_light::Mode::Light => LIGHT,
|
||||
dark_light::Mode::Dark | dark_light::Mode::Unspecified => DARK,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Theme {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::Dark => "Dark",
|
||||
Self::Light => "Light",
|
||||
Self::Lupin => "Lupin",
|
||||
Self::Auto => "Auto (follow system theme)",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a string to the GUI's Theme type
|
||||
#[must_use]
|
||||
pub fn string_to_theme(theme: &str) -> Theme {
|
||||
match theme {
|
||||
"Lupin" => Theme::Lupin,
|
||||
"Dark" => Theme::Dark,
|
||||
"Light" => Theme::Light,
|
||||
_ => Theme::Auto,
|
||||
}
|
||||
}
|
||||
|
||||
impl theme::Base for Theme {
|
||||
fn default(preference: Mode) -> Self {
|
||||
match preference {
|
||||
Mode::Light => Self::Light,
|
||||
Mode::Dark => Self::Dark,
|
||||
Mode::None => match *OS_COLOR_SCHEME {
|
||||
dark_light::Mode::Light => Self::Light,
|
||||
dark_light::Mode::Dark | dark_light::Mode::Unspecified => Self::Dark,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn mode(&self) -> Mode {
|
||||
let resolved = match self {
|
||||
Self::Auto => match *OS_COLOR_SCHEME {
|
||||
dark_light::Mode::Light => Self::Light,
|
||||
dark_light::Mode::Dark | dark_light::Mode::Unspecified => Self::Dark,
|
||||
},
|
||||
other => *other,
|
||||
};
|
||||
|
||||
match resolved {
|
||||
Self::Light => Mode::Light,
|
||||
_ => Mode::Dark,
|
||||
}
|
||||
}
|
||||
|
||||
fn base(&self) -> Style {
|
||||
let p = Self::palette(*self);
|
||||
Style {
|
||||
background_color: p.base.background,
|
||||
text_color: p.bright.surface,
|
||||
}
|
||||
}
|
||||
|
||||
fn palette(&self) -> Option<Palette> {
|
||||
let p = Self::palette(*self);
|
||||
Some(Palette {
|
||||
background: p.base.background,
|
||||
text: p.bright.surface,
|
||||
primary: p.normal.primary,
|
||||
success: p.bright.secondary,
|
||||
warning: p.normal.surface,
|
||||
danger: p.normal.error,
|
||||
})
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
match self {
|
||||
Self::Dark => "Dark",
|
||||
Self::Light => "Light",
|
||||
Self::Lupin => "Lupin",
|
||||
Self::Auto => "Auto",
|
||||
}
|
||||
}
|
||||
}
|
||||
174
crates/uad-gui/src/views/about.rs
Normal file
174
crates/uad-gui/src/views/about.rs
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
use crate::helpers::button_primary;
|
||||
use crate::theme::Theme;
|
||||
use crate::{gui::UpdateState, style, widgets::text};
|
||||
use iced::widget::{Space, column, container, row};
|
||||
use iced::{Alignment, Element, Length, Renderer};
|
||||
use log::error;
|
||||
use std::path::PathBuf;
|
||||
use uad_core::CACHE_DIR;
|
||||
use uad_core::adb;
|
||||
use uad_core::uad_lists::LIST_FNAME;
|
||||
use uad_core::utils::{FULL_NAME, NAME, last_modified_date, open_url};
|
||||
|
||||
#[cfg(feature = "self-update")]
|
||||
use uad_core::update::SelfUpdateStatus;
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct About {}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Message {
|
||||
UrlPressed(PathBuf),
|
||||
UpdateUadLists,
|
||||
DoSelfUpdate,
|
||||
}
|
||||
|
||||
impl About {
|
||||
#[allow(
|
||||
clippy::unused_self,
|
||||
reason = "Trait-like shape required by GUI architecture"
|
||||
)]
|
||||
pub fn update(&mut self, msg: Message) {
|
||||
if let Message::UrlPressed(url) = msg {
|
||||
open_url(url);
|
||||
}
|
||||
// other events are handled by UadGui update()
|
||||
}
|
||||
#[allow(
|
||||
clippy::unused_self,
|
||||
reason = "Trait-like shape required by GUI architecture"
|
||||
)]
|
||||
pub fn view(&self, update_state: &UpdateState) -> Element<'_, Message, Theme, Renderer> {
|
||||
let about_text = text(format!(
|
||||
"{FULL_NAME} ({NAME}) is a free and open-source community project \naiming at simplifying the removal of pre-installed apps on any Android device."
|
||||
));
|
||||
|
||||
let descr_container = container(about_text)
|
||||
.width(Length::Fill)
|
||||
.padding(25)
|
||||
.style(style::Container::Frame);
|
||||
|
||||
let date = last_modified_date(CACHE_DIR.join(LIST_FNAME));
|
||||
let uad_list_text =
|
||||
text(format!("{NAME} package list: v{}", date.format("%Y%m%d"))).width(250);
|
||||
let last_update_text = text(update_state.uad_list.to_string());
|
||||
let uad_lists_btn = button_primary("Update").on_press(Message::UpdateUadLists);
|
||||
|
||||
#[cfg(feature = "self-update")]
|
||||
let self_update_row = {
|
||||
let self_update_btn = button_primary("Update").on_press(Message::DoSelfUpdate);
|
||||
|
||||
let uad_version_text =
|
||||
text(format!("{NAME} version: v{}", env!("CARGO_PKG_VERSION"))).width(250);
|
||||
|
||||
let self_update_text = update_state
|
||||
.self_update
|
||||
.latest_release
|
||||
.as_ref()
|
||||
.map_or_else(
|
||||
|| {
|
||||
if update_state.self_update.status == SelfUpdateStatus::Done {
|
||||
"(No update available)".to_string()
|
||||
} else {
|
||||
update_state.self_update.status.to_string()
|
||||
}
|
||||
},
|
||||
|r| {
|
||||
if update_state.self_update.status == SelfUpdateStatus::Updating {
|
||||
update_state.self_update.status.to_string()
|
||||
} else {
|
||||
format!("({} available)", r.tag_name)
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
let last_self_update_text = text(self_update_text).style(style::Text::Default);
|
||||
|
||||
row![uad_version_text, self_update_btn, last_self_update_text,]
|
||||
.align_y(Alignment::Center)
|
||||
.spacing(10)
|
||||
.width(550)
|
||||
};
|
||||
|
||||
let uad_list_row = row![uad_list_text, uad_lists_btn, last_update_text,]
|
||||
.align_y(Alignment::Center)
|
||||
.spacing(10)
|
||||
.width(550);
|
||||
|
||||
/*
|
||||
There's no need to fetch this info every time the view is updated,
|
||||
we could cache it in a `static` `LazyLock`.
|
||||
|
||||
But what if the system updates ADB while the app is running?
|
||||
the numbers will be out of sync!
|
||||
|
||||
However, the server will still be the "old" version
|
||||
until it's killed
|
||||
*/
|
||||
let adb_version_text = text(match adb::ACommand::new().version() {
|
||||
Ok(s) => s
|
||||
.lines()
|
||||
.nth(0)
|
||||
.unwrap_or_else(|| unreachable!())
|
||||
// This allocation is good.
|
||||
// If it was a ref, the app would hold the entire string
|
||||
// instead of the relevant slice.
|
||||
.to_string(),
|
||||
Err(e) => {
|
||||
error!("{e}");
|
||||
"Couldn't fetch ADB version. Is it installed?".into()
|
||||
// satisfy `match` by inferring the type of the `Ok` arm
|
||||
}
|
||||
})
|
||||
.width(250);
|
||||
let adb_version_row = row![adb_version_text].align_y(Alignment::Center).width(550);
|
||||
|
||||
#[cfg(feature = "self-update")]
|
||||
let update_column = column![uad_list_row, self_update_row, adb_version_row];
|
||||
#[cfg(not(feature = "self-update"))]
|
||||
let update_column = column![uad_list_row, adb_version_row];
|
||||
|
||||
let update_column = update_column.align_x(Alignment::Center).spacing(10);
|
||||
|
||||
let update_container = container(update_column)
|
||||
.width(Length::Fill)
|
||||
.center_x(Length::Fill)
|
||||
.padding(10)
|
||||
.style(style::Container::Frame);
|
||||
|
||||
let website_btn =
|
||||
button_primary("GitHub page").on_press(Message::UrlPressed(PathBuf::from(
|
||||
"https://github.com/Universal-Debloater-Alliance/universal-android-debloater",
|
||||
)));
|
||||
|
||||
let issue_btn = button_primary("Have an issue?")
|
||||
.on_press(Message::UrlPressed(PathBuf::from(
|
||||
"https://github.com/Universal-Debloater-Alliance/universal-android-debloater/issues",
|
||||
)));
|
||||
|
||||
let log_btn = button_primary("Locate the logfiles")
|
||||
.on_press(Message::UrlPressed(CACHE_DIR.to_path_buf()));
|
||||
|
||||
let wiki_btn = button_primary("Wiki").on_press(Message::UrlPressed(PathBuf::from(
|
||||
"https://github.com/Universal-Debloater-Alliance/universal-android-debloater/wiki",
|
||||
)));
|
||||
|
||||
let row = row![website_btn, wiki_btn, issue_btn, log_btn,].spacing(20);
|
||||
|
||||
let content = column![
|
||||
Space::new().width(Length::Fill).height(Length::Shrink),
|
||||
descr_container,
|
||||
update_container,
|
||||
row,
|
||||
]
|
||||
.width(Length::Fill)
|
||||
.spacing(20)
|
||||
.align_x(Alignment::Center);
|
||||
|
||||
container(content)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.padding(10)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
1597
crates/uad-gui/src/views/list.rs
Normal file
1597
crates/uad-gui/src/views/list.rs
Normal file
File diff suppressed because it is too large
Load diff
694
crates/uad-gui/src/views/settings.rs
Normal file
694
crates/uad-gui/src/views/settings.rs
Normal file
|
|
@ -0,0 +1,694 @@
|
|||
use crate::helpers::{button_primary, open_folder};
|
||||
use crate::theme::Theme;
|
||||
use crate::theme::string_to_theme;
|
||||
use crate::{
|
||||
style,
|
||||
views::list::{List as AppsView, PackageInfo},
|
||||
widgets::modal::Modal,
|
||||
widgets::navigation_menu::ICONS,
|
||||
widgets::package_row::PackageRow,
|
||||
widgets::text,
|
||||
};
|
||||
use chrono;
|
||||
use iced::widget::{Space, button, checkbox, column, container, pick_list, radio, row, scrollable};
|
||||
use iced::{Alignment, Element, Length, Renderer, Task, alignment};
|
||||
use log::{debug, error, info};
|
||||
use std::path::PathBuf;
|
||||
use uad_core::{
|
||||
config::{BackupSettings, Config, DeviceSettings, GeneralSettings},
|
||||
save::{backup_phone, list_available_backup_user, list_available_backups, restore_backup},
|
||||
sync::{
|
||||
AdbError, CorePackage, Phone, User, get_android_sdk, run_adb_shell_action,
|
||||
supports_disabling, supports_multi_user,
|
||||
},
|
||||
utils::{DisplayablePath, Error, NAME, export_packages, generate_backup_name, open_url},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum PopUpModal {
|
||||
ExportUninstalled,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Settings {
|
||||
pub general: GeneralSettings,
|
||||
pub device: DeviceSettings,
|
||||
is_loading: bool,
|
||||
modal: Option<PopUpModal>,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
general: Config::load_configuration_file().general,
|
||||
device: DeviceSettings::default(),
|
||||
is_loading: false,
|
||||
modal: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Message {
|
||||
LoadDeviceSettings,
|
||||
ExpertMode(bool),
|
||||
DisableMode(bool),
|
||||
MultiUserMode(bool),
|
||||
ApplyTheme(Theme),
|
||||
UrlPressed(PathBuf),
|
||||
BackupSelected(DisplayablePath),
|
||||
BackupDevice,
|
||||
RestoreDevice,
|
||||
RestoringDevice(Result<PackageInfo, AdbError>),
|
||||
DeviceBackedUp(Result<bool, String>),
|
||||
ChooseBackUpFolder,
|
||||
FolderChosen(Result<PathBuf, Error>),
|
||||
ExportPackages,
|
||||
PackagesExported(Result<bool, String>),
|
||||
ModalHide,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
pub fn update(
|
||||
&mut self,
|
||||
phone: &Phone,
|
||||
packages: &[Vec<PackageRow>],
|
||||
nb_running_async_adb_commands: &mut u32,
|
||||
msg: Message,
|
||||
selected_user: Option<User>,
|
||||
) -> Task<Message> {
|
||||
match msg {
|
||||
Message::ModalHide => self.handle_modal_hide(),
|
||||
Message::ExpertMode(toggled) => self.handle_expert_mode(phone, toggled),
|
||||
Message::DisableMode(toggled) => self.handle_disable_mode(phone, toggled),
|
||||
Message::MultiUserMode(toggled) => self.handle_multi_user_mode(phone, toggled),
|
||||
Message::ApplyTheme(theme) => self.handle_apply_theme(phone, theme),
|
||||
Message::UrlPressed(url) => Self::handle_url_pressed(url),
|
||||
Message::LoadDeviceSettings => self.handle_load_device_settings(phone),
|
||||
Message::BackupSelected(d_path) => self.handle_backup_selected(d_path),
|
||||
Message::BackupDevice => self.handle_backup_device(phone, packages),
|
||||
Message::DeviceBackedUp(result) => self.handle_device_backed_up(phone, result),
|
||||
Message::RestoreDevice => {
|
||||
self.handle_restore_device(phone, packages, nb_running_async_adb_commands)
|
||||
}
|
||||
Message::RestoringDevice(_) => Task::none(),
|
||||
Message::FolderChosen(result) => self.handle_folder_chosen(phone, result),
|
||||
Message::ChooseBackUpFolder => self.handle_choose_backup_folder(),
|
||||
Message::ExportPackages => Self::handle_export_packages(selected_user, packages),
|
||||
Message::PackagesExported(result) => self.handle_packages_exported(result),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_modal_hide(&mut self) -> Task<Message> {
|
||||
self.modal = None;
|
||||
Task::none()
|
||||
}
|
||||
|
||||
fn handle_expert_mode(&mut self, _phone: &Phone, toggled: bool) -> Task<Message> {
|
||||
self.general.expert_mode = toggled;
|
||||
debug!("Config change: {self:?}");
|
||||
let mut config = Config::load_configuration_file();
|
||||
config.save_device_settings(self.device.clone(), self.general.clone());
|
||||
Task::none()
|
||||
}
|
||||
|
||||
fn handle_disable_mode(&mut self, phone: &Phone, toggled: bool) -> Task<Message> {
|
||||
if supports_disabling(phone) {
|
||||
self.device.disable_mode = toggled;
|
||||
debug!("Config change: {self:?}");
|
||||
let mut config = Config::load_configuration_file();
|
||||
config.save_device_settings(self.device.clone(), self.general.clone());
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
fn handle_multi_user_mode(&mut self, _phone: &Phone, toggled: bool) -> Task<Message> {
|
||||
self.device.multi_user_mode = toggled;
|
||||
debug!("Config change: {self:?}");
|
||||
let mut config = Config::load_configuration_file();
|
||||
config.save_device_settings(self.device.clone(), self.general.clone());
|
||||
Task::none()
|
||||
}
|
||||
|
||||
fn handle_apply_theme(&mut self, _phone: &Phone, theme: Theme) -> Task<Message> {
|
||||
self.general.theme = theme.to_string();
|
||||
debug!("Config change: {self:?}");
|
||||
let mut config = Config::load_configuration_file();
|
||||
config.save_device_settings(self.device.clone(), self.general.clone());
|
||||
Task::none()
|
||||
}
|
||||
|
||||
fn handle_url_pressed(url: PathBuf) -> Task<Message> {
|
||||
open_url(url);
|
||||
Task::none()
|
||||
}
|
||||
|
||||
fn handle_load_device_settings(&mut self, phone: &Phone) -> Task<Message> {
|
||||
self.load_device_settings(phone);
|
||||
Task::none()
|
||||
}
|
||||
|
||||
fn load_device_settings(&mut self, phone: &Phone) {
|
||||
let backups = list_available_backups(&self.general.backup_folder.join(&phone.adb_id));
|
||||
let backup = BackupSettings {
|
||||
backups: backups.clone(),
|
||||
selected: backups.first().cloned(),
|
||||
users: phone.user_list.clone(),
|
||||
selected_user: phone.user_list.first().copied(),
|
||||
backup_state: String::default(),
|
||||
};
|
||||
|
||||
match Config::load_configuration_file()
|
||||
.devices
|
||||
.iter()
|
||||
.find(|d| d.device_id == phone.adb_id)
|
||||
{
|
||||
Some(device) => {
|
||||
self.device.clone_from(device);
|
||||
self.device.backup = backup;
|
||||
}
|
||||
None => {
|
||||
self.device = DeviceSettings {
|
||||
device_id: phone.adb_id.clone(),
|
||||
// see FAQ, and GH-issue #1426
|
||||
disable_mode: supports_disabling(phone),
|
||||
multi_user_mode: supports_multi_user(phone),
|
||||
backup,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_backup_selected(&mut self, d_path: DisplayablePath) -> Task<Message> {
|
||||
self.device.backup.selected = Some(d_path.clone());
|
||||
self.device.backup.users = list_available_backup_user(d_path);
|
||||
Task::none()
|
||||
}
|
||||
|
||||
fn handle_backup_device(
|
||||
&mut self,
|
||||
phone: &Phone,
|
||||
packages: &[Vec<PackageRow>],
|
||||
) -> Task<Message> {
|
||||
let core_packages: Vec<Vec<_>> = packages
|
||||
.iter()
|
||||
.map(|user_packages| user_packages.iter().map(CorePackage::from).collect())
|
||||
.collect();
|
||||
// Avoid capturing &self and &phone in the future; clone owned data first.
|
||||
let user_list = phone.user_list.clone();
|
||||
let device_id = self.device.device_id.clone();
|
||||
let packages_owned = core_packages;
|
||||
Task::perform(
|
||||
async move { backup_phone(user_list, device_id, &packages_owned) },
|
||||
Message::DeviceBackedUp,
|
||||
)
|
||||
}
|
||||
|
||||
fn handle_device_backed_up(
|
||||
&mut self,
|
||||
phone: &Phone,
|
||||
result: Result<bool, String>,
|
||||
) -> Task<Message> {
|
||||
match result {
|
||||
Ok(_) => {
|
||||
info!("[BACKUP] Backup successfully created");
|
||||
self.device.backup.backups =
|
||||
list_available_backups(&self.general.backup_folder.join(phone.adb_id.clone()));
|
||||
self.device.backup.selected = self.device.backup.backups.first().cloned();
|
||||
}
|
||||
Err(err) => {
|
||||
error!("[BACKUP FAILED] Backup creation failed: {err:?}");
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
fn handle_restore_device(
|
||||
&mut self,
|
||||
phone: &Phone,
|
||||
packages: &[Vec<PackageRow>],
|
||||
nb_running_async_adb_commands: &mut u32,
|
||||
) -> Task<Message> {
|
||||
let core_packages: Vec<Vec<_>> = packages
|
||||
.iter()
|
||||
.map(|user_packages| user_packages.iter().map(CorePackage::from).collect())
|
||||
.collect();
|
||||
match restore_backup(phone, &core_packages, &self.device) {
|
||||
Ok(restore_result) => {
|
||||
let mut commands = vec![];
|
||||
*nb_running_async_adb_commands = 0;
|
||||
for p in &restore_result.packages {
|
||||
let p_info = PackageInfo {
|
||||
i_user: p.i_user,
|
||||
index: p.index,
|
||||
removal: "RESTORE".to_string(),
|
||||
before_cross_user_states: vec![],
|
||||
};
|
||||
for command in p.commands.clone() {
|
||||
*nb_running_async_adb_commands += 1;
|
||||
let p_info_clone = p_info.clone();
|
||||
let phone_id = phone.adb_id.clone();
|
||||
commands.push(Task::perform(
|
||||
async move {
|
||||
run_adb_shell_action(phone_id, command.as_str())
|
||||
.map(|_| p_info_clone)
|
||||
},
|
||||
Message::RestoringDevice,
|
||||
));
|
||||
}
|
||||
}
|
||||
if restore_result.skipped_count > 0 {
|
||||
self.device.backup.backup_state = format!(
|
||||
"Restore completed with {} packages skipped (not found on device)",
|
||||
restore_result.skipped_count
|
||||
);
|
||||
} else if restore_result.packages.is_empty() {
|
||||
if get_android_sdk(&phone.adb_id) == 0 {
|
||||
self.device.backup.backup_state = "Device is not connected".to_string();
|
||||
} else {
|
||||
self.device.backup.backup_state =
|
||||
"Device state is already restored".to_string();
|
||||
}
|
||||
} else {
|
||||
self.device.backup.backup_state = "Restore completed successfully".to_string();
|
||||
}
|
||||
info!(
|
||||
"[RESTORE] Restoring backup {}",
|
||||
self.device.backup.selected.as_ref().unwrap()
|
||||
);
|
||||
Task::batch(commands)
|
||||
}
|
||||
Err(e) => {
|
||||
self.device.backup.backup_state.clone_from(&e);
|
||||
error!("{} - {}", self.device.backup.selected.as_ref().unwrap(), e);
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_folder_chosen(
|
||||
&mut self,
|
||||
phone: &Phone,
|
||||
result: Result<PathBuf, Error>,
|
||||
) -> Task<Message> {
|
||||
self.is_loading = false;
|
||||
|
||||
if let Ok(path) = result {
|
||||
self.general.backup_folder = path;
|
||||
let mut config = Config::load_configuration_file();
|
||||
config.save_device_settings(self.device.clone(), self.general.clone());
|
||||
self.load_device_settings(phone);
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
fn handle_choose_backup_folder(&mut self) -> Task<Message> {
|
||||
if self.is_loading {
|
||||
Task::none()
|
||||
} else {
|
||||
self.is_loading = true;
|
||||
Task::perform(open_folder(), Message::FolderChosen)
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_export_packages(
|
||||
selected_user: Option<User>,
|
||||
packages: &[Vec<PackageRow>],
|
||||
) -> Task<Message> {
|
||||
let core_packages: Vec<Vec<_>> = packages
|
||||
.iter()
|
||||
.map(|user_packages| user_packages.iter().map(CorePackage::from).collect())
|
||||
.collect();
|
||||
Task::perform(
|
||||
async move { export_packages(selected_user.unwrap_or_default(), &core_packages) },
|
||||
Message::PackagesExported,
|
||||
)
|
||||
}
|
||||
|
||||
fn handle_packages_exported(&mut self, result: Result<bool, String>) -> Task<Message> {
|
||||
match result {
|
||||
Ok(_) => self.modal = Some(PopUpModal::ExportUninstalled),
|
||||
Err(err) => error!("Failed to export list of uninstalled packages: {err:?}"),
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
pub fn view(
|
||||
&self,
|
||||
phone: &Phone,
|
||||
apps_view: &AppsView,
|
||||
) -> Element<'_, Message, Theme, Renderer> {
|
||||
let content = if phone.adb_id.is_empty() {
|
||||
self.build_no_device_content()
|
||||
} else {
|
||||
self.build_device_content(phone, apps_view)
|
||||
};
|
||||
|
||||
if let Some(PopUpModal::ExportUninstalled) = self.modal {
|
||||
return Self::render_export_modal(content);
|
||||
}
|
||||
|
||||
container(scrollable(content))
|
||||
.padding(10)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn build_no_device_content(&self) -> Element<'_, Message, Theme, Renderer> {
|
||||
column![
|
||||
text("Theme").size(26),
|
||||
self.theme_container(),
|
||||
text("General").size(26),
|
||||
self.general_container(),
|
||||
text("Current device").size(26),
|
||||
Self::no_device_container(),
|
||||
text("Backup / Restore").size(26),
|
||||
Self::no_device_container(),
|
||||
]
|
||||
.width(Length::Fill)
|
||||
.spacing(20)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn build_device_content(
|
||||
&self,
|
||||
phone: &Phone,
|
||||
apps_view: &AppsView,
|
||||
) -> Element<'_, Message, Theme, Renderer> {
|
||||
column![
|
||||
text("Theme").size(26),
|
||||
self.theme_container(),
|
||||
text("General").size(26),
|
||||
self.general_container(),
|
||||
text("Current device").size(26),
|
||||
Self::warning_container(phone),
|
||||
self.device_specific_container(phone),
|
||||
text("Backup / Restore").size(26),
|
||||
self.backup_restore_container(phone, apps_view),
|
||||
]
|
||||
.width(Length::Fill)
|
||||
.spacing(20)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn theme_container(&self) -> Element<'_, Message, Theme, Renderer> {
|
||||
let radio_btn_theme = Theme::ALL
|
||||
.iter()
|
||||
.fold(row![].spacing(10), |column, option| {
|
||||
column.push(
|
||||
radio(
|
||||
format!("{}", option.clone()),
|
||||
*option,
|
||||
Some(string_to_theme(&self.general.theme)),
|
||||
Message::ApplyTheme,
|
||||
)
|
||||
.size(24),
|
||||
)
|
||||
});
|
||||
|
||||
container(radio_btn_theme)
|
||||
.padding(10)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Shrink)
|
||||
.style(style::Container::Frame)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn general_container(&self) -> Element<'_, Message, Theme, Renderer> {
|
||||
let expert_mode_checkbox = checkbox(self.general.expert_mode)
|
||||
.label("Allow to uninstall packages marked as \"unsafe\" (I KNOW WHAT I AM DOING)")
|
||||
.on_toggle(Message::ExpertMode)
|
||||
.size(20)
|
||||
.style(style::CheckBox::SettingsEnabled);
|
||||
|
||||
let expert_mode_descr =
|
||||
text("Most unsafe packages are known to bootloop the device if removed.")
|
||||
.style(style::Text::Commentary);
|
||||
|
||||
let choose_backup_descr = text("Note: If you have previous backups, you will need to transfer them manually to newly changed backup folder to be able to use Restore functionality")
|
||||
.style(style::Text::Commentary);
|
||||
|
||||
let choose_backup_btn = button(text("\u{E930}").font(ICONS))
|
||||
.padding([5, 10])
|
||||
.on_press(Message::ChooseBackUpFolder)
|
||||
.style(style::Button::Primary);
|
||||
|
||||
let choose_backup_row = row![
|
||||
choose_backup_btn,
|
||||
"Choose backup folder",
|
||||
Space::new().width(Length::Fill).height(Length::Shrink),
|
||||
"Current folder: ",
|
||||
text(self.general.backup_folder.to_string_lossy())
|
||||
]
|
||||
.spacing(10)
|
||||
.align_y(Alignment::Center);
|
||||
|
||||
container(
|
||||
column![
|
||||
expert_mode_checkbox,
|
||||
expert_mode_descr,
|
||||
choose_backup_row,
|
||||
choose_backup_descr,
|
||||
]
|
||||
.spacing(10),
|
||||
)
|
||||
.padding(10)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Shrink)
|
||||
.style(style::Container::Frame)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn warning_container(phone: &Phone) -> Element<'static, Message, Theme, Renderer> {
|
||||
container(
|
||||
row![
|
||||
text("The following settings only affect the currently selected device:")
|
||||
.style(style::Text::Danger),
|
||||
text(phone.model.clone()),
|
||||
Space::new().width(Length::Fill).height(Length::Shrink),
|
||||
text(phone.adb_id.clone()).style(style::Text::Commentary)
|
||||
]
|
||||
.spacing(7),
|
||||
)
|
||||
.padding(10)
|
||||
.width(Length::Fill)
|
||||
.style(style::Container::BorderedFrame)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn device_specific_container(&self, phone: &Phone) -> Element<'_, Message, Theme, Renderer> {
|
||||
let multi_user_mode_descr = row![
|
||||
text("This will not affect the following protected work profile users: ")
|
||||
.style(style::Text::Commentary),
|
||||
text(
|
||||
phone
|
||||
.user_list
|
||||
.iter()
|
||||
.filter(|&u| u.protected)
|
||||
.map(|u| u.id.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ")
|
||||
)
|
||||
.style(style::Text::Danger)
|
||||
];
|
||||
|
||||
let multi_user_mode_checkbox = checkbox(self.device.multi_user_mode)
|
||||
.label("Affect all the users of the device (not only the selected user)")
|
||||
.on_toggle(Message::MultiUserMode)
|
||||
.size(20)
|
||||
.style(style::CheckBox::SettingsEnabled);
|
||||
|
||||
let disable_checkbox_style = if supports_disabling(phone) {
|
||||
style::CheckBox::SettingsEnabled
|
||||
} else {
|
||||
style::CheckBox::SettingsDisabled
|
||||
};
|
||||
|
||||
let disable_mode_descr =
|
||||
text("In many cases, it's better to disable a package instead of uninstalling it")
|
||||
.style(style::Text::Commentary);
|
||||
|
||||
let unavailable_btn = button(text("Unavailable").size(14))
|
||||
.on_press(Message::UrlPressed(PathBuf::from(
|
||||
"https://github.com/Universal-Debloater-Alliance/universal-android-debloater/wiki/FAQ#\
|
||||
why-is-the-disable-mode-setting-not-available-for-my-device",
|
||||
)))
|
||||
.height(22)
|
||||
.style(style::Button::Unavailable);
|
||||
|
||||
let disable_mode_checkbox = checkbox(self.device.disable_mode)
|
||||
.label("Clear and disable packages instead of uninstalling them")
|
||||
.on_toggle(Message::DisableMode)
|
||||
.size(20)
|
||||
.style(disable_checkbox_style);
|
||||
|
||||
let disable_setting_row = if supports_disabling(phone) {
|
||||
row![
|
||||
disable_mode_checkbox,
|
||||
Space::new().width(Length::Fill).height(Length::Shrink),
|
||||
]
|
||||
.width(Length::Fill)
|
||||
} else {
|
||||
row![
|
||||
disable_mode_checkbox,
|
||||
Space::new().width(Length::Fill).height(Length::Shrink),
|
||||
unavailable_btn,
|
||||
]
|
||||
.width(Length::Fill)
|
||||
};
|
||||
|
||||
container(
|
||||
column![
|
||||
multi_user_mode_checkbox,
|
||||
multi_user_mode_descr,
|
||||
disable_setting_row,
|
||||
disable_mode_descr,
|
||||
]
|
||||
.spacing(10),
|
||||
)
|
||||
.padding(10)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Shrink)
|
||||
.style(style::Container::Frame)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn backup_restore_container(
|
||||
&self,
|
||||
phone: &Phone,
|
||||
apps_view: &AppsView,
|
||||
) -> Element<'_, Message, Theme, Renderer> {
|
||||
let backup_pick_list = pick_list(
|
||||
self.device.backup.backups.clone(),
|
||||
self.device.backup.selected.clone(),
|
||||
Message::BackupSelected,
|
||||
)
|
||||
.padding(6);
|
||||
|
||||
let backup_btn = button_primary(text("Backup").align_x(alignment::Horizontal::Center))
|
||||
.on_press(Message::BackupDevice)
|
||||
.width(77);
|
||||
|
||||
let restore_btn = |enabled| {
|
||||
if enabled {
|
||||
button(text("Restore").align_x(alignment::Horizontal::Center))
|
||||
.padding([5, 10])
|
||||
.on_press(Message::RestoreDevice)
|
||||
.width(77)
|
||||
} else {
|
||||
button(
|
||||
text("No backup")
|
||||
.align_x(alignment::Horizontal::Center)
|
||||
.align_y(alignment::Vertical::Center),
|
||||
)
|
||||
.padding([5, 10])
|
||||
.width(77)
|
||||
}
|
||||
};
|
||||
|
||||
let locate_backup_btn = if self.device.backup.backups.is_empty() {
|
||||
button_primary("Open backup directory")
|
||||
} else {
|
||||
button_primary("Open backup directory").on_press(Message::UrlPressed(
|
||||
self.general.backup_folder.join(phone.adb_id.clone()),
|
||||
))
|
||||
};
|
||||
|
||||
let export_btn = button_primary("Export").on_press(Message::ExportPackages);
|
||||
|
||||
let backup_row = row![
|
||||
backup_btn,
|
||||
"Backup the current state of the phone",
|
||||
Space::new().width(Length::Fill).height(Length::Shrink),
|
||||
locate_backup_btn,
|
||||
]
|
||||
.spacing(10)
|
||||
.align_y(Alignment::Center);
|
||||
|
||||
let restore_row = if self.device.backup.backups.is_empty() {
|
||||
row![]
|
||||
} else {
|
||||
row![
|
||||
restore_btn(true),
|
||||
"Restore the state of the device",
|
||||
Space::new().width(Length::Fill).height(Length::Shrink),
|
||||
text(self.device.backup.backup_state.clone()).style(style::Text::Danger),
|
||||
backup_pick_list,
|
||||
]
|
||||
.spacing(10)
|
||||
.align_y(Alignment::Center)
|
||||
};
|
||||
|
||||
let export_row = row![
|
||||
export_btn,
|
||||
"Export uninstalled packages with their description",
|
||||
Space::new().width(Length::Fill).height(Length::Shrink),
|
||||
text(format!(
|
||||
"Selected: user {}",
|
||||
apps_view.selected_user.unwrap_or_default().id
|
||||
)),
|
||||
]
|
||||
.spacing(10)
|
||||
.align_y(Alignment::Center);
|
||||
|
||||
container(column![backup_row, restore_row, export_row].spacing(10))
|
||||
.padding(10)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Shrink)
|
||||
.style(style::Container::Frame)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn no_device_container() -> Element<'static, Message, Theme, Renderer> {
|
||||
container(text("No device detected").style(style::Text::Danger))
|
||||
.padding(10)
|
||||
.width(Length::Fill)
|
||||
.style(style::Container::BorderedFrame)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn render_export_modal<'a>(
|
||||
content: Element<'a, Message, Theme, Renderer>,
|
||||
) -> Element<'a, Message, Theme, Renderer> {
|
||||
let title = container(row![text("Success").size(24)].align_y(Alignment::Center))
|
||||
.width(Length::Fill)
|
||||
.style(style::Container::Frame)
|
||||
.padding([10, 0])
|
||||
.center_y(Length::Shrink)
|
||||
.center_x(Length::Shrink);
|
||||
|
||||
let text_box = row![
|
||||
text(format!(
|
||||
"Exported uninstalled packages into file.\nFile is exported in same directory where {NAME} is located."
|
||||
))
|
||||
.width(Length::Fill),
|
||||
]
|
||||
.padding(20);
|
||||
|
||||
let file_row =
|
||||
row![text(generate_backup_name(chrono::Local::now())).style(style::Text::Commentary)]
|
||||
.padding(20);
|
||||
|
||||
let modal_btn_row = row![
|
||||
Space::new().width(Length::Fill).height(Length::Shrink),
|
||||
button(text("Close").width(Length::Shrink))
|
||||
.width(Length::Shrink)
|
||||
.on_press(Message::ModalHide),
|
||||
Space::new().width(Length::Fill).height(Length::Shrink),
|
||||
];
|
||||
|
||||
let ctn = container(column![title, text_box, file_row, modal_btn_row])
|
||||
.height(Length::Shrink)
|
||||
.width(500)
|
||||
.padding(10)
|
||||
.style(style::Container::Frame);
|
||||
|
||||
let padded_content: Element<'a, Message, Theme, Renderer> =
|
||||
container(content).padding(10).into();
|
||||
|
||||
Modal::new(padded_content, ctn)
|
||||
.on_blur(Message::ModalHide)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,6 @@
|
|||
pub mod modal;
|
||||
pub mod navigation_menu;
|
||||
pub mod package_row;
|
||||
|
||||
mod text;
|
||||
pub use text::text;
|
||||
|
|
@ -1,22 +1,20 @@
|
|||
use iced_native::alignment::Alignment;
|
||||
use iced_native::widget::{self, Tree};
|
||||
use iced_native::{
|
||||
event, layout, mouse, overlay, renderer, Clipboard, Color, Element, Event, Layout, Length,
|
||||
Point, Rectangle, Shell, Size, Widget,
|
||||
};
|
||||
use iced::advanced::widget::{self, Tree, Widget};
|
||||
use iced::advanced::{Clipboard, Layout, Shell, layout, overlay, renderer};
|
||||
use iced::mouse::{self, Cursor};
|
||||
use iced::{Alignment, Color, Element, Event, Length, Point, Rectangle, Size, Vector, advanced};
|
||||
|
||||
/// A widget that centers a modal element over some base element
|
||||
pub struct Modal<'a, Message, Renderer> {
|
||||
base: Element<'a, Message, Renderer>,
|
||||
modal: Element<'a, Message, Renderer>,
|
||||
pub struct Modal<'a, Message, Theme, Renderer> {
|
||||
base: Element<'a, Message, Theme, Renderer>,
|
||||
modal: Element<'a, Message, Theme, Renderer>,
|
||||
on_blur: Option<Message>,
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Modal<'a, Message, Renderer> {
|
||||
impl<'a, Message, Theme, Renderer> Modal<'a, Message, Theme, Renderer> {
|
||||
/// Returns a new [`Modal`]
|
||||
pub fn new(
|
||||
base: impl Into<Element<'a, Message, Renderer>>,
|
||||
modal: impl Into<Element<'a, Message, Renderer>>,
|
||||
base: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||
modal: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
base: base.into(),
|
||||
|
|
@ -25,9 +23,9 @@ impl<'a, Message, Renderer> Modal<'a, Message, Renderer> {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::missing_const_for_fn)]
|
||||
/// Sets the message that will be produces when the background
|
||||
/// of the [`Modal`] is pressed
|
||||
#[must_use]
|
||||
pub fn on_blur(self, on_blur: Message) -> Self {
|
||||
Self {
|
||||
on_blur: Some(on_blur),
|
||||
|
|
@ -36,42 +34,47 @@ impl<'a, Message, Renderer> Modal<'a, Message, Renderer> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Widget<Message, Renderer> for Modal<'a, Message, Renderer>
|
||||
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Modal<'_, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: iced_native::Renderer,
|
||||
Renderer: advanced::Renderer,
|
||||
Message: Clone,
|
||||
{
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
vec![Tree::new(&self.base), Tree::new(&self.modal)]
|
||||
}
|
||||
|
||||
fn size(&self) -> Size<Length> {
|
||||
self.base.as_widget().size()
|
||||
}
|
||||
|
||||
fn diff(&self, tree: &mut Tree) {
|
||||
tree.diff_children(&[&self.base, &self.modal]);
|
||||
}
|
||||
|
||||
fn width(&self) -> Length {
|
||||
self.base.as_widget().width()
|
||||
fn layout(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
self.base
|
||||
.as_widget_mut()
|
||||
.layout(&mut tree.children[0], renderer, limits)
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
self.base.as_widget().height()
|
||||
}
|
||||
|
||||
fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node {
|
||||
self.base.as_widget().layout(renderer, limits)
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
fn update(
|
||||
&mut self,
|
||||
state: &mut Tree,
|
||||
event: Event,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
cursor_position: Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
self.base.as_widget_mut().on_event(
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
self.base.as_widget_mut().update(
|
||||
&mut state.children[0],
|
||||
event,
|
||||
layout,
|
||||
|
|
@ -79,17 +82,18 @@ where
|
|||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
)
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
state: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &<Renderer as iced_native::Renderer>::Theme,
|
||||
theme: &Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
cursor: Cursor,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
self.base.as_widget().draw(
|
||||
|
|
@ -98,7 +102,7 @@ where
|
|||
theme,
|
||||
style,
|
||||
layout,
|
||||
cursor_position,
|
||||
cursor,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
|
|
@ -106,123 +110,131 @@ where
|
|||
fn overlay<'b>(
|
||||
&'b mut self,
|
||||
state: &'b mut Tree,
|
||||
layout: Layout<'_>,
|
||||
layout: Layout<'b>,
|
||||
_renderer: &Renderer,
|
||||
) -> Option<overlay::Element<'b, Message, Renderer>> {
|
||||
Some(overlay::Element::new(
|
||||
layout.position(),
|
||||
Box::new(Overlay {
|
||||
content: &mut self.modal,
|
||||
tree: &mut state.children[1],
|
||||
size: layout.bounds().size(),
|
||||
on_blur: self.on_blur.clone(),
|
||||
}),
|
||||
))
|
||||
viewport: &Rectangle,
|
||||
_translation: Vector,
|
||||
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||
Some(overlay::Element::new(Box::new(Overlay {
|
||||
position: layout.position(),
|
||||
content: &mut self.modal,
|
||||
tree: &mut state.children[1],
|
||||
size: layout.bounds().size(),
|
||||
on_blur: self.on_blur.clone(),
|
||||
viewport: *viewport,
|
||||
})))
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
state: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
cursor: Cursor,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.base.as_widget().mouse_interaction(
|
||||
&state.children[0],
|
||||
layout,
|
||||
cursor_position,
|
||||
cursor,
|
||||
viewport,
|
||||
renderer,
|
||||
)
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&self,
|
||||
&mut self,
|
||||
state: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation<Message>,
|
||||
operation: &mut dyn widget::Operation,
|
||||
) {
|
||||
self.base
|
||||
.as_widget()
|
||||
.as_widget_mut()
|
||||
.operate(&mut state.children[0], layout, renderer, operation);
|
||||
}
|
||||
}
|
||||
|
||||
struct Overlay<'a, 'b, Message, Renderer> {
|
||||
content: &'b mut Element<'a, Message, Renderer>,
|
||||
struct Overlay<'a, 'b, Message, Theme, Renderer> {
|
||||
position: Point,
|
||||
content: &'b mut Element<'a, Message, Theme, Renderer>,
|
||||
tree: &'b mut Tree,
|
||||
size: Size,
|
||||
on_blur: Option<Message>,
|
||||
viewport: Rectangle,
|
||||
}
|
||||
|
||||
impl<'a, 'b, Message, Renderer> overlay::Overlay<Message, Renderer>
|
||||
for Overlay<'a, 'b, Message, Renderer>
|
||||
impl<Message, Theme, Renderer> overlay::Overlay<Message, Theme, Renderer>
|
||||
for Overlay<'_, '_, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: iced_native::Renderer,
|
||||
Renderer: advanced::Renderer,
|
||||
Message: Clone,
|
||||
{
|
||||
fn layout(&self, renderer: &Renderer, _bounds: Size, position: Point) -> layout::Node {
|
||||
fn layout(&mut self, renderer: &Renderer, _bounds: Size) -> layout::Node {
|
||||
let limits = layout::Limits::new(Size::ZERO, self.size)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill);
|
||||
|
||||
let mut child = self.content.as_widget().layout(renderer, &limits);
|
||||
child.align(Alignment::Center, Alignment::Center, limits.max());
|
||||
let child = self
|
||||
.content
|
||||
.as_widget_mut()
|
||||
.layout(self.tree, renderer, &limits)
|
||||
.align(Alignment::Center, Alignment::Center, limits.max());
|
||||
|
||||
let mut node = layout::Node::with_children(self.size, vec![child]);
|
||||
node.move_to(position);
|
||||
|
||||
node
|
||||
layout::Node::with_children(self.size, vec![child]).move_to(self.position)
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
fn update(
|
||||
&mut self,
|
||||
event: Event,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
cursor: Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
let content_bounds = layout.children().next().unwrap().bounds();
|
||||
|
||||
#[allow(clippy::equatable_if_let)]
|
||||
if let Some(message) = self.on_blur.as_ref() {
|
||||
if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) = &event {
|
||||
if !content_bounds.contains(cursor_position) {
|
||||
shell.publish(message.clone());
|
||||
return event::Status::Captured;
|
||||
}
|
||||
) {
|
||||
if let Some(message) = self.on_blur.as_ref()
|
||||
&& matches!(
|
||||
event,
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||
)
|
||||
&& let Some(cursor_position) = cursor.position()
|
||||
{
|
||||
let content_bounds = layout
|
||||
.children()
|
||||
.next()
|
||||
.expect("Layout must have at least 1 child")
|
||||
.bounds();
|
||||
if !content_bounds.contains(cursor_position) {
|
||||
shell.publish(message.clone());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.content.as_widget_mut().on_event(
|
||||
self.content.as_widget_mut().update(
|
||||
self.tree,
|
||||
event,
|
||||
layout.children().next().unwrap(),
|
||||
cursor_position,
|
||||
cursor,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
)
|
||||
&self.viewport,
|
||||
);
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
theme: &Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
cursor: Cursor,
|
||||
) {
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: layout.bounds(),
|
||||
border_radius: renderer::BorderRadius::from(0.0),
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
Color {
|
||||
a: 0.80,
|
||||
|
|
@ -236,7 +248,7 @@ where
|
|||
theme,
|
||||
style,
|
||||
layout.children().next().unwrap(),
|
||||
cursor_position,
|
||||
cursor,
|
||||
&layout.bounds(),
|
||||
);
|
||||
}
|
||||
|
|
@ -245,9 +257,9 @@ where
|
|||
&mut self,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation<Message>,
|
||||
operation: &mut dyn widget::Operation,
|
||||
) {
|
||||
self.content.as_widget().operate(
|
||||
self.content.as_widget_mut().operate(
|
||||
self.tree,
|
||||
layout.children().next().unwrap(),
|
||||
renderer,
|
||||
|
|
@ -258,26 +270,27 @@ where
|
|||
fn mouse_interaction(
|
||||
&self,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: Point,
|
||||
viewport: &Rectangle,
|
||||
cursor: Cursor,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.content.as_widget().mouse_interaction(
|
||||
self.tree,
|
||||
layout.children().next().unwrap(),
|
||||
cursor_position,
|
||||
viewport,
|
||||
cursor,
|
||||
&self.viewport,
|
||||
renderer,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> From<Modal<'a, Message, Renderer>> for Element<'a, Message, Renderer>
|
||||
impl<'a, Message, Theme, Renderer> From<Modal<'a, Message, Theme, Renderer>>
|
||||
for Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: 'a + iced_native::Renderer,
|
||||
Theme: 'a,
|
||||
Renderer: 'a + advanced::Renderer,
|
||||
Message: 'a + Clone,
|
||||
{
|
||||
fn from(modal: Modal<'a, Message, Renderer>) -> Self {
|
||||
fn from(modal: Modal<'a, Message, Theme, Renderer>) -> Self {
|
||||
Element::new(modal)
|
||||
}
|
||||
}
|
||||
141
crates/uad-gui/src/widgets/navigation_menu.rs
Normal file
141
crates/uad-gui/src/widgets/navigation_menu.rs
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
use crate::helpers::button_primary;
|
||||
use crate::theme::Theme;
|
||||
pub use crate::views::about::Message as AboutMessage;
|
||||
pub use crate::views::list::{List as AppsView, LoadingState as ListLoadingState};
|
||||
use crate::{gui::Message, style, widgets::text};
|
||||
use iced::widget::{Space, button, container, pick_list, row, tooltip};
|
||||
use iced::{Alignment, Element, Font, Length, Renderer, alignment, font};
|
||||
pub use uad_core::sync::Phone;
|
||||
use uad_core::update::{SelfUpdateState, SelfUpdateStatus};
|
||||
/// resources/assets/icons.ttf, loaded in [`crate::gui::UadGui`]
|
||||
pub const ICONS: Font = Font {
|
||||
family: font::Family::Name("icomoon"),
|
||||
..Font::DEFAULT
|
||||
};
|
||||
|
||||
#[allow(
|
||||
clippy::too_many_lines,
|
||||
reason = "Navigation layout composes many widgets inline"
|
||||
)]
|
||||
pub fn nav_menu<'a>(
|
||||
device_list: &'a [Phone],
|
||||
selected_device: Option<Phone>,
|
||||
apps_view: &AppsView,
|
||||
self_update_state: &SelfUpdateState,
|
||||
) -> Element<'a, Message, Theme, Renderer> {
|
||||
let apps_refresh_btn = button_primary(
|
||||
text("\u{E900}")
|
||||
.font(ICONS)
|
||||
.width(22)
|
||||
.align_x(alignment::Horizontal::Center),
|
||||
)
|
||||
.on_press(Message::RefreshButtonPressed);
|
||||
|
||||
let apps_refresh_tooltip = tooltip(
|
||||
apps_refresh_btn,
|
||||
"Refresh apps (Ctrl+Shift+5)",
|
||||
tooltip::Position::Bottom,
|
||||
)
|
||||
.style(style::Container::Tooltip)
|
||||
.gap(4);
|
||||
|
||||
let reboot_btn = button_primary("Reboot").on_press(Message::RebootButtonPressed);
|
||||
|
||||
let reboot_btn = tooltip(
|
||||
reboot_btn,
|
||||
"Reboot device (Ctrl+Shift+R)",
|
||||
tooltip::Position::Bottom,
|
||||
)
|
||||
.style(style::Container::Tooltip)
|
||||
.gap(4);
|
||||
|
||||
let uad_version_text = if let Some(r) = &self_update_state.latest_release {
|
||||
match self_update_state.status {
|
||||
SelfUpdateStatus::Failed => text(format!("Failed to update to {}", r.tag_name)),
|
||||
SelfUpdateStatus::Checking => text(SelfUpdateStatus::Checking.to_string()),
|
||||
SelfUpdateStatus::Done => text(format!(
|
||||
"Update available: {} -> {}",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
r.tag_name
|
||||
)),
|
||||
SelfUpdateStatus::Updating => text("Updating please wait..."),
|
||||
}
|
||||
} else {
|
||||
text(format!("v{}", env!("CARGO_PKG_VERSION")))
|
||||
};
|
||||
|
||||
let update_btn = if self_update_state.latest_release.is_some() {
|
||||
button("Update")
|
||||
.on_press(Message::AboutAction(AboutMessage::DoSelfUpdate))
|
||||
.padding([5, 10])
|
||||
.style(style::Button::SelfUpdate)
|
||||
} else {
|
||||
button("").height(0).width(0).style(style::Button::Hidden)
|
||||
};
|
||||
|
||||
let apps_btn = button_primary("Apps").on_press(Message::AppsPress);
|
||||
|
||||
let apps_btn = tooltip(apps_btn, "(Ctrl+Shift+A)", tooltip::Position::Bottom)
|
||||
.style(style::Container::Tooltip)
|
||||
.gap(4);
|
||||
|
||||
let about_btn = button_primary("About").on_press(Message::AboutPressed);
|
||||
|
||||
let about_btn = tooltip(about_btn, "(Ctrl+Shift+I)", tooltip::Position::Bottom)
|
||||
.style(style::Container::Tooltip)
|
||||
.gap(4);
|
||||
|
||||
let settings_btn = button_primary(
|
||||
text("\u{E994}")
|
||||
.font(ICONS)
|
||||
.width(22)
|
||||
.align_x(alignment::Horizontal::Center),
|
||||
)
|
||||
.on_press(Message::SettingsPressed);
|
||||
|
||||
let settings_btn = tooltip(settings_btn, "(Ctrl+Shift+S)", tooltip::Position::Bottom)
|
||||
.style(style::Container::Tooltip)
|
||||
.gap(4);
|
||||
|
||||
let device_list_text = match apps_view.loading_state {
|
||||
ListLoadingState::FindingPhones => text("Finding connected devices..."),
|
||||
_ => text("No devices/emulators found"),
|
||||
};
|
||||
|
||||
let row = match selected_device {
|
||||
Some(phone) => row![
|
||||
reboot_btn,
|
||||
apps_refresh_tooltip,
|
||||
pick_list(device_list, Some(phone), Message::DeviceSelected,),
|
||||
Space::new().width(Length::Fill).height(Length::Shrink),
|
||||
uad_version_text,
|
||||
update_btn,
|
||||
apps_btn,
|
||||
about_btn,
|
||||
settings_btn,
|
||||
]
|
||||
.width(Length::Fill)
|
||||
.align_y(Alignment::Center)
|
||||
.spacing(10),
|
||||
None => row![
|
||||
reboot_btn,
|
||||
apps_refresh_tooltip,
|
||||
device_list_text,
|
||||
Space::new().width(Length::Fill).height(Length::Shrink),
|
||||
uad_version_text,
|
||||
update_btn,
|
||||
apps_btn,
|
||||
about_btn,
|
||||
settings_btn,
|
||||
]
|
||||
.width(Length::Fill)
|
||||
.align_y(Alignment::Center)
|
||||
.spacing(10),
|
||||
};
|
||||
|
||||
container(row)
|
||||
.width(Length::Fill)
|
||||
.padding(10)
|
||||
.style(style::Container::Frame)
|
||||
.into()
|
||||
}
|
||||
|
|
@ -1,19 +1,21 @@
|
|||
use crate::core::sync::Phone;
|
||||
use crate::core::theme::Theme;
|
||||
use crate::core::uad_lists::{PackageState, Removal, UadList};
|
||||
use crate::gui::style;
|
||||
use crate::gui::views::settings::Settings;
|
||||
use crate::style;
|
||||
use crate::theme::Theme;
|
||||
use crate::views::settings::Settings;
|
||||
use crate::widgets::text;
|
||||
use log::warn;
|
||||
use uad_core::sync::{CorePackage, Phone};
|
||||
use uad_core::uad_lists::{PackageState, Removal, UadList};
|
||||
|
||||
use iced::widget::{button, checkbox, row, text, Space};
|
||||
use iced::{alignment, Alignment, Command, Element, Length, Renderer};
|
||||
use iced::widget::{Space, button, checkbox, row};
|
||||
use iced::{Alignment, Element, Length, Renderer, Task, alignment};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PackageRow {
|
||||
pub name: String,
|
||||
pub state: PackageState,
|
||||
pub description: String,
|
||||
pub uad_list: UadList,
|
||||
pub removal: Removal,
|
||||
pub state: PackageState,
|
||||
pub list: UadList,
|
||||
pub selected: bool,
|
||||
pub current: bool,
|
||||
}
|
||||
|
|
@ -26,34 +28,43 @@ pub enum Message {
|
|||
}
|
||||
|
||||
impl PackageRow {
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
name: &str,
|
||||
state: PackageState,
|
||||
description: &str,
|
||||
uad_list: UadList,
|
||||
removal: Removal,
|
||||
state: PackageState,
|
||||
list: UadList,
|
||||
selected: bool,
|
||||
current: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
state,
|
||||
description: description.to_string(),
|
||||
uad_list,
|
||||
removal,
|
||||
state,
|
||||
list,
|
||||
selected,
|
||||
current,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, _message: &Message) -> Command<Message> {
|
||||
Command::none()
|
||||
#[allow(
|
||||
clippy::unused_self,
|
||||
reason = "Consistent component API; may change later"
|
||||
)]
|
||||
pub fn update(&mut self, _message: &Message) -> Task<Message> {
|
||||
Task::none()
|
||||
}
|
||||
|
||||
pub fn view(&self, settings: &Settings, _phone: &Phone) -> Element<Message, Renderer<Theme>> {
|
||||
pub fn view(
|
||||
&self,
|
||||
settings: &Settings,
|
||||
_phone: &Phone,
|
||||
) -> Element<'_, Message, Theme, Renderer> {
|
||||
//let trash_svg = format!("{}/resources/assets/trash.svg", env!("CARGO_MANIFEST_DIR"));
|
||||
//let restore_svg = format!("{}/resources/assets/rotate.svg", env!("CARGO_MANIFEST_DIR"));
|
||||
let button_style;
|
||||
let button_style: fn(&Theme, iced::widget::button::Status) -> iced::widget::button::Style;
|
||||
let action_text;
|
||||
let action_btn;
|
||||
let selection_checkbox;
|
||||
|
|
@ -86,22 +97,26 @@ impl PackageRow {
|
|||
|| self.state != PackageState::Enabled
|
||||
|| settings.general.expert_mode
|
||||
{
|
||||
selection_checkbox = checkbox("", self.selected, Message::ToggleSelection)
|
||||
selection_checkbox = checkbox(self.selected)
|
||||
.on_toggle(Message::ToggleSelection)
|
||||
.size(20)
|
||||
.style(style::CheckBox::PackageEnabled);
|
||||
|
||||
action_btn = button(
|
||||
text(action_text)
|
||||
.horizontal_alignment(alignment::Horizontal::Center)
|
||||
.align_x(alignment::Horizontal::Center)
|
||||
.width(100),
|
||||
)
|
||||
.on_press(Message::ActionPressed);
|
||||
} else {
|
||||
selection_checkbox = checkbox("", self.selected, Message::ToggleSelection)
|
||||
selection_checkbox = checkbox(self.selected)
|
||||
.on_toggle(Message::ToggleSelection)
|
||||
.size(20)
|
||||
.style(style::CheckBox::PackageDisabled);
|
||||
|
||||
action_btn = button(
|
||||
text(action_text)
|
||||
.horizontal_alignment(alignment::Horizontal::Center)
|
||||
.align_x(alignment::Horizontal::Center)
|
||||
.width(100),
|
||||
);
|
||||
}
|
||||
|
|
@ -113,7 +128,8 @@ impl PackageRow {
|
|||
text(&self.name).width(Length::FillPortion(8)),
|
||||
action_btn.style(button_style)
|
||||
]
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(8)
|
||||
.align_y(Alignment::Center)
|
||||
)
|
||||
.padding(8)
|
||||
.style(if self.current {
|
||||
|
|
@ -123,9 +139,62 @@ impl PackageRow {
|
|||
})
|
||||
.width(Length::Fill)
|
||||
.on_press(Message::PackagePressed),
|
||||
Space::with_width(15)
|
||||
Space::new().width(Length::Fixed(15.0))
|
||||
]
|
||||
.align_items(Alignment::Center)
|
||||
.align_y(Alignment::Center)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
// Conversions between PackageRow and CorePackage
|
||||
impl From<CorePackage> for PackageRow {
|
||||
fn from(core: CorePackage) -> Self {
|
||||
Self {
|
||||
name: core.name.clone(),
|
||||
description: core.description,
|
||||
removal: core.removal,
|
||||
state: core.state,
|
||||
list: core.list,
|
||||
selected: false, // Default to not selected
|
||||
current: false, // Default to not current
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&CorePackage> for PackageRow {
|
||||
fn from(core: &CorePackage) -> Self {
|
||||
Self {
|
||||
name: core.name.clone(),
|
||||
description: core.description.clone(),
|
||||
removal: core.removal,
|
||||
state: core.state,
|
||||
list: core.list,
|
||||
selected: false,
|
||||
current: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&PackageRow> for CorePackage {
|
||||
fn from(row: &PackageRow) -> Self {
|
||||
Self {
|
||||
name: row.name.clone(),
|
||||
description: row.description.clone(),
|
||||
removal: row.removal,
|
||||
state: row.state,
|
||||
list: row.list,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PackageRow> for CorePackage {
|
||||
fn from(row: PackageRow) -> Self {
|
||||
Self {
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
removal: row.removal,
|
||||
state: row.state,
|
||||
list: row.list,
|
||||
}
|
||||
}
|
||||
}
|
||||
13
crates/uad-gui/src/widgets/text.rs
Normal file
13
crates/uad-gui/src/widgets/text.rs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
#![allow(
|
||||
clippy::disallowed_types,
|
||||
reason = "this is the replacement that enforces advanced shaping for disallowed [`iced::widget::Text`]"
|
||||
)]
|
||||
|
||||
use crate::theme::Theme;
|
||||
use iced::advanced::text::IntoFragment;
|
||||
use iced::widget;
|
||||
|
||||
// Creates a new Text widget with advanced shaping.
|
||||
pub fn text<'a>(text: impl IntoFragment<'a>) -> widget::Text<'a, Theme> {
|
||||
widget::Text::new(text).shaping(iced::widget::text::Shaping::Advanced)
|
||||
}
|
||||
27
flake.lock
generated
Normal file
27
flake.lock
generated
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1776949667,
|
||||
"narHash": "sha256-GMSVw35Q+294GlrTUKlx087E31z7KurReQ1YHSKp5iw=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "01fbdeef22b76df85ea168fbfe1bfd9e63681b30",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
69
flake.nix
Normal file
69
flake.nix
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
{
|
||||
description = "devshell for uad-ng";
|
||||
|
||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
|
||||
outputs =
|
||||
{ nixpkgs, ... }:
|
||||
let
|
||||
forSystems =
|
||||
f:
|
||||
nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed (
|
||||
system: f (import nixpkgs { inherit system; })
|
||||
);
|
||||
in
|
||||
{
|
||||
formatter = forSystems (pkgs: pkgs.nixfmt-tree);
|
||||
|
||||
devShells = forSystems (
|
||||
pkgs:
|
||||
let
|
||||
inherit (pkgs) lib stdenv;
|
||||
runtimeLibs = [
|
||||
pkgs.fontconfig
|
||||
pkgs.freetype
|
||||
]
|
||||
++ lib.optionals stdenv.isLinux (
|
||||
with pkgs;
|
||||
[
|
||||
libglvnd
|
||||
libX11
|
||||
libXcursor
|
||||
libXi
|
||||
libXrandr
|
||||
libxkbcommon
|
||||
wayland
|
||||
]
|
||||
);
|
||||
in
|
||||
{
|
||||
default = pkgs.mkShell (
|
||||
{
|
||||
nativeBuildInputs = with pkgs; [
|
||||
clang
|
||||
pkg-config
|
||||
];
|
||||
buildInputs =
|
||||
with pkgs;
|
||||
[
|
||||
rustc
|
||||
cargo
|
||||
rustfmt
|
||||
rust-analyzer
|
||||
clippy
|
||||
android-tools
|
||||
]
|
||||
++ runtimeLibs;
|
||||
|
||||
RUST_SRC_PATH = "${pkgs.rustPlatform.rustLibSrc}/lib/rustlib/src/rust";
|
||||
LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib";
|
||||
}
|
||||
// lib.optionalAttrs stdenv.isLinux { LD_LIBRARY_PATH = lib.makeLibraryPath runtimeLibs; }
|
||||
// lib.optionalAttrs stdenv.isDarwin {
|
||||
DYLD_FALLBACK_LIBRARY_PATH = lib.makeLibraryPath runtimeLibs;
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
BIN
resources/assets/icon.ico
Normal file
BIN
resources/assets/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 401 KiB |
|
|
@ -416,8 +416,11 @@
|
|||
{
|
||||
"ligatures": "folder-open, directory2",
|
||||
"name": "folder-open",
|
||||
"order": 0,
|
||||
"id": 49
|
||||
"order": 4,
|
||||
"id": 49,
|
||||
"prevSize": 28,
|
||||
"code": 59696,
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"ligatures": "folder-plus, directory3",
|
||||
|
|
@ -1016,8 +1019,11 @@
|
|||
{
|
||||
"ligatures": "cog, gear",
|
||||
"name": "cog",
|
||||
"order": 0,
|
||||
"id": 149
|
||||
"order": 3,
|
||||
"id": 149,
|
||||
"prevSize": 28,
|
||||
"code": 59796,
|
||||
"tempChar": ""
|
||||
},
|
||||
{
|
||||
"ligatures": "cogs, gears",
|
||||
|
|
|
|||
Binary file not shown.
BIN
resources/assets/logo-dark.png
Normal file
BIN
resources/assets/logo-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
resources/assets/logo-light.png
Normal file
BIN
resources/assets/logo-light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
1
resources/assets/manifest.rc
Normal file
1
resources/assets/manifest.rc
Normal file
|
|
@ -0,0 +1 @@
|
|||
MAINICON ICON "icon.ico"
|
||||
File diff suppressed because it is too large
Load diff
1
resources/images/icon_clyde_blurple.svg
Normal file
1
resources/images/icon_clyde_blurple.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?><svg id="Discord-Logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 126.644 96"><defs><style>.cls-1{fill:#5865f2;}</style></defs><path id="Discord-Symbol-Blurple" class="cls-1" d="M81.15,0c-1.2376,2.1973-2.3489,4.4704-3.3591,6.794-9.5975-1.4396-19.3718-1.4396-28.9945,0-.985-2.3236-2.1216-4.5967-3.3591-6.794-9.0166,1.5407-17.8059,4.2431-26.1405,8.0568C2.779,32.5304-1.6914,56.3725.5312,79.8863c9.6732,7.1476,20.5083,12.603,32.0505,16.0884,2.6014-3.4854,4.8998-7.1981,6.8698-11.0623-3.738-1.3891-7.3497-3.1318-10.8098-5.1523.9092-.6567,1.7932-1.3386,2.6519-1.9953,20.281,9.547,43.7696,9.547,64.0758,0,.8587.7072,1.7427,1.3891,2.6519,1.9953-3.4601,2.0457-7.0718,3.7632-10.835,5.1776,1.97,3.8642,4.2683,7.5769,6.8698,11.0623,11.5419-3.4854,22.3769-8.9156,32.0509-16.0631,2.626-27.2771-4.496-50.9172-18.817-71.8548C98.9811,4.2684,90.1918,1.5659,81.1752.0505l-.0252-.0505ZM42.2802,65.4144c-6.2383,0-11.4159-5.6575-11.4159-12.6535s4.9755-12.6788,11.3907-12.6788,11.5169,5.708,11.4159,12.6788c-.101,6.9708-5.026,12.6535-11.3907,12.6535ZM84.3576,65.4144c-6.2637,0-11.3907-5.6575-11.3907-12.6535s4.9755-12.6788,11.3907-12.6788,11.4917,5.708,11.3906,12.6788c-.101,6.9708-5.026,12.6535-11.3906,12.6535Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
BIN
resources/screenshots/v0.6.2.png
Normal file
BIN
resources/screenshots/v0.6.2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 171 KiB |
BIN
resources/screenshots/v1.0.2.png
Normal file
BIN
resources/screenshots/v1.0.2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
|
|
@ -1,86 +0,0 @@
|
|||
use crate::core::sync::{get_android_sdk, User};
|
||||
use crate::core::utils::DisplayablePath;
|
||||
use crate::gui::views::settings::Settings;
|
||||
use crate::CONFIG_DIR;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use static_init::dynamic;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Config {
|
||||
pub general: GeneralSettings,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
|
||||
pub devices: Vec<DeviceSettings>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct GeneralSettings {
|
||||
pub theme: String,
|
||||
pub expert_mode: bool,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct BackupSettings {
|
||||
pub backups: Vec<DisplayablePath>,
|
||||
pub selected: Option<DisplayablePath>,
|
||||
pub users: Vec<User>,
|
||||
pub selected_user: Option<User>,
|
||||
pub backup_state: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct DeviceSettings {
|
||||
pub device_id: String,
|
||||
pub disable_mode: bool,
|
||||
pub multi_user_mode: bool,
|
||||
#[serde(skip)]
|
||||
pub backup: BackupSettings,
|
||||
}
|
||||
|
||||
impl Default for DeviceSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
device_id: String::new(),
|
||||
multi_user_mode: get_android_sdk() > 21,
|
||||
disable_mode: false,
|
||||
backup: BackupSettings::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[dynamic]
|
||||
static CONFIG_FILE: PathBuf = CONFIG_DIR.join("config.toml");
|
||||
|
||||
impl Config {
|
||||
pub fn save_changes(settings: &Settings, device_id: &String) {
|
||||
let mut config = Self::load_configuration_file();
|
||||
if let Some(device) = config
|
||||
.devices
|
||||
.iter_mut()
|
||||
.find(|x| x.device_id == *device_id)
|
||||
{
|
||||
*device = settings.device.clone();
|
||||
} else {
|
||||
debug!("config: New device settings saved");
|
||||
config.devices.push(settings.device.clone());
|
||||
}
|
||||
config.general = settings.general.clone();
|
||||
let toml = toml::to_string(&config).unwrap();
|
||||
fs::write(&*CONFIG_FILE, toml).expect("Could not write config file to disk!");
|
||||
}
|
||||
|
||||
pub fn load_configuration_file() -> Self {
|
||||
match fs::read_to_string(&*CONFIG_FILE) {
|
||||
Ok(s) => match toml::from_str(&s) {
|
||||
Ok(config) => return config,
|
||||
Err(e) => error!("Invalid config file: `{}`", e),
|
||||
},
|
||||
Err(e) => error!("Failed to read config file: `{}`", e),
|
||||
}
|
||||
error!("Restoring default config file");
|
||||
let toml = toml::to_string(&Self::default()).unwrap();
|
||||
fs::write(&*CONFIG_FILE, toml).expect("Could not write config file to disk!");
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
pub mod config;
|
||||
pub mod save;
|
||||
pub mod sync;
|
||||
pub mod theme;
|
||||
pub mod uad_lists;
|
||||
pub mod update;
|
||||
pub mod utils;
|
||||
336
src/core/sync.rs
336
src/core/sync.rs
|
|
@ -1,336 +0,0 @@
|
|||
use crate::core::uad_lists::PackageState;
|
||||
use crate::gui::views::list::PackageInfo;
|
||||
use crate::gui::widgets::package_row::PackageRow;
|
||||
use regex::Regex;
|
||||
use retry::{delay::Fixed, retry, OperationResult};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use static_init::dynamic;
|
||||
use std::collections::HashSet;
|
||||
use std::env;
|
||||
use std::process::Command;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::os::windows::process::CommandExt;
|
||||
|
||||
#[dynamic]
|
||||
static RE: Regex = Regex::new(r"\n(\S+)\s+device").unwrap();
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Phone {
|
||||
pub model: String,
|
||||
pub android_sdk: u8,
|
||||
pub user_list: Vec<User>,
|
||||
pub adb_id: String,
|
||||
}
|
||||
|
||||
impl Default for Phone {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
model: "fetching devices...".to_string(),
|
||||
android_sdk: 0,
|
||||
user_list: vec![],
|
||||
adb_id: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Phone {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.model)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Copy)]
|
||||
pub struct User {
|
||||
pub id: u16,
|
||||
pub index: usize,
|
||||
pub protected: bool,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for User {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "user {}", self.id)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn adb_shell_command(shell: bool, args: &str) -> Result<String, String> {
|
||||
let adb_command = if shell {
|
||||
vec!["shell", args]
|
||||
} else {
|
||||
vec![args]
|
||||
};
|
||||
|
||||
let mut command = Command::new("adb");
|
||||
command.args(adb_command);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let command = command.creation_flags(0x08000000); // do not open a cmd window
|
||||
|
||||
match command.output() {
|
||||
Err(e) => {
|
||||
error!("ADB: {}", e);
|
||||
Err("ADB was not found".to_string())
|
||||
}
|
||||
Ok(o) => {
|
||||
if o.status.success() {
|
||||
Ok(String::from_utf8(o.stdout)
|
||||
.map_err(|e| e.to_string())?
|
||||
.trim_end()
|
||||
.to_string())
|
||||
} else {
|
||||
let stdout = String::from_utf8(o.stdout)
|
||||
.map_err(|e| e.to_string())?
|
||||
.trim_end()
|
||||
.to_string();
|
||||
let stderr = String::from_utf8(o.stderr)
|
||||
.map_err(|e| e.to_string())?
|
||||
.trim_end()
|
||||
.to_string();
|
||||
|
||||
// ADB does really weird things. Some errors are not redirected to stderr
|
||||
let err = if stdout.is_empty() { stderr } else { stdout };
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum CommandType {
|
||||
PackageManager(PackageInfo),
|
||||
Shell,
|
||||
}
|
||||
pub async fn perform_adb_commands(
|
||||
action: String,
|
||||
command_type: CommandType,
|
||||
) -> Result<CommandType, ()> {
|
||||
let label = match command_type {
|
||||
CommandType::PackageManager(ref p) => p.removal.to_string(),
|
||||
CommandType::Shell => "Shell".to_string(),
|
||||
};
|
||||
|
||||
match adb_shell_command(true, &action) {
|
||||
Ok(o) => {
|
||||
// On old devices, adb commands can return the '0' exit code even if there
|
||||
// is an error. On Android 4.4, ADB doesn't check if the package exists.
|
||||
// It does not return any error if you try to `pm block` a non-existent package.
|
||||
// Some commands are even killed by ADB before finishing and UAD can't catch
|
||||
// the output.
|
||||
if ["Error", "Failure"].iter().any(|&e| o.contains(e)) {
|
||||
error!("[{}] {} -> {}", label, action, o);
|
||||
Err(())
|
||||
} else {
|
||||
info!("[{}] {} -> {}", label, action, o);
|
||||
Ok(command_type)
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
if !err.contains("[not installed for") {
|
||||
error!("[{}] {} -> {}", label, action, err);
|
||||
}
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::option_if_let_else)]
|
||||
pub fn user_flag(user_id: Option<&User>) -> String {
|
||||
match user_id {
|
||||
Some(user_id) => format!(" --user {}", user_id.id),
|
||||
None => "".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn list_all_system_packages(user_id: Option<&User>) -> String {
|
||||
let action = format!("pm list packages -s -u{}", user_flag(user_id));
|
||||
|
||||
adb_shell_command(true, &action)
|
||||
.unwrap_or_else(|_| String::new())
|
||||
.replace("package:", "")
|
||||
}
|
||||
|
||||
pub fn hashset_system_packages(state: PackageState, user_id: Option<&User>) -> HashSet<String> {
|
||||
let user = user_flag(user_id);
|
||||
let action = match state {
|
||||
PackageState::Enabled => format!("pm list packages -s -e{user}"),
|
||||
PackageState::Disabled => format!("pm list package -s -d{user}"),
|
||||
_ => String::new(), // You probably don't need to use this function for anything else
|
||||
};
|
||||
|
||||
adb_shell_command(true, &action)
|
||||
.unwrap_or_default()
|
||||
.replace("package:", "")
|
||||
.lines()
|
||||
.map(String::from)
|
||||
.collect()
|
||||
}
|
||||
|
||||
// Minimum information for processing adb commands
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
|
||||
pub struct CorePackage {
|
||||
pub name: String,
|
||||
pub state: PackageState,
|
||||
}
|
||||
|
||||
impl From<&mut PackageRow> for CorePackage {
|
||||
fn from(pr: &mut PackageRow) -> Self {
|
||||
Self {
|
||||
name: pr.name.clone(),
|
||||
state: pr.state,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<PackageRow> for CorePackage {
|
||||
fn from(pr: PackageRow) -> Self {
|
||||
Self {
|
||||
name: pr.name.clone(),
|
||||
state: pr.state,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&PackageRow> for CorePackage {
|
||||
fn from(pr: &PackageRow) -> Self {
|
||||
Self {
|
||||
name: pr.name.clone(),
|
||||
state: pr.state,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_pkg_state_commands(
|
||||
package: &CorePackage,
|
||||
wanted_state: PackageState,
|
||||
selected_user: &User,
|
||||
phone: &Phone,
|
||||
) -> Vec<String> {
|
||||
// https://github.com/Universal-Debloater-Alliance/universal-android-debloater/wiki/ADB-reference
|
||||
// ALWAYS PUT THE COMMAND THAT CHANGES THE PACKAGE STATE FIRST!
|
||||
let commands = match wanted_state {
|
||||
PackageState::Enabled => {
|
||||
match package.state {
|
||||
PackageState::Disabled => match phone.android_sdk {
|
||||
i if i >= 23 => vec!["pm enable"],
|
||||
_ => vec!["pm enable"],
|
||||
},
|
||||
PackageState::Uninstalled => match phone.android_sdk {
|
||||
i if i >= 23 => vec!["cmd package install-existing"],
|
||||
21 | 22 => vec!["pm unhide"],
|
||||
19 | 20 => vec!["pm unblock", "pm clear"],
|
||||
_ => vec![], // Impossible action already prevented by the GUI
|
||||
},
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
PackageState::Disabled => match package.state {
|
||||
PackageState::Uninstalled | PackageState::Enabled => match phone.android_sdk {
|
||||
sdk if sdk >= 23 => vec!["pm disable-user", "am force-stop", "pm clear"],
|
||||
_ => vec![],
|
||||
},
|
||||
_ => vec![],
|
||||
},
|
||||
PackageState::Uninstalled => match package.state {
|
||||
PackageState::Enabled | PackageState::Disabled => match phone.android_sdk {
|
||||
sdk if sdk >= 23 => vec!["pm uninstall"], // > Android Marshmallow (6.0)
|
||||
21 | 22 => vec!["pm hide", "pm clear"], // Android Lollipop (5.x)
|
||||
19 | 20 => vec!["pm block", "pm clear"], // Android KitKat (4.4/4.4W)
|
||||
_ => vec!["pm uninstall"], // Disable mode is unavailable on older devices because the specific ADB commands need root
|
||||
},
|
||||
_ => vec![],
|
||||
},
|
||||
PackageState::All => vec![],
|
||||
};
|
||||
if phone.android_sdk < 21 {
|
||||
request_builder(&commands, &package.name, None)
|
||||
} else {
|
||||
request_builder(&commands, &package.name, Some(selected_user))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request_builder(commands: &[&str], package: &str, user: Option<&User>) -> Vec<String> {
|
||||
#[allow(clippy::option_if_let_else)]
|
||||
match user {
|
||||
Some(u) => commands
|
||||
.iter()
|
||||
.map(|c| format!("{} --user {} {}", c, u.id, package))
|
||||
.collect(),
|
||||
None => commands.iter().map(|c| format!("{c} {package}")).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_phone_model() -> String {
|
||||
adb_shell_command(true, "getprop ro.product.model").unwrap_or_else(|err| {
|
||||
println!("ERROR: {err}");
|
||||
if err.contains("adb: no devices/emulators found") {
|
||||
"no devices/emulators found".to_string()
|
||||
} else {
|
||||
err
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_android_sdk() -> u8 {
|
||||
adb_shell_command(true, "getprop ro.build.version.sdk").map_or(0, |sdk| sdk.parse().unwrap())
|
||||
}
|
||||
|
||||
pub fn get_phone_brand() -> String {
|
||||
format!(
|
||||
"{} {}",
|
||||
adb_shell_command(true, "getprop ro.product.brand")
|
||||
.map(|s| s.trim().to_string())
|
||||
.unwrap_or_default(),
|
||||
get_phone_model()
|
||||
)
|
||||
}
|
||||
|
||||
pub fn is_protected_user(user_id: &str) -> bool {
|
||||
adb_shell_command(true, &format!("pm list packages -s --user {user_id}")).is_err()
|
||||
}
|
||||
|
||||
pub fn get_user_list() -> Vec<User> {
|
||||
#[dynamic]
|
||||
static RE: Regex = Regex::new(r"\{([0-9]+)").unwrap();
|
||||
adb_shell_command(true, "pm list users")
|
||||
.map(|users| {
|
||||
RE.find_iter(&users)
|
||||
.enumerate()
|
||||
.map(|(i, u)| User {
|
||||
id: u.as_str()[1..].parse().unwrap(),
|
||||
index: i,
|
||||
protected: is_protected_user(&u.as_str()[1..]),
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
// getprop ro.serialno
|
||||
pub async fn get_devices_list() -> Vec<Phone> {
|
||||
retry(
|
||||
Fixed::from_millis(500).take(120),
|
||||
|| match adb_shell_command(false, "devices") {
|
||||
Ok(devices) => {
|
||||
let mut device_list: Vec<Phone> = vec![];
|
||||
if !RE.is_match(&devices) {
|
||||
return OperationResult::Retry(vec![]);
|
||||
}
|
||||
for device in RE.captures_iter(&devices) {
|
||||
env::set_var("ANDROID_SERIAL", &device[1]);
|
||||
device_list.push(Phone {
|
||||
model: get_phone_brand(),
|
||||
android_sdk: get_android_sdk(),
|
||||
user_list: get_user_list(),
|
||||
adb_id: device[1].to_string(),
|
||||
});
|
||||
}
|
||||
OperationResult::Ok(device_list)
|
||||
}
|
||||
Err(err) => {
|
||||
error!("get_device_list() -> {}", err);
|
||||
let test: Vec<Phone> = vec![];
|
||||
OperationResult::Retry(test)
|
||||
}
|
||||
},
|
||||
)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
use iced::{color, Color};
|
||||
|
||||
#[derive(Default, Debug, PartialEq, Eq, Copy, Clone)]
|
||||
pub enum Theme {
|
||||
#[default]
|
||||
Lupin,
|
||||
Dark,
|
||||
Light,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct BaseColors {
|
||||
pub background: Color,
|
||||
pub foreground: Color,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct NormalColors {
|
||||
pub primary: Color,
|
||||
pub secondary: Color,
|
||||
pub surface: Color,
|
||||
pub error: Color,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct BrightColors {
|
||||
pub primary: Color,
|
||||
pub secondary: Color,
|
||||
pub surface: Color,
|
||||
pub error: Color,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ColorPalette {
|
||||
pub base: BaseColors,
|
||||
pub normal: NormalColors,
|
||||
pub bright: BrightColors,
|
||||
}
|
||||
|
||||
impl Theme {
|
||||
pub const ALL: [Self; 3] = [Self::Lupin, Self::Dark, Self::Light];
|
||||
pub fn palette(self) -> ColorPalette {
|
||||
match self {
|
||||
Self::Dark => ColorPalette {
|
||||
base: BaseColors {
|
||||
background: color!(0x111111),
|
||||
foreground: color!(0x1C1C1C),
|
||||
},
|
||||
normal: NormalColors {
|
||||
primary: color!(0x5E4266),
|
||||
secondary: color!(0x386e50),
|
||||
surface: color!(0x828282),
|
||||
error: color!(0x992B2B),
|
||||
},
|
||||
bright: BrightColors {
|
||||
primary: color!(0xBA84FC),
|
||||
secondary: color!(0x49eb7a),
|
||||
surface: color!(0xE0E0E0),
|
||||
error: color!(0xC13047),
|
||||
},
|
||||
},
|
||||
Self::Light => ColorPalette {
|
||||
base: BaseColors {
|
||||
background: color!(0xEEEEEE),
|
||||
foreground: color!(0xE0E0E0),
|
||||
},
|
||||
normal: NormalColors {
|
||||
primary: color!(0x230F08),
|
||||
secondary: color!(0xF9D659),
|
||||
surface: color!(0x818181),
|
||||
error: color!(0x992B2B),
|
||||
},
|
||||
bright: BrightColors {
|
||||
primary: color!(0x673AB7),
|
||||
secondary: color!(0x3797A4),
|
||||
surface: color!(0x000000),
|
||||
error: color!(0xC13047),
|
||||
},
|
||||
},
|
||||
Self::Lupin => ColorPalette {
|
||||
base: BaseColors {
|
||||
background: color!(0x282a36),
|
||||
foreground: color!(0x353746),
|
||||
},
|
||||
normal: NormalColors {
|
||||
primary: color!(0x58406F),
|
||||
secondary: color!(0x386e50),
|
||||
surface: color!(0xa2a4a3),
|
||||
error: color!(0xA13034),
|
||||
},
|
||||
bright: BrightColors {
|
||||
primary: color!(0xbd94f9),
|
||||
secondary: color!(0x49eb7a),
|
||||
surface: color!(0xf4f8f3),
|
||||
error: color!(0xE63E6D),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Theme {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::Dark => "Dark",
|
||||
Self::Light => "Light",
|
||||
Self::Lupin => "Lupin",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
use crate::core::sync::{hashset_system_packages, list_all_system_packages, User};
|
||||
use crate::core::theme::Theme;
|
||||
use crate::core::uad_lists::{Package, PackageState, Removal, UadList};
|
||||
use crate::gui::widgets::package_row::PackageRow;
|
||||
use chrono::offset::Utc;
|
||||
use chrono::DateTime;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::{fmt, fs};
|
||||
|
||||
pub fn fetch_packages(
|
||||
uad_lists: &HashMap<String, Package>,
|
||||
user_id: Option<&User>,
|
||||
) -> Vec<PackageRow> {
|
||||
let all_system_packages = list_all_system_packages(user_id); // installed and uninstalled packages
|
||||
let enabled_system_packages = hashset_system_packages(PackageState::Enabled, user_id);
|
||||
let disabled_system_packages = hashset_system_packages(PackageState::Disabled, user_id);
|
||||
let mut description;
|
||||
let mut uad_list;
|
||||
let mut state;
|
||||
let mut removal;
|
||||
let mut user_package: Vec<PackageRow> = Vec::new();
|
||||
|
||||
for p_name in all_system_packages.lines() {
|
||||
state = PackageState::Uninstalled;
|
||||
description = "[No description] : CONTRIBUTION WELCOMED";
|
||||
uad_list = UadList::Unlisted;
|
||||
removal = Removal::Unlisted;
|
||||
|
||||
if uad_lists.contains_key(p_name) {
|
||||
description = &uad_lists.get(p_name).unwrap().description;
|
||||
if description.is_empty() {
|
||||
description = "[No description] : CONTRIBUTION WELCOMED";
|
||||
};
|
||||
uad_list = uad_lists.get(p_name).unwrap().list;
|
||||
removal = uad_lists.get(p_name).unwrap().removal;
|
||||
}
|
||||
|
||||
if enabled_system_packages.contains(p_name) {
|
||||
state = PackageState::Enabled;
|
||||
} else if disabled_system_packages.contains(p_name) {
|
||||
state = PackageState::Disabled;
|
||||
}
|
||||
|
||||
let package_row =
|
||||
PackageRow::new(p_name, state, description, uad_list, removal, false, false);
|
||||
user_package.push(package_row);
|
||||
}
|
||||
user_package.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
|
||||
user_package
|
||||
}
|
||||
|
||||
pub fn string_to_theme(theme: &str) -> Theme {
|
||||
match theme {
|
||||
"Dark" => Theme::Dark,
|
||||
"Light" => Theme::Light,
|
||||
"Lupin" => Theme::Lupin,
|
||||
_ => Theme::Lupin,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setup_uad_dir(dir: Option<PathBuf>) -> PathBuf {
|
||||
let dir = dir.unwrap().join("uad");
|
||||
fs::create_dir_all(&dir).expect("Can't create cache directory");
|
||||
dir
|
||||
}
|
||||
|
||||
pub fn open_url(dir: PathBuf) {
|
||||
#[cfg(target_os = "windows")]
|
||||
let output = Command::new("explorer").args([dir]).output();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let output = Command::new("open").args([dir]).output();
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
|
||||
let output = Command::new("xdg-open").args([dir]).output();
|
||||
|
||||
match output {
|
||||
Ok(o) => {
|
||||
if !o.status.success() {
|
||||
let stderr = String::from_utf8(o.stderr).unwrap().trim_end().to_string();
|
||||
error!("Can't open the following URL: {}", stderr);
|
||||
}
|
||||
}
|
||||
Err(e) => error!("Failed to run command to open the file explorer: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
#[allow(clippy::option_if_let_else)]
|
||||
pub fn last_modified_date(file: PathBuf) -> DateTime<Utc> {
|
||||
fs::metadata(file).map_or_else(|_| Utc::now(), |metadata| match metadata.modified() {
|
||||
Ok(time) => time.into(),
|
||||
Err(_) => Utc::now(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn format_diff_time_from_now(date: DateTime<Utc>) -> String {
|
||||
let now: DateTime<Utc> = Utc::now();
|
||||
let last_update = now - date;
|
||||
if last_update.num_days() == 0 {
|
||||
if last_update.num_hours() == 0 {
|
||||
last_update.num_minutes().to_string() + " min(s) ago"
|
||||
} else {
|
||||
last_update.num_hours().to_string() + " hour(s) ago"
|
||||
}
|
||||
} else {
|
||||
last_update.num_days().to_string() + " day(s) ago"
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct DisplayablePath {
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
impl fmt::Display for DisplayablePath {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let stem = self.path.file_stem().map_or_else(
|
||||
|| {
|
||||
error!("[PATH STEM]: No file stem found");
|
||||
"[File steam not found]".to_string()
|
||||
},
|
||||
|p| match p.to_os_string().into_string() {
|
||||
Ok(stem) => stem,
|
||||
Err(e) => {
|
||||
error!("[PATH ENCODING]: {:?}", e);
|
||||
"[PATH ENCODING ERROR]".to_string()
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
write!(f, "{stem}")
|
||||
}
|
||||
}
|
||||
367
src/gui/mod.rs
367
src/gui/mod.rs
|
|
@ -1,367 +0,0 @@
|
|||
pub mod style;
|
||||
pub mod views;
|
||||
pub mod widgets;
|
||||
|
||||
use crate::core::sync::{get_devices_list, perform_adb_commands, CommandType, Phone};
|
||||
use crate::core::theme::Theme;
|
||||
use crate::core::uad_lists::UadListState;
|
||||
use crate::core::update::{get_latest_release, Release, SelfUpdateState, SelfUpdateStatus};
|
||||
use crate::core::utils::string_to_theme;
|
||||
|
||||
use views::about::{About as AboutView, Message as AboutMessage};
|
||||
use views::list::{List as AppsView, LoadingState as ListLoadingState, Message as AppsMessage};
|
||||
use views::settings::{Message as SettingsMessage, Settings as SettingsView};
|
||||
use widgets::navigation_menu::nav_menu;
|
||||
|
||||
use iced::widget::column;
|
||||
use iced::{
|
||||
window::Settings as Window, Alignment, Application, Command, Element, Length, Renderer,
|
||||
Settings,
|
||||
};
|
||||
use std::{env, path::PathBuf};
|
||||
|
||||
#[cfg(feature = "self-update")]
|
||||
use crate::core::update::{bin_name, download_update_to_temp_file, remove_file};
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
enum View {
|
||||
#[default]
|
||||
List,
|
||||
About,
|
||||
Settings,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct UpdateState {
|
||||
self_update: SelfUpdateState,
|
||||
uad_list: UadListState,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct UadGui {
|
||||
view: View,
|
||||
apps_view: AppsView,
|
||||
about_view: AboutView,
|
||||
settings_view: SettingsView,
|
||||
devices_list: Vec<Phone>,
|
||||
selected_device: Option<Phone>, // index of devices_list
|
||||
update_state: UpdateState,
|
||||
nb_running_async_adb_commands: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Message {
|
||||
// Navigation Panel
|
||||
AboutPressed,
|
||||
SettingsPressed,
|
||||
AppsPress,
|
||||
DeviceSelected(Phone),
|
||||
AboutAction(AboutMessage),
|
||||
AppsAction(AppsMessage),
|
||||
SettingsAction(SettingsMessage),
|
||||
RefreshButtonPressed,
|
||||
RebootButtonPressed,
|
||||
LoadDevices(Vec<Phone>),
|
||||
_NewReleaseDownloaded(Result<(PathBuf, PathBuf), ()>),
|
||||
GetLatestRelease(Result<Option<Release>, ()>),
|
||||
Nothing,
|
||||
}
|
||||
|
||||
impl Application for UadGui {
|
||||
type Theme = Theme;
|
||||
type Executor = iced::executor::Default;
|
||||
type Message = Message;
|
||||
type Flags = ();
|
||||
|
||||
fn new(_flags: ()) -> (Self, Command<Message>) {
|
||||
(
|
||||
Self::default(),
|
||||
Command::batch([
|
||||
Command::perform(get_devices_list(), Message::LoadDevices),
|
||||
Command::perform(
|
||||
async move { get_latest_release() },
|
||||
Message::GetLatestRelease,
|
||||
),
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
fn theme(&self) -> Theme {
|
||||
string_to_theme(&self.settings_view.general.theme)
|
||||
}
|
||||
|
||||
fn title(&self) -> String {
|
||||
String::from("Universal Android Debloater")
|
||||
}
|
||||
fn update(&mut self, msg: Message) -> Command<Message> {
|
||||
match msg {
|
||||
#[allow(clippy::option_if_let_else)]
|
||||
Message::LoadDevices(devices_list) => {
|
||||
self.selected_device = match &self.selected_device {
|
||||
Some(s_device) => {
|
||||
// Try to reload last selected phone
|
||||
devices_list
|
||||
.iter()
|
||||
.find(|phone| phone.adb_id == s_device.adb_id)
|
||||
.cloned()
|
||||
}
|
||||
None => devices_list.first().cloned(),
|
||||
};
|
||||
self.devices_list = devices_list;
|
||||
|
||||
#[allow(unused_must_use)]
|
||||
{
|
||||
self.update(Message::SettingsAction(SettingsMessage::LoadDeviceSettings));
|
||||
}
|
||||
|
||||
self.update(Message::AppsAction(AppsMessage::LoadUadList(true)))
|
||||
}
|
||||
Message::AppsPress => {
|
||||
self.view = View::List;
|
||||
Command::none()
|
||||
}
|
||||
Message::AboutPressed => {
|
||||
self.view = View::About;
|
||||
self.update_state.self_update = SelfUpdateState::default();
|
||||
Command::perform(
|
||||
async move { get_latest_release() },
|
||||
Message::GetLatestRelease,
|
||||
)
|
||||
}
|
||||
Message::SettingsPressed => {
|
||||
self.view = View::Settings;
|
||||
Command::none()
|
||||
}
|
||||
Message::RefreshButtonPressed => {
|
||||
self.apps_view = AppsView::default();
|
||||
Command::perform(get_devices_list(), Message::LoadDevices)
|
||||
}
|
||||
Message::RebootButtonPressed => {
|
||||
self.apps_view = AppsView::default();
|
||||
self.selected_device = None;
|
||||
self.devices_list = vec![];
|
||||
Command::perform(
|
||||
perform_adb_commands("reboot".to_string(), CommandType::Shell),
|
||||
|_| Message::Nothing,
|
||||
)
|
||||
}
|
||||
Message::AppsAction(msg) => self
|
||||
.apps_view
|
||||
.update(
|
||||
&mut self.settings_view,
|
||||
&mut self.selected_device.clone().unwrap_or_default(),
|
||||
&mut self.update_state.uad_list,
|
||||
msg,
|
||||
)
|
||||
.map(Message::AppsAction),
|
||||
Message::SettingsAction(msg) => {
|
||||
match msg {
|
||||
SettingsMessage::RestoringDevice(ref output) => {
|
||||
self.nb_running_async_adb_commands -= 1;
|
||||
self.view = View::List;
|
||||
|
||||
#[allow(unused_must_use)]
|
||||
{
|
||||
self.apps_view.update(
|
||||
&mut self.settings_view,
|
||||
&mut self.selected_device.clone().unwrap_or_default(),
|
||||
&mut self.update_state.uad_list,
|
||||
AppsMessage::RestoringDevice(output.clone()),
|
||||
);
|
||||
}
|
||||
if self.nb_running_async_adb_commands == 0 {
|
||||
return self.update(Message::RefreshButtonPressed);
|
||||
}
|
||||
}
|
||||
SettingsMessage::MultiUserMode(toggled) => {
|
||||
if toggled {
|
||||
for user in self.apps_view.phone_packages.clone() {
|
||||
for (i, _) in
|
||||
user.iter().enumerate().filter(|&(_, pkg)| pkg.selected)
|
||||
{
|
||||
for u in self
|
||||
.selected_device
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.user_list
|
||||
.iter()
|
||||
.filter(|&u| !u.protected)
|
||||
{
|
||||
self.apps_view.phone_packages[u.index][i].selected = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
self.settings_view
|
||||
.update(
|
||||
&self.selected_device.clone().unwrap_or_default(),
|
||||
&self.apps_view.phone_packages,
|
||||
&mut self.nb_running_async_adb_commands,
|
||||
msg,
|
||||
)
|
||||
.map(Message::SettingsAction)
|
||||
}
|
||||
Message::AboutAction(msg) => {
|
||||
self.about_view.update(msg.clone());
|
||||
|
||||
match msg {
|
||||
AboutMessage::UpdateUadLists => {
|
||||
self.update_state.uad_list = UadListState::Downloading;
|
||||
self.apps_view.loading_state =
|
||||
ListLoadingState::DownloadingList(String::new());
|
||||
self.update(Message::AppsAction(AppsMessage::LoadUadList(true)))
|
||||
}
|
||||
AboutMessage::DoSelfUpdate => {
|
||||
#[cfg(feature = "self-update")]
|
||||
if self.update_state.self_update.latest_release.is_some() {
|
||||
self.update_state.self_update.status = SelfUpdateStatus::Updating;
|
||||
self.apps_view.loading_state =
|
||||
ListLoadingState::_UpdatingUad(String::new());
|
||||
let bin_name = bin_name().to_owned();
|
||||
let release = self
|
||||
.update_state
|
||||
.self_update
|
||||
.latest_release
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.clone();
|
||||
Command::perform(
|
||||
download_update_to_temp_file(bin_name, release),
|
||||
Message::_NewReleaseDownloaded,
|
||||
)
|
||||
} else {
|
||||
Command::none()
|
||||
}
|
||||
#[cfg(not(feature = "self-update"))]
|
||||
Command::none()
|
||||
}
|
||||
AboutMessage::UrlPressed(_) => Command::none(),
|
||||
}
|
||||
}
|
||||
Message::DeviceSelected(s_device) => {
|
||||
self.selected_device = Some(s_device.clone());
|
||||
self.view = View::List;
|
||||
env::set_var("ANDROID_SERIAL", s_device.adb_id);
|
||||
info!("{:-^65}", "-");
|
||||
info!(
|
||||
"ANDROID_SDK: {} | DEVICE: {}",
|
||||
s_device.android_sdk, s_device.model
|
||||
);
|
||||
info!("{:-^65}", "-");
|
||||
self.apps_view.loading_state = ListLoadingState::FindingPhones(String::new());
|
||||
|
||||
#[allow(unused_must_use)]
|
||||
{
|
||||
self.update(Message::SettingsAction(SettingsMessage::LoadDeviceSettings));
|
||||
}
|
||||
self.update(Message::AppsAction(AppsMessage::LoadPhonePackages((
|
||||
self.apps_view.uad_lists.clone(),
|
||||
UadListState::Done,
|
||||
))))
|
||||
}
|
||||
Message::_NewReleaseDownloaded(res) => {
|
||||
debug!("UAD update has been download!");
|
||||
|
||||
#[cfg(feature = "self-update")]
|
||||
if let Ok((relaunch_path, cleanup_path)) = res {
|
||||
// Remove first arg, which is path to binary. We don't use this first
|
||||
// arg as binary path because it's not reliable, per the docs.
|
||||
let mut args = std::env::args();
|
||||
args.next();
|
||||
let mut args: Vec<_> = args.collect();
|
||||
|
||||
// Remove the `--self-update-temp` arg from args if it exists,
|
||||
// since we need to pass it cleanly. Otherwise new process will
|
||||
// fail during arg parsing.
|
||||
if let Some(idx) = args.iter().position(|a| a == "--self-update-temp") {
|
||||
args.remove(idx);
|
||||
// Remove path passed after this arg
|
||||
args.remove(idx);
|
||||
}
|
||||
|
||||
match std::process::Command::new(relaunch_path)
|
||||
.args(args)
|
||||
.arg("--self-update-temp")
|
||||
.arg(&cleanup_path)
|
||||
.spawn()
|
||||
{
|
||||
Ok(_) => {
|
||||
if let Err(e) = remove_file(cleanup_path) {
|
||||
error!("Could not remove temp update file: {}", e);
|
||||
}
|
||||
std::process::exit(0)
|
||||
}
|
||||
Err(error) => {
|
||||
if let Err(e) = remove_file(cleanup_path) {
|
||||
error!("Could not remove temp update file: {}", e);
|
||||
}
|
||||
error!("Failed to update UAD: {}", error);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error!("Failed to update UAD!");
|
||||
}
|
||||
Command::none()
|
||||
}
|
||||
Message::GetLatestRelease(release) => {
|
||||
match release {
|
||||
Ok(r) => {
|
||||
self.update_state.self_update.status = SelfUpdateStatus::Done;
|
||||
self.update_state.self_update.latest_release = r;
|
||||
}
|
||||
Err(_) => self.update_state.self_update.status = SelfUpdateStatus::Failed,
|
||||
};
|
||||
Command::none()
|
||||
}
|
||||
Message::Nothing => Command::none(),
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self) -> Element<Self::Message, Renderer<Self::Theme>> {
|
||||
let navigation_container = nav_menu(
|
||||
&self.devices_list,
|
||||
self.selected_device.clone(),
|
||||
&self.apps_view,
|
||||
&self.update_state.self_update,
|
||||
);
|
||||
|
||||
let selected_device = self.selected_device.clone().unwrap_or_default();
|
||||
let main_container = match self.view {
|
||||
View::List => self
|
||||
.apps_view
|
||||
.view(&self.settings_view, &selected_device)
|
||||
.map(Message::AppsAction),
|
||||
View::About => self
|
||||
.about_view
|
||||
.view(&self.update_state)
|
||||
.map(Message::AboutAction),
|
||||
View::Settings => self
|
||||
.settings_view
|
||||
.view(&selected_device)
|
||||
.map(Message::SettingsAction),
|
||||
};
|
||||
|
||||
column![navigation_container, main_container]
|
||||
.width(Length::Fill)
|
||||
.align_items(Alignment::Center)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl UadGui {
|
||||
pub fn start() -> iced::Result {
|
||||
Self::run(Settings {
|
||||
window: Window {
|
||||
size: (820, 640),
|
||||
resizable: true,
|
||||
decorations: true,
|
||||
..iced::window::Settings::default()
|
||||
},
|
||||
default_text_size: 17.0,
|
||||
..Settings::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
510
src/gui/style.rs
510
src/gui/style.rs
|
|
@ -1,510 +0,0 @@
|
|||
use crate::core::theme::Theme;
|
||||
use iced::overlay::menu;
|
||||
use iced::widget::{
|
||||
button, checkbox, container, pick_list, radio, rule, scrollable, text, text_input,
|
||||
};
|
||||
use iced::{application, Background, Color};
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy)]
|
||||
pub enum Application {
|
||||
#[default]
|
||||
Default,
|
||||
}
|
||||
|
||||
impl application::StyleSheet for Theme {
|
||||
type Style = Application;
|
||||
|
||||
fn appearance(&self, _style: &Self::Style) -> application::Appearance {
|
||||
application::Appearance {
|
||||
background_color: self.palette().base.background,
|
||||
text_color: self.palette().bright.surface,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy)]
|
||||
pub enum Container {
|
||||
#[default]
|
||||
Invisible,
|
||||
Frame,
|
||||
BorderedFrame,
|
||||
Tooltip,
|
||||
Background,
|
||||
}
|
||||
|
||||
impl container::StyleSheet for Theme {
|
||||
type Style = Container;
|
||||
|
||||
fn appearance(&self, style: &Self::Style) -> container::Appearance {
|
||||
match style {
|
||||
Container::Invisible => container::Appearance::default(),
|
||||
Container::Frame => container::Appearance {
|
||||
background: Some(Background::Color(self.palette().base.foreground)),
|
||||
text_color: Some(self.palette().bright.surface),
|
||||
border_radius: 5.0,
|
||||
..container::Appearance::default()
|
||||
},
|
||||
Container::BorderedFrame => container::Appearance {
|
||||
background: Some(Background::Color(self.palette().base.foreground)),
|
||||
text_color: Some(self.palette().bright.surface),
|
||||
border_radius: 5.0,
|
||||
border_width: 1.0,
|
||||
border_color: self.palette().normal.error,
|
||||
},
|
||||
Container::Tooltip => container::Appearance {
|
||||
background: Some(Background::Color(self.palette().base.foreground)),
|
||||
text_color: Some(self.palette().bright.surface),
|
||||
border_radius: 8.0,
|
||||
border_width: 1.0,
|
||||
border_color: self.palette().normal.primary,
|
||||
},
|
||||
|
||||
Container::Background => container::Appearance {
|
||||
background: Some(Background::Color(self.palette().base.background)),
|
||||
text_color: Some(self.palette().bright.surface),
|
||||
border_radius: 5.0,
|
||||
..container::Appearance::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Button {
|
||||
#[default]
|
||||
Primary,
|
||||
Unavailable,
|
||||
SelfUpdate,
|
||||
Refresh,
|
||||
UninstallPackage,
|
||||
RestorePackage,
|
||||
NormalPackage,
|
||||
SelectedPackage,
|
||||
}
|
||||
|
||||
impl button::StyleSheet for Theme {
|
||||
type Style = Button;
|
||||
|
||||
fn active(&self, style: &Self::Style) -> button::Appearance {
|
||||
let p = self.palette();
|
||||
|
||||
let appearance = button::Appearance {
|
||||
border_width: 1.0,
|
||||
border_radius: 2.0,
|
||||
..button::Appearance::default()
|
||||
};
|
||||
|
||||
let active_appearance = |bg: Option<Color>, mc| button::Appearance {
|
||||
background: Some(Background::Color(bg.unwrap_or(p.base.foreground))),
|
||||
border_color: Color { a: 0.5, ..mc },
|
||||
text_color: mc,
|
||||
..appearance
|
||||
};
|
||||
|
||||
match style {
|
||||
Button::Primary | Button::SelfUpdate | Button::Refresh => {
|
||||
active_appearance(None, p.bright.primary)
|
||||
}
|
||||
Button::RestorePackage => active_appearance(None, p.bright.secondary),
|
||||
Button::NormalPackage => button::Appearance {
|
||||
background: Some(Background::Color(p.base.foreground)),
|
||||
text_color: p.bright.surface,
|
||||
border_radius: 5.0,
|
||||
border_width: 0.0,
|
||||
border_color: p.base.background,
|
||||
..appearance
|
||||
},
|
||||
Button::SelectedPackage => button::Appearance {
|
||||
background: Some(Background::Color(Color {
|
||||
a: 0.25,
|
||||
..p.normal.primary
|
||||
})),
|
||||
text_color: p.bright.primary,
|
||||
border_radius: 5.0,
|
||||
border_width: 0.0,
|
||||
border_color: p.normal.primary,
|
||||
..appearance
|
||||
},
|
||||
Button::Unavailable | Button::UninstallPackage => {
|
||||
active_appearance(None, p.bright.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn hovered(&self, style: &Self::Style) -> button::Appearance {
|
||||
let active = self.active(style);
|
||||
let p = self.palette();
|
||||
|
||||
let hover_appearance = |bg, tc: Option<Color>| button::Appearance {
|
||||
background: Some(Background::Color(Color { a: 0.25, ..bg })),
|
||||
text_color: tc.unwrap_or(bg),
|
||||
..active
|
||||
};
|
||||
|
||||
match style {
|
||||
Button::Primary | Button::SelfUpdate | Button::Refresh => {
|
||||
hover_appearance(p.bright.primary, None)
|
||||
}
|
||||
Button::NormalPackage => hover_appearance(p.normal.primary, Some(p.bright.surface)),
|
||||
Button::SelectedPackage => hover_appearance(p.normal.primary, None),
|
||||
Button::RestorePackage => hover_appearance(p.bright.secondary, None),
|
||||
Button::Unavailable | Button::UninstallPackage => {
|
||||
hover_appearance(p.bright.error, None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn pressed(&self, style: &Self::Style) -> button::Appearance {
|
||||
self.active(style)
|
||||
}
|
||||
|
||||
fn disabled(&self, style: &Self::Style) -> button::Appearance {
|
||||
let active = self.active(style);
|
||||
let p = self.palette();
|
||||
|
||||
let disabled_appearance = |bg, tc: Option<Color>| button::Appearance {
|
||||
background: Some(Background::Color(Color { a: 0.05, ..bg })),
|
||||
text_color: Color {
|
||||
a: 0.50,
|
||||
..tc.unwrap_or(bg)
|
||||
},
|
||||
..active
|
||||
};
|
||||
|
||||
match style {
|
||||
Button::RestorePackage => disabled_appearance(p.normal.primary, Some(p.bright.primary)),
|
||||
Button::UninstallPackage => disabled_appearance(p.bright.error, None),
|
||||
Button::Primary => disabled_appearance(p.bright.primary, Some(p.bright.primary)),
|
||||
_ => active,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy)]
|
||||
pub enum Scrollable {
|
||||
#[default]
|
||||
Description,
|
||||
Packages,
|
||||
}
|
||||
|
||||
impl scrollable::StyleSheet for Theme {
|
||||
type Style = Scrollable;
|
||||
|
||||
fn active(&self, style: &Self::Style) -> scrollable::Scrollbar {
|
||||
let from_appearance = |c: Color| scrollable::Scrollbar {
|
||||
background: Some(Background::Color(Color::TRANSPARENT)),
|
||||
border_radius: 5.0,
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
scroller: scrollable::Scroller {
|
||||
color: c,
|
||||
border_radius: 5.0,
|
||||
border_width: 1.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
},
|
||||
};
|
||||
|
||||
match style {
|
||||
Scrollable::Description => from_appearance(self.palette().normal.surface),
|
||||
Scrollable::Packages => from_appearance(self.palette().base.foreground),
|
||||
}
|
||||
}
|
||||
|
||||
fn hovered(&self, style: &Self::Style, _mouse_over_scrollbar: bool) -> scrollable::Scrollbar {
|
||||
scrollable::Scrollbar {
|
||||
scroller: self.active(style).scroller,
|
||||
..self.active(style)
|
||||
}
|
||||
}
|
||||
|
||||
fn dragging(&self, style: &Self::Style) -> scrollable::Scrollbar {
|
||||
let hovered = self.hovered(style, true);
|
||||
scrollable::Scrollbar {
|
||||
scroller: hovered.scroller,
|
||||
..hovered
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy)]
|
||||
pub enum CheckBox {
|
||||
#[default]
|
||||
PackageEnabled,
|
||||
PackageDisabled,
|
||||
SettingsEnabled,
|
||||
SettingsDisabled,
|
||||
}
|
||||
|
||||
impl checkbox::StyleSheet for Theme {
|
||||
type Style = CheckBox;
|
||||
|
||||
fn active(&self, style: &Self::Style, _is_checked: bool) -> checkbox::Appearance {
|
||||
match style {
|
||||
CheckBox::PackageEnabled => checkbox::Appearance {
|
||||
background: Background::Color(self.palette().base.background),
|
||||
icon_color: self.palette().bright.primary,
|
||||
border_radius: 5.0,
|
||||
border_width: 1.0,
|
||||
border_color: self.palette().base.background,
|
||||
text_color: Some(self.palette().bright.surface),
|
||||
},
|
||||
CheckBox::PackageDisabled => checkbox::Appearance {
|
||||
background: Background::Color(Color {
|
||||
a: 0.55,
|
||||
..self.palette().base.background
|
||||
}),
|
||||
icon_color: self.palette().bright.primary,
|
||||
border_radius: 5.0,
|
||||
border_width: 1.0,
|
||||
border_color: self.palette().normal.primary,
|
||||
text_color: Some(self.palette().normal.primary),
|
||||
},
|
||||
CheckBox::SettingsEnabled => checkbox::Appearance {
|
||||
background: Background::Color(self.palette().base.background),
|
||||
icon_color: self.palette().bright.primary,
|
||||
border_radius: 5.0,
|
||||
border_width: 1.0,
|
||||
border_color: self.palette().bright.primary,
|
||||
text_color: Some(self.palette().bright.surface),
|
||||
},
|
||||
CheckBox::SettingsDisabled => checkbox::Appearance {
|
||||
background: Background::Color(self.palette().base.foreground),
|
||||
icon_color: self.palette().bright.primary,
|
||||
border_radius: 5.0,
|
||||
border_width: 1.0,
|
||||
border_color: self.palette().normal.primary,
|
||||
text_color: Some(self.palette().bright.surface),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn hovered(&self, style: &Self::Style, is_checked: bool) -> checkbox::Appearance {
|
||||
let from_appearance = || checkbox::Appearance {
|
||||
background: Background::Color(self.palette().base.foreground),
|
||||
icon_color: self.palette().bright.primary,
|
||||
border_radius: 5.0,
|
||||
border_width: 2.0,
|
||||
border_color: self.palette().bright.primary,
|
||||
text_color: Some(self.palette().bright.surface),
|
||||
};
|
||||
|
||||
match style {
|
||||
CheckBox::PackageEnabled | CheckBox::SettingsEnabled => from_appearance(),
|
||||
CheckBox::PackageDisabled | CheckBox::SettingsDisabled => {
|
||||
self.active(style, is_checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy)]
|
||||
pub enum TextInput {
|
||||
#[default]
|
||||
Default,
|
||||
}
|
||||
|
||||
impl text_input::StyleSheet for Theme {
|
||||
type Style = TextInput;
|
||||
|
||||
fn active(&self, _style: &Self::Style) -> text_input::Appearance {
|
||||
text_input::Appearance {
|
||||
background: Background::Color(self.palette().base.foreground),
|
||||
border_radius: 5.0,
|
||||
border_width: 0.0,
|
||||
border_color: self.palette().base.foreground,
|
||||
icon_color: Color {
|
||||
a: 0.5,
|
||||
..self.palette().normal.primary
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn focused(&self, _style: &Self::Style) -> text_input::Appearance {
|
||||
text_input::Appearance {
|
||||
background: Background::Color(self.palette().base.foreground),
|
||||
border_radius: 2.0,
|
||||
border_width: 1.0,
|
||||
border_color: Color {
|
||||
a: 0.5,
|
||||
..self.palette().normal.primary
|
||||
},
|
||||
icon_color: Color {
|
||||
a: 0.5,
|
||||
..self.palette().normal.primary
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn disabled_color(
|
||||
&self,
|
||||
_: &<Self as iced::widget::text_input::StyleSheet>::Style,
|
||||
) -> iced::Color {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn disabled(&self, _style: &Self::Style) -> text_input::Appearance {
|
||||
text_input::Appearance {
|
||||
background: Background::Color(self.palette().base.background),
|
||||
border_radius: 2.0,
|
||||
border_width: 1.0,
|
||||
border_color: Color {
|
||||
a: 0.5,
|
||||
..self.palette().base.foreground
|
||||
},
|
||||
icon_color: Color {
|
||||
a: 0.5,
|
||||
..self.palette().base.foreground
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn placeholder_color(&self, _style: &Self::Style) -> Color {
|
||||
self.palette().normal.surface
|
||||
}
|
||||
|
||||
fn value_color(&self, _style: &Self::Style) -> Color {
|
||||
self.palette().bright.primary
|
||||
}
|
||||
|
||||
fn selection_color(&self, _style: &Self::Style) -> Color {
|
||||
self.palette().normal.primary
|
||||
}
|
||||
|
||||
/// Produces the style of an hovered text input.
|
||||
fn hovered(&self, style: &Self::Style) -> text_input::Appearance {
|
||||
self.focused(style)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy)]
|
||||
pub enum PickList {
|
||||
#[default]
|
||||
Default,
|
||||
}
|
||||
|
||||
impl menu::StyleSheet for Theme {
|
||||
type Style = ();
|
||||
|
||||
fn appearance(&self, _style: &Self::Style) -> menu::Appearance {
|
||||
let p = self.palette();
|
||||
|
||||
menu::Appearance {
|
||||
text_color: p.bright.surface,
|
||||
background: p.base.background.into(),
|
||||
border_width: 1.0,
|
||||
border_radius: 2.0,
|
||||
border_color: p.base.background,
|
||||
selected_text_color: p.bright.surface,
|
||||
selected_background: p.normal.primary.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl pick_list::StyleSheet for Theme {
|
||||
type Style = ();
|
||||
|
||||
fn active(&self, _style: &()) -> pick_list::Appearance {
|
||||
pick_list::Appearance {
|
||||
text_color: self.palette().bright.surface,
|
||||
background: self.palette().base.background.into(),
|
||||
border_width: 1.0,
|
||||
border_color: Color {
|
||||
a: 0.5,
|
||||
..self.palette().normal.primary
|
||||
},
|
||||
border_radius: 2.0,
|
||||
handle_color: self.palette().bright.surface,
|
||||
placeholder_color: self.palette().bright.surface,
|
||||
}
|
||||
}
|
||||
|
||||
fn hovered(&self, style: &()) -> pick_list::Appearance {
|
||||
let active = self.active(style);
|
||||
pick_list::Appearance {
|
||||
border_color: self.palette().normal.primary,
|
||||
..active
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy)]
|
||||
pub enum Text {
|
||||
#[default]
|
||||
Default,
|
||||
Ok,
|
||||
Danger,
|
||||
Commentary,
|
||||
Color(Color),
|
||||
}
|
||||
|
||||
impl From<Color> for Text {
|
||||
fn from(color: Color) -> Self {
|
||||
Self::Color(color)
|
||||
}
|
||||
}
|
||||
|
||||
impl text::StyleSheet for Theme {
|
||||
type Style = Text;
|
||||
|
||||
fn appearance(&self, style: Self::Style) -> text::Appearance {
|
||||
match style {
|
||||
Text::Default => text::Appearance::default(),
|
||||
Text::Ok => text::Appearance {
|
||||
color: Some(self.palette().bright.secondary),
|
||||
},
|
||||
Text::Danger => text::Appearance {
|
||||
color: Some(self.palette().bright.error),
|
||||
},
|
||||
Text::Commentary => text::Appearance {
|
||||
color: Some(self.palette().normal.surface),
|
||||
},
|
||||
Text::Color(c) => text::Appearance { color: Some(c) },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl radio::StyleSheet for Theme {
|
||||
type Style = ();
|
||||
|
||||
fn active(&self, _style: &Self::Style, _is_selected: bool) -> radio::Appearance {
|
||||
radio::Appearance {
|
||||
background: Color::TRANSPARENT.into(),
|
||||
dot_color: self.palette().bright.primary,
|
||||
border_width: 1.0,
|
||||
border_color: self.palette().bright.primary,
|
||||
text_color: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn hovered(&self, style: &Self::Style, _is_selected: bool) -> radio::Appearance {
|
||||
let active = self.active(style, true);
|
||||
|
||||
radio::Appearance {
|
||||
dot_color: self.palette().bright.primary,
|
||||
border_color: self.palette().bright.primary,
|
||||
border_width: 2.0,
|
||||
..active
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy)]
|
||||
pub enum Rule {
|
||||
#[default]
|
||||
Default,
|
||||
}
|
||||
|
||||
impl rule::StyleSheet for Theme {
|
||||
type Style = Rule;
|
||||
|
||||
fn appearance(&self, style: &Self::Style) -> rule::Appearance {
|
||||
match style {
|
||||
Rule::Default => rule::Appearance {
|
||||
color: self.palette().bright.surface,
|
||||
width: 2,
|
||||
radius: 2.0,
|
||||
fill_mode: rule::FillMode::Full,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,145 +0,0 @@
|
|||
use crate::core::theme::Theme;
|
||||
use crate::core::utils::{last_modified_date, open_url};
|
||||
use crate::gui::{style, UpdateState};
|
||||
use crate::CACHE_DIR;
|
||||
use iced::widget::{button, column, container, row, text, Space};
|
||||
use iced::{Alignment, Element, Length, Renderer};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[cfg(feature = "self-update")]
|
||||
use crate::core::update::SelfUpdateStatus;
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct About {}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Message {
|
||||
UrlPressed(PathBuf),
|
||||
UpdateUadLists,
|
||||
DoSelfUpdate,
|
||||
}
|
||||
|
||||
impl About {
|
||||
pub fn update(&mut self, msg: Message) {
|
||||
if let Message::UrlPressed(url) = msg {
|
||||
open_url(url);
|
||||
}
|
||||
// other events are handled by UadGui update()
|
||||
}
|
||||
pub fn view(&self, update_state: &UpdateState) -> Element<Message, Renderer<Theme>> {
|
||||
let about_text = text(
|
||||
"Universal Android Debloater (UAD) is a Free and Open-Source community project aiming at simplifying \
|
||||
the removal of pre-installed apps on any Android device.",
|
||||
);
|
||||
|
||||
let descr_container = container(about_text)
|
||||
.width(Length::Fill)
|
||||
.padding(25)
|
||||
.style(style::Container::Frame);
|
||||
|
||||
let date = last_modified_date(CACHE_DIR.join("uad_lists.json"));
|
||||
let uad_list_text = text(format!("Documentation: v{}", date.format("%Y%m%d"))).width(250);
|
||||
let last_update_text = text(update_state.uad_list.to_string());
|
||||
let uad_lists_btn = button("Update")
|
||||
.on_press(Message::UpdateUadLists)
|
||||
.padding(5)
|
||||
.style(style::Button::Primary);
|
||||
|
||||
#[cfg(feature = "self-update")]
|
||||
let self_update_btn = button("Update")
|
||||
.on_press(Message::DoSelfUpdate)
|
||||
.padding(5)
|
||||
.style(style::Button::Primary);
|
||||
|
||||
#[cfg(feature = "self-update")]
|
||||
let uad_version_text =
|
||||
text(format!("UAD version: v{}", env!("CARGO_PKG_VERSION"))).width(250);
|
||||
|
||||
#[cfg(feature = "self-update")]
|
||||
#[rustfmt::skip]
|
||||
let self_update_text = update_state.self_update.latest_release.as_ref().map_or_else(||
|
||||
if update_state.self_update.status == SelfUpdateStatus::Done {
|
||||
"(No update available)".to_string()
|
||||
} else {
|
||||
update_state.self_update.status.to_string()
|
||||
}, |r| if update_state.self_update.status == SelfUpdateStatus::Updating {
|
||||
update_state.self_update.status.to_string()
|
||||
} else {
|
||||
format!("(v{} available)", r.tag_name)
|
||||
});
|
||||
|
||||
#[cfg(feature = "self-update")]
|
||||
let last_self_update_text = text(self_update_text).style(style::Text::Default);
|
||||
|
||||
#[cfg(feature = "self-update")]
|
||||
let self_update_row = row![uad_version_text, self_update_btn, last_self_update_text,]
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(10)
|
||||
.width(550);
|
||||
|
||||
let uad_list_row = row![uad_list_text, uad_lists_btn, last_update_text,]
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(10)
|
||||
.width(550);
|
||||
|
||||
#[cfg(feature = "self-update")]
|
||||
let update_column = column![uad_list_row, self_update_row]
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(10);
|
||||
|
||||
#[cfg(not(feature = "self-update"))]
|
||||
let update_column = column![uad_list_row]
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(10);
|
||||
|
||||
let update_container = container(update_column)
|
||||
.width(Length::Fill)
|
||||
.center_x()
|
||||
.padding(10)
|
||||
.style(style::Container::Frame);
|
||||
|
||||
let website_btn = button("Github page")
|
||||
.on_press(Message::UrlPressed(PathBuf::from(
|
||||
"https://github.com/Universal-Debloater-Alliance/universal-android-debloater",
|
||||
)))
|
||||
.padding(5)
|
||||
.style(style::Button::Primary);
|
||||
|
||||
let issue_btn = button("Have an issue?")
|
||||
.on_press(Message::UrlPressed(PathBuf::from(
|
||||
"https://github.com/Universal-Debloater-Alliance/universal-android-debloater/issues",
|
||||
)))
|
||||
.padding(5)
|
||||
.style(style::Button::Primary);
|
||||
|
||||
let log_btn = button("Locate the logfiles")
|
||||
.on_press(Message::UrlPressed(CACHE_DIR.to_path_buf()))
|
||||
.padding(5)
|
||||
.style(style::Button::Primary);
|
||||
|
||||
let wiki_btn = button("Wiki")
|
||||
.on_press(Message::UrlPressed(PathBuf::from(
|
||||
"https://github.com/Universal-Debloater-Alliance/universal-android-debloater/wiki",
|
||||
)))
|
||||
.padding(5)
|
||||
.style(style::Button::Primary);
|
||||
|
||||
let row = row![website_btn, wiki_btn, issue_btn, log_btn,].spacing(20);
|
||||
|
||||
let content = column![
|
||||
Space::new(Length::Fill, Length::Shrink),
|
||||
descr_container,
|
||||
update_container,
|
||||
row,
|
||||
]
|
||||
.width(Length::Fill)
|
||||
.spacing(20)
|
||||
.align_items(Alignment::Center);
|
||||
|
||||
container(content)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.padding(10)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,840 +0,0 @@
|
|||
use crate::core::config::DeviceSettings;
|
||||
use crate::core::sync::{apply_pkg_state_commands, perform_adb_commands, CommandType, Phone, User};
|
||||
use crate::core::theme::Theme;
|
||||
use crate::core::uad_lists::{
|
||||
load_debloat_lists, Opposite, Package, PackageState, Removal, UadList, UadListState,
|
||||
};
|
||||
use crate::core::utils::fetch_packages;
|
||||
use crate::gui::style;
|
||||
use crate::gui::widgets::navigation_menu::ICONS;
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
|
||||
use crate::gui::views::settings::Settings;
|
||||
use crate::gui::widgets::modal::Modal;
|
||||
use crate::gui::widgets::package_row::{Message as RowMessage, PackageRow};
|
||||
use iced::widget::{
|
||||
button, column, container, horizontal_space, pick_list, radio, row, scrollable, text,
|
||||
text_input, tooltip, vertical_rule, Space,
|
||||
};
|
||||
use iced::{alignment, Alignment, Command, Element, Length, Renderer};
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct PackageInfo {
|
||||
pub i_user: usize,
|
||||
pub index: usize,
|
||||
pub removal: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum LoadingState {
|
||||
DownloadingList(String),
|
||||
FindingPhones(String),
|
||||
LoadingPackages(String),
|
||||
_UpdatingUad(String),
|
||||
Ready(String),
|
||||
RestoringDevice(String),
|
||||
}
|
||||
|
||||
impl Default for LoadingState {
|
||||
fn default() -> Self {
|
||||
Self::FindingPhones(String::new())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct List {
|
||||
pub loading_state: LoadingState,
|
||||
pub uad_lists: HashMap<String, Package>,
|
||||
pub phone_packages: Vec<Vec<PackageRow>>, // packages of all users of the phone
|
||||
filtered_packages: Vec<usize>, // phone_packages indexes of the selected user (= what you see on screen)
|
||||
selected_packages: Vec<(usize, usize)>, // Vec of (user_index, pkg_index)
|
||||
selected_package_state: Option<PackageState>,
|
||||
selected_removal: Option<Removal>,
|
||||
selected_list: Option<UadList>,
|
||||
selected_user: Option<User>,
|
||||
pub input_value: String,
|
||||
description: String,
|
||||
selection_modal: bool,
|
||||
current_package_index: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Message {
|
||||
LoadUadList(bool),
|
||||
LoadPhonePackages((HashMap<String, Package>, UadListState)),
|
||||
RestoringDevice(Result<CommandType, ()>),
|
||||
ApplyFilters(Vec<Vec<PackageRow>>),
|
||||
SearchInputChanged(String),
|
||||
ToggleAllSelected(bool),
|
||||
ListSelected(UadList),
|
||||
UserSelected(User),
|
||||
PackageStateSelected(PackageState),
|
||||
RemovalSelected(Removal),
|
||||
ApplyActionOnSelection,
|
||||
List(usize, RowMessage),
|
||||
ChangePackageState(Result<CommandType, ()>),
|
||||
Nothing,
|
||||
ModalHide,
|
||||
ModalUserSelected(User),
|
||||
ModalValidate,
|
||||
}
|
||||
|
||||
impl List {
|
||||
pub fn update(
|
||||
&mut self,
|
||||
settings: &mut Settings,
|
||||
selected_device: &mut Phone,
|
||||
list_update_state: &mut UadListState,
|
||||
message: Message,
|
||||
) -> Command<Message> {
|
||||
let i_user = self.selected_user.unwrap_or_default().index;
|
||||
match message {
|
||||
Message::ModalHide => {
|
||||
self.selection_modal = false;
|
||||
Command::none()
|
||||
}
|
||||
Message::ModalValidate => {
|
||||
let mut commands = vec![];
|
||||
self.selected_packages.sort_unstable();
|
||||
self.selected_packages.dedup();
|
||||
for selection in &self.selected_packages {
|
||||
commands.append(&mut build_action_pkg_commands(
|
||||
&self.phone_packages,
|
||||
selected_device,
|
||||
&settings.device,
|
||||
*selection,
|
||||
));
|
||||
}
|
||||
self.selection_modal = false;
|
||||
Command::batch(commands)
|
||||
}
|
||||
Message::RestoringDevice(output) => {
|
||||
if let Ok(res) = output {
|
||||
if let CommandType::PackageManager(p) = res {
|
||||
self.loading_state = LoadingState::RestoringDevice(
|
||||
self.phone_packages[i_user][p.index].name.clone(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
self.loading_state = LoadingState::RestoringDevice("Error [TODO]".to_string());
|
||||
}
|
||||
Command::none()
|
||||
}
|
||||
Message::LoadUadList(remote) => {
|
||||
info!("{:-^65}", "-");
|
||||
info!(
|
||||
"ANDROID_SDK: {} | DEVICE: {}",
|
||||
selected_device.android_sdk, selected_device.model
|
||||
);
|
||||
info!("{:-^65}", "-");
|
||||
self.loading_state = LoadingState::DownloadingList(String::new());
|
||||
Command::perform(
|
||||
Self::init_apps_view(remote, selected_device.clone()),
|
||||
Message::LoadPhonePackages,
|
||||
)
|
||||
}
|
||||
Message::LoadPhonePackages(list_box) => {
|
||||
let (uad_list, list_state) = list_box;
|
||||
self.loading_state = LoadingState::LoadingPackages(String::new());
|
||||
self.uad_lists = uad_list.clone();
|
||||
*list_update_state = list_state;
|
||||
Command::perform(
|
||||
Self::load_packages(uad_list, selected_device.user_list.clone()),
|
||||
Message::ApplyFilters,
|
||||
)
|
||||
}
|
||||
Message::ApplyFilters(packages) => {
|
||||
self.phone_packages = packages;
|
||||
self.filtered_packages = (0..self.phone_packages[i_user].len()).collect();
|
||||
self.selected_package_state = Some(PackageState::Enabled);
|
||||
self.selected_removal = Some(Removal::Recommended);
|
||||
self.selected_list = Some(UadList::All);
|
||||
self.selected_user = Some(User::default());
|
||||
Self::filter_package_lists(self);
|
||||
self.loading_state = LoadingState::Ready(String::new());
|
||||
Command::none()
|
||||
}
|
||||
Message::ToggleAllSelected(selected) => {
|
||||
#[allow(unused_must_use)]
|
||||
for i in self.filtered_packages.clone() {
|
||||
if self.phone_packages[i_user][i].selected != selected {
|
||||
self.update(
|
||||
settings,
|
||||
selected_device,
|
||||
list_update_state,
|
||||
Message::List(i, RowMessage::ToggleSelection(selected)),
|
||||
);
|
||||
}
|
||||
}
|
||||
Command::none()
|
||||
}
|
||||
Message::SearchInputChanged(letter) => {
|
||||
self.input_value = letter;
|
||||
Self::filter_package_lists(self);
|
||||
Command::none()
|
||||
}
|
||||
Message::ListSelected(list) => {
|
||||
self.selected_list = Some(list);
|
||||
Self::filter_package_lists(self);
|
||||
Command::none()
|
||||
}
|
||||
Message::PackageStateSelected(package_state) => {
|
||||
self.selected_package_state = Some(package_state);
|
||||
Self::filter_package_lists(self);
|
||||
Command::none()
|
||||
}
|
||||
Message::RemovalSelected(removal) => {
|
||||
self.selected_removal = Some(removal);
|
||||
Self::filter_package_lists(self);
|
||||
Command::none()
|
||||
}
|
||||
Message::List(i_package, row_message) => {
|
||||
#[allow(unused_must_use)]
|
||||
{
|
||||
self.phone_packages[i_user][i_package]
|
||||
.update(&row_message)
|
||||
.map(move |row_message| Message::List(i_package, row_message));
|
||||
}
|
||||
|
||||
let package = &mut self.phone_packages[i_user][i_package];
|
||||
|
||||
match row_message {
|
||||
RowMessage::ToggleSelection(toggle) => {
|
||||
if package.removal == Removal::Unsafe && !settings.general.expert_mode {
|
||||
package.selected = false;
|
||||
return Command::none();
|
||||
}
|
||||
|
||||
if settings.device.multi_user_mode {
|
||||
for u in selected_device.user_list.iter().filter(|&u| !u.protected) {
|
||||
self.phone_packages[u.index][i_package].selected = toggle;
|
||||
if toggle {
|
||||
self.selected_packages.push((u.index, i_package));
|
||||
}
|
||||
}
|
||||
if !toggle {
|
||||
self.selected_packages.retain(|&x| x.1 != i_package);
|
||||
}
|
||||
} else {
|
||||
package.selected = toggle;
|
||||
if toggle {
|
||||
self.selected_packages.push((i_user, i_package));
|
||||
} else {
|
||||
self.selected_packages
|
||||
.retain(|&x| x.1 != i_package || x.0 != i_user);
|
||||
}
|
||||
}
|
||||
Command::none()
|
||||
}
|
||||
RowMessage::ActionPressed => {
|
||||
self.phone_packages[i_user][i_package].selected = true;
|
||||
Command::batch(build_action_pkg_commands(
|
||||
&self.phone_packages,
|
||||
selected_device,
|
||||
&settings.device,
|
||||
(i_user, i_package),
|
||||
))
|
||||
}
|
||||
RowMessage::PackagePressed => {
|
||||
self.description = package.clone().description;
|
||||
package.current = true;
|
||||
if self.current_package_index != i_package {
|
||||
self.phone_packages[i_user][self.current_package_index].current = false;
|
||||
}
|
||||
self.current_package_index = i_package;
|
||||
Command::none()
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::ApplyActionOnSelection => {
|
||||
self.selection_modal = true;
|
||||
Command::none()
|
||||
}
|
||||
Message::UserSelected(user) => {
|
||||
self.selected_user = Some(user);
|
||||
self.filtered_packages = (0..self.phone_packages[user.index].len()).collect();
|
||||
Self::filter_package_lists(self);
|
||||
Command::none()
|
||||
}
|
||||
Message::ChangePackageState(res) => {
|
||||
if let Ok(CommandType::PackageManager(p)) = res {
|
||||
let package = &mut self.phone_packages[p.i_user][p.index];
|
||||
package.state = package.state.opposite(settings.device.disable_mode);
|
||||
package.selected = false;
|
||||
self.selected_packages
|
||||
.retain(|&x| x.1 != p.index && x.0 != p.i_user);
|
||||
Self::filter_package_lists(self);
|
||||
}
|
||||
Command::none()
|
||||
}
|
||||
Message::ModalUserSelected(user) => {
|
||||
self.selected_user = Some(user);
|
||||
self.update(
|
||||
settings,
|
||||
selected_device,
|
||||
list_update_state,
|
||||
Message::UserSelected(user),
|
||||
)
|
||||
}
|
||||
Message::Nothing => Command::none(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view(
|
||||
&self,
|
||||
settings: &Settings,
|
||||
selected_device: &Phone,
|
||||
) -> Element<Message, Renderer<Theme>> {
|
||||
match &self.loading_state {
|
||||
LoadingState::DownloadingList(_) => {
|
||||
let text = "Downloading latest UAD lists from Github. Please wait...";
|
||||
waiting_view(settings, text, true)
|
||||
}
|
||||
LoadingState::FindingPhones(_) => {
|
||||
let text = "Finding connected devices...";
|
||||
waiting_view(settings, text, false)
|
||||
}
|
||||
LoadingState::LoadingPackages(_) => {
|
||||
let text = "Pulling packages from the device. Please wait...";
|
||||
waiting_view(settings, text, false)
|
||||
}
|
||||
LoadingState::_UpdatingUad(_) => {
|
||||
let text = "Updating UAD. Please wait...";
|
||||
waiting_view(settings, text, false)
|
||||
}
|
||||
LoadingState::RestoringDevice(output) => {
|
||||
let text = format!("Restoring device: {output}");
|
||||
waiting_view(settings, &text, false)
|
||||
}
|
||||
LoadingState::Ready(_) => {
|
||||
let search_packages = text_input("Search packages...", &self.input_value)
|
||||
.on_input(Message::SearchInputChanged)
|
||||
.padding(5);
|
||||
|
||||
let user_picklist = pick_list(
|
||||
selected_device.user_list.clone(),
|
||||
self.selected_user,
|
||||
Message::UserSelected,
|
||||
)
|
||||
.width(85);
|
||||
|
||||
let divider = Space::new(Length::Fill, Length::Shrink);
|
||||
|
||||
let list_picklist =
|
||||
pick_list(&UadList::ALL[..], self.selected_list, Message::ListSelected);
|
||||
let package_state_picklist = pick_list(
|
||||
&PackageState::ALL[..],
|
||||
self.selected_package_state,
|
||||
Message::PackageStateSelected,
|
||||
);
|
||||
|
||||
let removal_picklist = pick_list(
|
||||
&Removal::ALL[..],
|
||||
self.selected_removal,
|
||||
Message::RemovalSelected,
|
||||
);
|
||||
|
||||
let control_panel = row![
|
||||
search_packages,
|
||||
user_picklist,
|
||||
divider,
|
||||
removal_picklist,
|
||||
package_state_picklist,
|
||||
list_picklist,
|
||||
]
|
||||
.width(Length::Fill)
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(10)
|
||||
.padding([0, 16, 0, 0]);
|
||||
|
||||
let packages =
|
||||
self.filtered_packages
|
||||
.iter()
|
||||
.fold(column![].spacing(6), |col, i| {
|
||||
col.push(
|
||||
self.phone_packages[self.selected_user.unwrap().index][*i]
|
||||
.view(settings, selected_device)
|
||||
.map(move |msg| Message::List(*i, msg)),
|
||||
)
|
||||
});
|
||||
|
||||
let packages_scrollable = scrollable(packages)
|
||||
.height(Length::FillPortion(6))
|
||||
.style(style::Scrollable::Packages);
|
||||
|
||||
let description_scroll = scrollable(text(&self.description).width(Length::Fill))
|
||||
.style(style::Scrollable::Description);
|
||||
|
||||
let description_panel = container(description_scroll)
|
||||
.padding(6)
|
||||
.height(Length::FillPortion(2))
|
||||
.width(Length::Fill)
|
||||
.style(style::Container::Frame);
|
||||
|
||||
let review_selection = if !self.selected_packages.is_empty() {
|
||||
button(text(format!(
|
||||
"Review selection ({})",
|
||||
self.selected_packages.len()
|
||||
)))
|
||||
.on_press(Message::ApplyActionOnSelection)
|
||||
.padding(5)
|
||||
.style(style::Button::Primary)
|
||||
} else {
|
||||
button(text(format!(
|
||||
"Review selection ({})",
|
||||
self.selected_packages.len()
|
||||
)))
|
||||
.padding(5)
|
||||
};
|
||||
|
||||
let select_all_btn = button("Select all")
|
||||
.padding(5)
|
||||
.on_press(Message::ToggleAllSelected(true))
|
||||
.style(style::Button::Primary);
|
||||
|
||||
let unselect_all_btn = button("Unselect all")
|
||||
.padding(5)
|
||||
.on_press(Message::ToggleAllSelected(false))
|
||||
.style(style::Button::Primary);
|
||||
|
||||
let action_row = row![
|
||||
select_all_btn,
|
||||
unselect_all_btn,
|
||||
Space::new(Length::Fill, Length::Shrink),
|
||||
review_selection,
|
||||
]
|
||||
.width(Length::Fill)
|
||||
.spacing(10)
|
||||
.align_items(Alignment::Center);
|
||||
|
||||
let unavailable = container(
|
||||
column![
|
||||
text("ADB is not authorized to access this user!").size(22)
|
||||
.style(style::Text::Danger),
|
||||
text("The most likely reason is that it is the user of your work profile (also called Secure Folder on Samsung devices). There's really no solution, other than completely disabling your work profile in your device settings.")
|
||||
.style(style::Text::Commentary)
|
||||
.horizontal_alignment(alignment::Horizontal::Center),
|
||||
]
|
||||
.spacing(6)
|
||||
.align_items(Alignment::Center)
|
||||
)
|
||||
.padding(10)
|
||||
.center_x()
|
||||
.style(style::Container::BorderedFrame);
|
||||
|
||||
let content = if selected_device.user_list.is_empty()
|
||||
|| !self.phone_packages[self.selected_user.unwrap().index].is_empty()
|
||||
{
|
||||
column![
|
||||
control_panel,
|
||||
packages_scrollable,
|
||||
description_panel,
|
||||
action_row,
|
||||
]
|
||||
.width(Length::Fill)
|
||||
.spacing(10)
|
||||
.align_items(Alignment::Center)
|
||||
} else {
|
||||
column![
|
||||
control_panel,
|
||||
container(unavailable).height(Length::Fill).center_y(),
|
||||
]
|
||||
.width(Length::Fill)
|
||||
.spacing(10)
|
||||
.align_items(Alignment::Center)
|
||||
};
|
||||
if self.selection_modal {
|
||||
Modal::new(
|
||||
content.padding(10),
|
||||
self.apply_selection_modal(
|
||||
selected_device,
|
||||
settings,
|
||||
&self.phone_packages[self.selected_user.unwrap().index],
|
||||
),
|
||||
)
|
||||
.on_blur(Message::ModalHide)
|
||||
.into()
|
||||
} else {
|
||||
container(content).height(Length::Fill).padding(10).into()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_selection_modal(
|
||||
&self,
|
||||
device: &Phone,
|
||||
settings: &Settings,
|
||||
packages: &[PackageRow],
|
||||
) -> Element<Message, Renderer<Theme>> {
|
||||
// (nb_to_restore, nb_to_remove)
|
||||
let mut h_recap: HashMap<Removal, (u8, u8)> = HashMap::new();
|
||||
for p in packages.iter().filter(|p| p.selected) {
|
||||
if p.state == PackageState::Uninstalled || p.state == PackageState::Disabled {
|
||||
h_recap.entry(p.removal).or_insert((0, 0)).1 += 1;
|
||||
} else {
|
||||
h_recap.entry(p.removal).or_insert((0, 0)).0 += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let radio_btn_users = device.user_list.iter().filter(|&u| !u.protected).fold(
|
||||
row![].spacing(10),
|
||||
|row, user| {
|
||||
row.push(
|
||||
radio(
|
||||
format!("{}", user.clone()),
|
||||
*user,
|
||||
self.selected_user,
|
||||
Message::ModalUserSelected,
|
||||
)
|
||||
.size(23),
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
let title_ctn =
|
||||
container(row![text("Review your selection").size(25)].align_items(Alignment::Center))
|
||||
.width(Length::Fill)
|
||||
.style(style::Container::Frame)
|
||||
.padding([10, 0, 10, 0])
|
||||
.center_y()
|
||||
.center_x();
|
||||
|
||||
let users_ctn = container(radio_btn_users)
|
||||
.padding(10)
|
||||
.center_x()
|
||||
.style(style::Container::Frame);
|
||||
|
||||
let explaination_ctn = container(
|
||||
row![
|
||||
text("The action for the selected user will be applied to all other users")
|
||||
.style(style::Text::Danger),
|
||||
tooltip(
|
||||
text("\u{EA0C}")
|
||||
.font(ICONS)
|
||||
.width(17)
|
||||
.horizontal_alignment(alignment::Horizontal::Center)
|
||||
.style(style::Text::Commentary)
|
||||
.size(17),
|
||||
"Let's say you choose user 0. If a selected package on user 0\n\
|
||||
is set to be uninstalled and if this same package is disabled on user 10,\n\
|
||||
then the package on both users will be uninstalled.",
|
||||
tooltip::Position::Top,
|
||||
)
|
||||
.gap(20)
|
||||
.padding(10)
|
||||
.size(17)
|
||||
.style(style::Container::Tooltip)
|
||||
]
|
||||
.spacing(10),
|
||||
)
|
||||
.center_x()
|
||||
.padding(10)
|
||||
.style(style::Container::BorderedFrame);
|
||||
|
||||
let modal_btn_row = row![
|
||||
button(text("Cancel")).on_press(Message::ModalHide),
|
||||
horizontal_space(Length::Fill),
|
||||
button(text("Apply")).on_press(Message::ModalValidate),
|
||||
]
|
||||
.padding([0, 15, 10, 10]);
|
||||
|
||||
let recap_view = Removal::ALL
|
||||
.iter()
|
||||
.filter(|&&r| r != Removal::All)
|
||||
.fold(column![].spacing(6).width(Length::Fill), |col, r| {
|
||||
col.push(recap(settings, &mut h_recap, *r))
|
||||
});
|
||||
|
||||
let selected_pkgs_ctn = container(
|
||||
container(
|
||||
scrollable(
|
||||
container(
|
||||
if !self
|
||||
.selected_packages
|
||||
.iter()
|
||||
.any(|s| s.0 == self.selected_user.unwrap().index)
|
||||
{
|
||||
column![text("No packages selected for this user")]
|
||||
.align_items(Alignment::Center)
|
||||
.width(Length::Fill)
|
||||
} else {
|
||||
self.selected_packages
|
||||
.iter()
|
||||
.filter(|s| s.0 == self.selected_user.unwrap().index)
|
||||
.fold(
|
||||
column![].spacing(6).width(Length::Fill),
|
||||
|col, selection| {
|
||||
col.push(
|
||||
row![
|
||||
row![text(
|
||||
self.phone_packages[selection.0][selection.1]
|
||||
.removal
|
||||
)]
|
||||
.width(100),
|
||||
row![text(
|
||||
self.phone_packages[selection.0][selection.1]
|
||||
.uad_list
|
||||
)]
|
||||
.width(60),
|
||||
row![text(
|
||||
self.phone_packages[selection.0][selection.1]
|
||||
.name
|
||||
.clone()
|
||||
),],
|
||||
horizontal_space(Length::Fill),
|
||||
row![match self.phone_packages[selection.0]
|
||||
[selection.1]
|
||||
.state
|
||||
{
|
||||
PackageState::Enabled =>
|
||||
if settings.device.disable_mode {
|
||||
text("Disable")
|
||||
.style(style::Text::Danger)
|
||||
} else {
|
||||
text("Uninstall")
|
||||
.style(style::Text::Danger)
|
||||
},
|
||||
PackageState::Disabled =>
|
||||
text("Enable").style(style::Text::Ok),
|
||||
PackageState::Uninstalled =>
|
||||
text("Restore").style(style::Text::Ok),
|
||||
PackageState::All => text("Impossible")
|
||||
.style(style::Text::Danger),
|
||||
},]
|
||||
.width(60),
|
||||
]
|
||||
.width(Length::Fill)
|
||||
.spacing(20),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
.padding(10)
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.style(style::Scrollable::Description),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.style(style::Container::Frame),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.max_height(150)
|
||||
.padding([0, 10, 0, 10]);
|
||||
|
||||
container(
|
||||
if device.user_list.iter().filter(|&u| !u.protected).count() > 1
|
||||
&& settings.device.multi_user_mode
|
||||
{
|
||||
column![
|
||||
title_ctn,
|
||||
users_ctn,
|
||||
row![explaination_ctn].padding([0, 10, 0, 10]),
|
||||
container(recap_view).padding(10),
|
||||
selected_pkgs_ctn,
|
||||
modal_btn_row,
|
||||
]
|
||||
.spacing(10)
|
||||
.align_items(Alignment::Center)
|
||||
} else if !settings.device.multi_user_mode {
|
||||
column![
|
||||
title_ctn,
|
||||
users_ctn,
|
||||
container(recap_view).padding(10),
|
||||
selected_pkgs_ctn,
|
||||
modal_btn_row,
|
||||
]
|
||||
.spacing(10)
|
||||
.align_items(Alignment::Center)
|
||||
} else {
|
||||
column![
|
||||
title_ctn,
|
||||
container(recap_view).padding(10),
|
||||
selected_pkgs_ctn,
|
||||
modal_btn_row,
|
||||
]
|
||||
.spacing(10)
|
||||
.align_items(Alignment::Center)
|
||||
},
|
||||
)
|
||||
.width(800)
|
||||
.height(Length::Shrink)
|
||||
.max_height(700)
|
||||
.style(style::Container::Background)
|
||||
.into()
|
||||
}
|
||||
fn filter_package_lists(&mut self) {
|
||||
let list_filter: UadList = self.selected_list.unwrap();
|
||||
let package_filter: PackageState = self.selected_package_state.unwrap();
|
||||
let removal_filter: Removal = self.selected_removal.unwrap();
|
||||
|
||||
self.filtered_packages = self.phone_packages[self.selected_user.unwrap().index]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, p)| {
|
||||
(list_filter == UadList::All || p.uad_list == list_filter)
|
||||
&& (package_filter == PackageState::All || p.state == package_filter)
|
||||
&& (removal_filter == Removal::All || p.removal == removal_filter)
|
||||
&& (self.input_value.is_empty() || p.name.contains(&self.input_value))
|
||||
})
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
}
|
||||
|
||||
async fn load_packages(
|
||||
uad_list: HashMap<String, Package>,
|
||||
user_list: Vec<User>,
|
||||
) -> Vec<Vec<PackageRow>> {
|
||||
let mut phone_packages = vec![];
|
||||
|
||||
if user_list.len() <= 1 {
|
||||
phone_packages.push(fetch_packages(&uad_list, None));
|
||||
} else {
|
||||
phone_packages.extend(
|
||||
user_list
|
||||
.iter()
|
||||
.map(|user| fetch_packages(&uad_list, Some(user))),
|
||||
);
|
||||
};
|
||||
phone_packages
|
||||
}
|
||||
|
||||
async fn init_apps_view(
|
||||
remote: bool,
|
||||
phone: Phone,
|
||||
) -> (HashMap<String, Package>, UadListState) {
|
||||
let (uad_lists, _) = load_debloat_lists(remote);
|
||||
match uad_lists {
|
||||
Ok(list) => {
|
||||
env::set_var("ANDROID_SERIAL", phone.adb_id.clone());
|
||||
if phone.adb_id.is_empty() {
|
||||
error!("AppsView ready but no phone found");
|
||||
}
|
||||
(list, UadListState::Done)
|
||||
}
|
||||
Err(local_list) => {
|
||||
error!("Error loading remote debloat list for the phone. Fallback to embedded (and outdated) list");
|
||||
(local_list, UadListState::Failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn waiting_view<'a>(
|
||||
_settings: &Settings,
|
||||
displayed_text: &str,
|
||||
btn: bool,
|
||||
) -> Element<'a, Message, Renderer<Theme>> {
|
||||
let col = if btn {
|
||||
let no_internet_btn = button("No internet?")
|
||||
.padding(5)
|
||||
.on_press(Message::LoadUadList(false))
|
||||
.style(style::Button::Primary);
|
||||
|
||||
column![]
|
||||
.spacing(10)
|
||||
.align_items(Alignment::Center)
|
||||
.push(text(displayed_text).size(20))
|
||||
.push(no_internet_btn)
|
||||
} else {
|
||||
column![]
|
||||
.spacing(10)
|
||||
.align_items(Alignment::Center)
|
||||
.push(text(displayed_text).size(20))
|
||||
};
|
||||
|
||||
container(col)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.center_y()
|
||||
.center_x()
|
||||
.style(style::Container::default())
|
||||
.into()
|
||||
}
|
||||
|
||||
fn build_action_pkg_commands(
|
||||
packages: &[Vec<PackageRow>],
|
||||
device: &Phone,
|
||||
settings: &DeviceSettings,
|
||||
selection: (usize, usize),
|
||||
) -> Vec<Command<Message>> {
|
||||
let pkg = &packages[selection.0][selection.1];
|
||||
let wanted_state = pkg.state.opposite(settings.disable_mode);
|
||||
|
||||
let mut commands = vec![];
|
||||
for u in device.user_list.iter().filter(|&&u| {
|
||||
!u.protected && (packages[u.index][selection.1].selected || settings.multi_user_mode)
|
||||
}) {
|
||||
let u_pkg = packages[u.index][selection.1].clone();
|
||||
let actions = if settings.multi_user_mode {
|
||||
apply_pkg_state_commands(&u_pkg.into(), wanted_state, u, device)
|
||||
} else {
|
||||
let wanted_state = u_pkg.state.opposite(settings.disable_mode);
|
||||
apply_pkg_state_commands(&u_pkg.into(), wanted_state, u, device)
|
||||
};
|
||||
for (j, action) in actions.into_iter().enumerate() {
|
||||
let p_info = PackageInfo {
|
||||
i_user: u.index,
|
||||
index: selection.1,
|
||||
removal: pkg.removal.to_string(),
|
||||
};
|
||||
// In the end there is only one package state change
|
||||
// even if we run multiple adb commands
|
||||
commands.push(Command::perform(
|
||||
perform_adb_commands(action, CommandType::PackageManager(p_info)),
|
||||
if j == 0 {
|
||||
Message::ChangePackageState
|
||||
} else {
|
||||
|_| Message::Nothing
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
commands
|
||||
}
|
||||
|
||||
fn recap<'a>(
|
||||
settings: &Settings,
|
||||
recap: &mut HashMap<Removal, (u8, u8)>,
|
||||
removal: Removal,
|
||||
) -> Element<'a, Message, Renderer<Theme>> {
|
||||
container(
|
||||
row![
|
||||
text(removal).size(25).width(Length::FillPortion(1)),
|
||||
vertical_rule(5),
|
||||
row![
|
||||
if settings.device.disable_mode {
|
||||
text("Disable").style(style::Text::Danger)
|
||||
} else {
|
||||
text("Uninstall").style(style::Text::Danger)
|
||||
},
|
||||
horizontal_space(Length::Fill),
|
||||
text(recap.entry(removal).or_insert((0, 0)).0.to_string())
|
||||
.style(style::Text::Danger)
|
||||
]
|
||||
.width(Length::FillPortion(1)),
|
||||
vertical_rule(5),
|
||||
row![
|
||||
if settings.device.disable_mode {
|
||||
text("Enable").style(style::Text::Ok)
|
||||
} else {
|
||||
text("Restore").style(style::Text::Ok)
|
||||
},
|
||||
horizontal_space(Length::Fill),
|
||||
text(recap.entry(removal).or_insert((0, 0)).1.to_string()).style(style::Text::Ok)
|
||||
]
|
||||
.width(Length::FillPortion(1))
|
||||
]
|
||||
.spacing(20)
|
||||
.padding([0, 10, 0, 0])
|
||||
.width(Length::Fill)
|
||||
.align_items(Alignment::Center),
|
||||
)
|
||||
.padding(10)
|
||||
.width(Length::Fill)
|
||||
.height(45)
|
||||
.style(style::Container::Frame)
|
||||
.into()
|
||||
}
|
||||
|
|
@ -1,427 +0,0 @@
|
|||
use crate::core::config::{BackupSettings, Config, DeviceSettings, GeneralSettings};
|
||||
use crate::core::save::{
|
||||
backup_phone, list_available_backup_user, list_available_backups, restore_backup, BACKUP_DIR,
|
||||
};
|
||||
use crate::core::sync::{get_android_sdk, perform_adb_commands, CommandType, Phone};
|
||||
use crate::core::theme::Theme;
|
||||
use crate::core::utils::{open_url, string_to_theme, DisplayablePath};
|
||||
use crate::gui::style;
|
||||
use crate::gui::views::list::PackageInfo;
|
||||
use crate::gui::widgets::package_row::PackageRow;
|
||||
|
||||
use iced::widget::{button, checkbox, column, container, pick_list, radio, row, text, Space};
|
||||
use iced::{alignment, Alignment, Command, Element, Length, Renderer};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Settings {
|
||||
pub general: GeneralSettings,
|
||||
pub device: DeviceSettings,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
general: Config::load_configuration_file().general,
|
||||
device: DeviceSettings::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Message {
|
||||
LoadDeviceSettings,
|
||||
ExpertMode(bool),
|
||||
DisableMode(bool),
|
||||
MultiUserMode(bool),
|
||||
ApplyTheme(Theme),
|
||||
UrlPressed(PathBuf),
|
||||
BackupSelected(DisplayablePath),
|
||||
BackupDevice,
|
||||
RestoreDevice,
|
||||
RestoringDevice(Result<CommandType, ()>),
|
||||
DeviceBackedUp(Result<(), String>),
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
pub fn update(
|
||||
&mut self,
|
||||
phone: &Phone,
|
||||
packages: &[Vec<PackageRow>],
|
||||
nb_running_async_adb_commands: &mut u32,
|
||||
msg: Message,
|
||||
) -> Command<Message> {
|
||||
match msg {
|
||||
Message::ExpertMode(toggled) => {
|
||||
self.general.expert_mode = toggled;
|
||||
debug!("Config change: {:?}", self);
|
||||
Config::save_changes(self, &phone.adb_id);
|
||||
Command::none()
|
||||
}
|
||||
Message::DisableMode(toggled) => {
|
||||
if phone.android_sdk >= 23 {
|
||||
self.device.disable_mode = toggled;
|
||||
debug!("Config change: {:?}", self);
|
||||
Config::save_changes(self, &phone.adb_id);
|
||||
}
|
||||
Command::none()
|
||||
}
|
||||
Message::MultiUserMode(toggled) => {
|
||||
self.device.multi_user_mode = toggled;
|
||||
debug!("Config change: {:?}", self);
|
||||
Config::save_changes(self, &phone.adb_id);
|
||||
Command::none()
|
||||
}
|
||||
Message::ApplyTheme(theme) => {
|
||||
self.general.theme = theme.to_string();
|
||||
debug!("Config change: {:?}", self);
|
||||
Config::save_changes(self, &phone.adb_id);
|
||||
Command::none()
|
||||
}
|
||||
Message::UrlPressed(url) => {
|
||||
open_url(url);
|
||||
Command::none()
|
||||
}
|
||||
Message::LoadDeviceSettings => {
|
||||
let backups = list_available_backups(&BACKUP_DIR.join(phone.adb_id.clone()));
|
||||
match Config::load_configuration_file()
|
||||
.devices
|
||||
.iter()
|
||||
.find(|d| d.device_id == phone.adb_id)
|
||||
{
|
||||
Some(device) => {
|
||||
self.device = device.clone();
|
||||
self.device.backup = BackupSettings {
|
||||
backups: backups.clone(),
|
||||
selected: backups.first().cloned(),
|
||||
users: phone.user_list.clone(),
|
||||
selected_user: phone.user_list.first().copied(),
|
||||
backup_state: String::new(),
|
||||
};
|
||||
}
|
||||
None => {
|
||||
self.device = DeviceSettings {
|
||||
device_id: phone.adb_id.clone(),
|
||||
multi_user_mode: phone.android_sdk > 21,
|
||||
disable_mode: false,
|
||||
backup: BackupSettings {
|
||||
backups: backups.clone(),
|
||||
selected: backups.first().cloned(),
|
||||
users: phone.user_list.clone(),
|
||||
selected_user: phone.user_list.first().copied(),
|
||||
backup_state: String::new(),
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
Command::none()
|
||||
}
|
||||
Message::BackupSelected(d_path) => {
|
||||
self.device.backup.selected = Some(d_path.clone());
|
||||
self.device.backup.users = list_available_backup_user(d_path);
|
||||
Command::none()
|
||||
}
|
||||
Message::BackupDevice => Command::perform(
|
||||
backup_phone(
|
||||
phone.user_list.clone(),
|
||||
self.device.device_id.clone(),
|
||||
packages.to_vec(),
|
||||
),
|
||||
Message::DeviceBackedUp,
|
||||
),
|
||||
Message::DeviceBackedUp(_) => {
|
||||
info!("[BACKUP] Backup successfully created");
|
||||
self.device.backup.backups =
|
||||
list_available_backups(&BACKUP_DIR.join(phone.adb_id.clone()));
|
||||
self.device.backup.selected = self.device.backup.backups.first().cloned();
|
||||
Command::none()
|
||||
}
|
||||
Message::RestoreDevice => match restore_backup(phone, packages, &self.device) {
|
||||
Ok(r_packages) => {
|
||||
let mut commands = vec![];
|
||||
*nb_running_async_adb_commands = 0;
|
||||
for p in &r_packages {
|
||||
let p_info = PackageInfo {
|
||||
i_user: 0,
|
||||
index: p.index,
|
||||
removal: "RESTORE".to_string(),
|
||||
};
|
||||
for command in p.commands.clone() {
|
||||
*nb_running_async_adb_commands += 1;
|
||||
commands.push(Command::perform(
|
||||
perform_adb_commands(
|
||||
command,
|
||||
CommandType::PackageManager(p_info.clone()),
|
||||
),
|
||||
Message::RestoringDevice,
|
||||
));
|
||||
}
|
||||
}
|
||||
if r_packages.is_empty() {
|
||||
if get_android_sdk() == 0 {
|
||||
self.device.backup.backup_state = "Device is not connected".to_string();
|
||||
} else {
|
||||
self.device.backup.backup_state =
|
||||
"Device state is already restored".to_string();
|
||||
}
|
||||
}
|
||||
info!(
|
||||
"[RESTORE] Restoring backup {}",
|
||||
self.device.backup.selected.as_ref().unwrap()
|
||||
);
|
||||
Command::batch(commands)
|
||||
}
|
||||
Err(e) => {
|
||||
self.device.backup.backup_state = e.to_string();
|
||||
error!("{} - {}", self.device.backup.selected.as_ref().unwrap(), e);
|
||||
Command::none()
|
||||
}
|
||||
},
|
||||
// Trigger an action in mod.rs (Message::SettingsAction(msg))
|
||||
Message::RestoringDevice(_) => Command::none(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view(&self, phone: &Phone) -> Element<Message, Renderer<Theme>> {
|
||||
let radio_btn_theme = Theme::ALL
|
||||
.iter()
|
||||
.fold(row![].spacing(10), |column, option| {
|
||||
column.push(
|
||||
radio(
|
||||
format!("{}", option.clone()),
|
||||
*option,
|
||||
Some(string_to_theme(&self.general.theme)),
|
||||
Message::ApplyTheme,
|
||||
)
|
||||
.size(23),
|
||||
)
|
||||
});
|
||||
let theme_ctn = container(radio_btn_theme)
|
||||
.padding(10)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Shrink)
|
||||
.style(style::Container::Frame);
|
||||
|
||||
let expert_mode_checkbox = checkbox(
|
||||
"Allow to uninstall packages marked as \"unsafe\" (I KNOW WHAT I AM DOING)",
|
||||
self.general.expert_mode,
|
||||
Message::ExpertMode,
|
||||
)
|
||||
.style(style::CheckBox::SettingsEnabled);
|
||||
|
||||
let expert_mode_descr =
|
||||
text("Most of unsafe packages are known to bootloop the device if removed.")
|
||||
.style(style::Text::Commentary)
|
||||
.size(15);
|
||||
|
||||
let general_ctn = container(column![expert_mode_checkbox, expert_mode_descr].spacing(10))
|
||||
.padding(10)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Shrink)
|
||||
.style(style::Container::Frame);
|
||||
|
||||
let warning_ctn = container(
|
||||
row![
|
||||
text("The following settings only affect the currently selected device :")
|
||||
.style(style::Text::Danger),
|
||||
text(phone.model.clone()),
|
||||
Space::new(Length::Fill, Length::Shrink),
|
||||
text(phone.adb_id.clone()).style(style::Text::Commentary)
|
||||
]
|
||||
.spacing(7),
|
||||
)
|
||||
.padding(10)
|
||||
.width(Length::Fill)
|
||||
.style(style::Container::BorderedFrame);
|
||||
|
||||
let multi_user_mode_descr = row![
|
||||
text("This will not affect the following protected work profile users: ")
|
||||
.size(15)
|
||||
.style(style::Text::Commentary),
|
||||
text(
|
||||
phone
|
||||
.user_list
|
||||
.iter()
|
||||
.filter(|&u| u.protected)
|
||||
.map(|u| u.id.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ")
|
||||
)
|
||||
.size(15)
|
||||
.style(style::Text::Danger)
|
||||
];
|
||||
|
||||
let multi_user_mode_checkbox = checkbox(
|
||||
"Affect all the users of the device (not only the selected user)",
|
||||
self.device.multi_user_mode,
|
||||
Message::MultiUserMode,
|
||||
)
|
||||
.style(style::CheckBox::SettingsEnabled);
|
||||
|
||||
let disable_checkbox_style = if phone.android_sdk >= 23 {
|
||||
style::CheckBox::SettingsEnabled
|
||||
} else {
|
||||
style::CheckBox::SettingsDisabled
|
||||
};
|
||||
|
||||
let disable_mode_descr =
|
||||
text("In some cases, it can be better to disable a package instead of uninstalling it")
|
||||
.style(style::Text::Commentary)
|
||||
.size(15);
|
||||
|
||||
let unavailable_btn = button(text("Unavailable").size(13))
|
||||
.on_press(Message::UrlPressed(PathBuf::from(
|
||||
"https://github.com/Universal-Debloater-Alliance/universal-android-debloater/wiki/FAQ#\
|
||||
why-is-the-disable-mode-setting-not-available-for-my-device",
|
||||
)))
|
||||
.height(22)
|
||||
.style(style::Button::Unavailable);
|
||||
|
||||
// Disabling package without root isn't really possible before Android Oreo (8.0)
|
||||
// see https://github.com/Universal-Debloater-Alliance/universal-android-debloater/wiki/ADB-reference
|
||||
let disable_mode_checkbox = checkbox(
|
||||
"Clear and disable packages instead of uninstalling them",
|
||||
self.device.disable_mode,
|
||||
Message::DisableMode,
|
||||
)
|
||||
.style(disable_checkbox_style);
|
||||
|
||||
let disable_setting_row = if phone.android_sdk >= 23 {
|
||||
row![
|
||||
disable_mode_checkbox,
|
||||
Space::new(Length::Fill, Length::Shrink),
|
||||
]
|
||||
.width(Length::Fill)
|
||||
} else {
|
||||
row![
|
||||
disable_mode_checkbox,
|
||||
Space::new(Length::Fill, Length::Shrink),
|
||||
unavailable_btn,
|
||||
]
|
||||
.width(Length::Fill)
|
||||
};
|
||||
|
||||
let device_specific_ctn = container(
|
||||
column![
|
||||
multi_user_mode_checkbox,
|
||||
multi_user_mode_descr,
|
||||
disable_setting_row,
|
||||
disable_mode_descr,
|
||||
]
|
||||
.spacing(10),
|
||||
)
|
||||
.padding(10)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Shrink)
|
||||
.style(style::Container::Frame);
|
||||
|
||||
let backup_pick_list = pick_list(
|
||||
self.device.backup.backups.clone(),
|
||||
self.device.backup.selected.clone(),
|
||||
Message::BackupSelected,
|
||||
)
|
||||
.padding(6);
|
||||
|
||||
let backup_btn = button(text("Backup").horizontal_alignment(alignment::Horizontal::Center))
|
||||
.padding(5)
|
||||
.on_press(Message::BackupDevice)
|
||||
.style(style::Button::Primary)
|
||||
.width(77);
|
||||
|
||||
let restore_btn = |enabled| {
|
||||
if enabled {
|
||||
button(text("Restore").horizontal_alignment(alignment::Horizontal::Center))
|
||||
.padding(5)
|
||||
.on_press(Message::RestoreDevice)
|
||||
.width(77)
|
||||
} else {
|
||||
button(text("No backup").horizontal_alignment(alignment::Horizontal::Center))
|
||||
.padding(5)
|
||||
.width(77)
|
||||
}
|
||||
};
|
||||
|
||||
let locate_backup_btn = if self.device.backup.backups.is_empty() {
|
||||
button("Open backup directory")
|
||||
.padding(5)
|
||||
.style(style::Button::Primary)
|
||||
} else {
|
||||
button("Open backup directory")
|
||||
.on_press(Message::UrlPressed(BACKUP_DIR.join(phone.adb_id.clone())))
|
||||
.padding(5)
|
||||
.style(style::Button::Primary)
|
||||
};
|
||||
|
||||
let backup_row = row![
|
||||
backup_btn,
|
||||
"Backup the current state of the phone",
|
||||
Space::new(Length::Fill, Length::Shrink),
|
||||
locate_backup_btn,
|
||||
]
|
||||
.spacing(10)
|
||||
.align_items(Alignment::Center);
|
||||
|
||||
let restore_row = if self.device.backup.backups.is_empty() {
|
||||
row![restore_btn(false), "Restore the state of the device",]
|
||||
.spacing(10)
|
||||
.align_items(Alignment::Center)
|
||||
} else {
|
||||
row![
|
||||
restore_btn(true),
|
||||
"Restore the state of the device",
|
||||
Space::new(Length::Fill, Length::Shrink),
|
||||
text(self.device.backup.backup_state.clone()).style(style::Text::Danger),
|
||||
backup_pick_list,
|
||||
]
|
||||
.spacing(10)
|
||||
.align_items(Alignment::Center)
|
||||
};
|
||||
|
||||
let backup_restore_ctn = container(column![backup_row, restore_row].spacing(10))
|
||||
.padding(10)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Shrink)
|
||||
.style(style::Container::Frame);
|
||||
|
||||
let no_device_ctn = || {
|
||||
container(text("No device detected").style(style::Text::Danger))
|
||||
.padding(10)
|
||||
.width(Length::Fill)
|
||||
.style(style::Container::BorderedFrame)
|
||||
};
|
||||
|
||||
let content = if phone.adb_id.clone().is_empty() {
|
||||
column![
|
||||
text("Theme").size(25),
|
||||
theme_ctn,
|
||||
text("General").size(25),
|
||||
general_ctn,
|
||||
text("Current device").size(25),
|
||||
no_device_ctn(),
|
||||
text("Backup / Restore").size(25),
|
||||
no_device_ctn(),
|
||||
]
|
||||
.width(Length::Fill)
|
||||
.spacing(20)
|
||||
} else {
|
||||
column![
|
||||
text("Theme").size(25),
|
||||
theme_ctn,
|
||||
text("General").size(25),
|
||||
general_ctn,
|
||||
text("Current device").size(25),
|
||||
warning_ctn,
|
||||
device_specific_ctn,
|
||||
backup_restore_ctn,
|
||||
]
|
||||
.width(Length::Fill)
|
||||
.spacing(20)
|
||||
};
|
||||
|
||||
container(content)
|
||||
.padding(10)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,113 +0,0 @@
|
|||
pub use crate::core::sync::Phone;
|
||||
use crate::core::theme::Theme;
|
||||
use crate::core::update::{SelfUpdateState, SelfUpdateStatus};
|
||||
pub use crate::gui::views::about::Message as AboutMessage;
|
||||
pub use crate::gui::views::list::{List as AppsView, LoadingState as ListLoadingState};
|
||||
use crate::gui::{style, Message};
|
||||
use iced::widget::{button, container, pick_list, row, text, Space, Text};
|
||||
use iced::{alignment, Alignment, Element, Font, Length, Renderer};
|
||||
|
||||
pub const ICONS: Font = Font::External {
|
||||
name: "Icons",
|
||||
bytes: include_bytes!("../../../resources/assets/icons.ttf"),
|
||||
};
|
||||
|
||||
pub fn nav_menu<'a>(
|
||||
device_list: &'a Vec<Phone>,
|
||||
selected_device: Option<Phone>,
|
||||
apps_view: &AppsView,
|
||||
self_update_state: &SelfUpdateState,
|
||||
) -> Element<'a, Message, Renderer<Theme>> {
|
||||
let apps_refresh_btn = button(
|
||||
Text::new("\u{E900}")
|
||||
.font(ICONS)
|
||||
.width(17)
|
||||
.horizontal_alignment(alignment::Horizontal::Center)
|
||||
.size(17),
|
||||
)
|
||||
.on_press(Message::RefreshButtonPressed)
|
||||
.padding(5)
|
||||
.style(style::Button::Refresh);
|
||||
|
||||
let reboot_btn = button("Reboot")
|
||||
.on_press(Message::RebootButtonPressed)
|
||||
.padding(5)
|
||||
.style(style::Button::Refresh);
|
||||
|
||||
#[allow(clippy::option_if_let_else)]
|
||||
let uad_version_text = if let Some(r) = &self_update_state.latest_release {
|
||||
if self_update_state.status == SelfUpdateStatus::Updating {
|
||||
Text::new("Updating please wait...")
|
||||
} else {
|
||||
Text::new(format!(
|
||||
"New UAD version available {} -> {}",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
r.tag_name
|
||||
))
|
||||
}
|
||||
} else {
|
||||
Text::new(env!("CARGO_PKG_VERSION"))
|
||||
};
|
||||
|
||||
let apps_btn = if self_update_state.latest_release.is_some() {
|
||||
button("Update")
|
||||
.on_press(Message::AboutAction(AboutMessage::DoSelfUpdate))
|
||||
.padding(5)
|
||||
.style(style::Button::SelfUpdate)
|
||||
} else {
|
||||
button("Apps")
|
||||
.on_press(Message::AppsPress)
|
||||
.padding(5)
|
||||
.style(style::Button::Primary)
|
||||
};
|
||||
|
||||
let about_btn = button("About")
|
||||
.on_press(Message::AboutPressed)
|
||||
.padding(5)
|
||||
.style(style::Button::Primary);
|
||||
|
||||
let settings_btn = button("Settings")
|
||||
.on_press(Message::SettingsPressed)
|
||||
.padding(5)
|
||||
.style(style::Button::Primary);
|
||||
|
||||
let device_list_text = match apps_view.loading_state {
|
||||
ListLoadingState::FindingPhones(_) => text("finding connected phone..."),
|
||||
_ => text("no devices/emulators found"),
|
||||
};
|
||||
|
||||
let row = match selected_device {
|
||||
Some(phone) => row![
|
||||
apps_refresh_btn,
|
||||
reboot_btn,
|
||||
pick_list(device_list, Some(phone), Message::DeviceSelected,),
|
||||
Space::new(Length::Fill, Length::Shrink),
|
||||
uad_version_text,
|
||||
apps_btn,
|
||||
about_btn,
|
||||
settings_btn,
|
||||
]
|
||||
.width(Length::Fill)
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(10),
|
||||
None => row![
|
||||
reboot_btn,
|
||||
apps_refresh_btn,
|
||||
device_list_text,
|
||||
Space::new(Length::Fill, Length::Shrink),
|
||||
uad_version_text,
|
||||
apps_btn,
|
||||
about_btn,
|
||||
settings_btn,
|
||||
]
|
||||
.width(Length::Fill)
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(10),
|
||||
};
|
||||
|
||||
container(row)
|
||||
.width(Length::Fill)
|
||||
.padding(10)
|
||||
.style(style::Container::Frame)
|
||||
.into()
|
||||
}
|
||||
75
src/main.rs
75
src/main.rs
|
|
@ -1,75 +0,0 @@
|
|||
#![windows_subsystem = "windows"]
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
use crate::core::utils::setup_uad_dir;
|
||||
use fern::{
|
||||
colors::{Color, ColoredLevelConfig},
|
||||
FormatCallback,
|
||||
};
|
||||
use log::Record;
|
||||
use static_init::dynamic;
|
||||
use std::path::PathBuf;
|
||||
use std::{fmt::Arguments, fs::OpenOptions};
|
||||
|
||||
mod core;
|
||||
mod gui;
|
||||
|
||||
#[dynamic]
|
||||
static CONFIG_DIR: PathBuf = setup_uad_dir(dirs::config_dir());
|
||||
|
||||
#[dynamic]
|
||||
static CACHE_DIR: PathBuf = setup_uad_dir(dirs::cache_dir());
|
||||
|
||||
fn main() -> iced::Result {
|
||||
setup_logger().expect("setup logging");
|
||||
gui::UadGui::start()
|
||||
}
|
||||
|
||||
pub fn setup_logger() -> Result<(), fern::InitError> {
|
||||
let colors = ColoredLevelConfig::new().info(Color::Green);
|
||||
|
||||
let make_formatter = |use_colors: bool| {
|
||||
move |out: FormatCallback, message: &Arguments, record: &Record| {
|
||||
out.finish(format_args!(
|
||||
"{} {} [{}:{}] {}",
|
||||
chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
|
||||
if use_colors {
|
||||
format!("{:5}", colors.color(record.level()))
|
||||
} else {
|
||||
format!("{:5}", record.level().to_string())
|
||||
},
|
||||
record.file().unwrap_or("?"),
|
||||
record.line().map(|l| l.to_string()).unwrap_or_default(),
|
||||
message
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let default_log_level = log::LevelFilter::Warn;
|
||||
let log_file = OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.append(true)
|
||||
.truncate(false)
|
||||
.open(CACHE_DIR.join(format!("UAD_{}.log", chrono::Local::now().format("%Y%m%d"))))?;
|
||||
|
||||
let file_dispatcher = fern::Dispatch::new()
|
||||
.format(make_formatter(false))
|
||||
.level(default_log_level)
|
||||
.level_for("uad_gui", log::LevelFilter::Debug)
|
||||
.chain(log_file);
|
||||
|
||||
let stdout_dispatcher = fern::Dispatch::new()
|
||||
.format(make_formatter(true))
|
||||
.level(default_log_level)
|
||||
.level_for("uad_gui", log::LevelFilter::Warn)
|
||||
.chain(std::io::stdout());
|
||||
|
||||
fern::Dispatch::new()
|
||||
.chain(stdout_dispatcher)
|
||||
.chain(file_dispatcher)
|
||||
.apply()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue