mirror of
https://github.com/ReVanced/revanced-manager.git
synced 2026-01-16 15:53:59 +00:00
Compare commits
1013 Commits
settings-r
...
compose-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d9c2439dd | ||
|
|
c1e90a45cd | ||
|
|
775c2d5cde | ||
|
|
de27b4b109 | ||
|
|
b58a11f9f1 | ||
|
|
2297e94cb8 | ||
|
|
a84ab244c7 | ||
|
|
2cdd6d1843 | ||
|
|
3b82767a89 | ||
|
|
af8e2b44c0 | ||
|
|
a8820a4daf | ||
|
|
e1278452b9 | ||
|
|
d4d2056585 | ||
|
|
55524f7284 | ||
|
|
cd2dbcc841 | ||
|
|
9fdca5a0af | ||
|
|
05444d8824 | ||
|
|
59d233e15c | ||
|
|
1fb94b711f | ||
|
|
0096169af8 | ||
|
|
7c410fef45 | ||
|
|
a91ff60533 | ||
|
|
0ce49582f1 | ||
|
|
f32ffbb6f2 | ||
|
|
3851983a44 | ||
|
|
e97b19d2b6 | ||
|
|
24c4cd3f99 | ||
|
|
f2ea00757a | ||
|
|
51132731a4 | ||
|
|
bf311664d5 | ||
|
|
581f7922ed | ||
|
|
83d33e87e3 | ||
|
|
537e0b5bed | ||
|
|
663cf2d6b8 | ||
|
|
0030c7a788 | ||
|
|
eba92e2644 | ||
|
|
2671e68004 | ||
|
|
6e08e9dc39 | ||
|
|
3710675ac0 | ||
|
|
244a8f4e17 | ||
|
|
015a2df457 | ||
|
|
8686c058ce | ||
|
|
5c21ec6eb5 | ||
|
|
f5e1e0b065 | ||
|
|
bb5d414abb | ||
|
|
bd07435d4b | ||
|
|
54c56f1d81 | ||
|
|
5133f02ad6 | ||
|
|
9f7eaa2123 | ||
|
|
9a82b78528 | ||
|
|
cedc6ad49f | ||
|
|
4d95a29a1c | ||
|
|
6f6296b8cd | ||
|
|
f3f8bc4ec2 | ||
|
|
55e7ebf4fc | ||
|
|
26778f57e6 | ||
|
|
7007010f14 | ||
|
|
8b3c4eb91c | ||
|
|
a53a8ba627 | ||
|
|
f07f75a55f | ||
|
|
2697077fc8 | ||
|
|
dc51d6134d | ||
|
|
a9a21fd15a | ||
|
|
cd142a70d3 | ||
|
|
ac0625892d | ||
|
|
7f5a291c99 | ||
|
|
93f4a5bb7c | ||
|
|
2de16e18e8 | ||
|
|
c73fdfdd2d | ||
|
|
bdfb5a9462 | ||
|
|
ca918eea46 | ||
|
|
8dc4e5b89e | ||
|
|
1a54313c1d | ||
|
|
a3604579fe | ||
|
|
3a63e42df9 | ||
|
|
66432764cf | ||
|
|
7ef496b98c | ||
|
|
9863c5161a | ||
|
|
7438f45903 | ||
|
|
5662863a64 | ||
|
|
426b28932f | ||
|
|
83eeeae801 | ||
|
|
4de534094a | ||
|
|
ca38737783 | ||
|
|
7e858a244c | ||
|
|
dbe5c44ec3 | ||
|
|
a5c8a23f9f | ||
|
|
f92cb9e191 | ||
|
|
e55566d3df | ||
|
|
76e0c95187 | ||
|
|
fedaedfda1 | ||
|
|
3bd4f0d8f3 | ||
|
|
43b3743213 | ||
|
|
089f200fe6 | ||
|
|
a3f31ea657 | ||
|
|
a907528a20 | ||
|
|
fa86c1a0bb | ||
|
|
ea29d0f00c | ||
|
|
b93ecc0db2 | ||
|
|
55be7a6a9c | ||
|
|
95efff8b66 | ||
|
|
44f8b1fb6b | ||
|
|
e4f19b0c25 | ||
|
|
3c23d573bf | ||
|
|
61f1ee0627 | ||
|
|
df52a7bdef | ||
|
|
113a74d270 | ||
|
|
15b47f9bb6 | ||
|
|
e2623d6d79 | ||
|
|
79a513f94b | ||
|
|
cc771817cb | ||
|
|
deea682651 | ||
|
|
7dae562819 | ||
|
|
8d939a6669 | ||
|
|
8419f75d59 | ||
|
|
d2119d3643 | ||
|
|
9a01273c43 | ||
|
|
c22e5b4051 | ||
|
|
f6ca4e9555 | ||
|
|
bf049c3c1a | ||
|
|
6ace71b739 | ||
|
|
d889677b29 | ||
|
|
5f2376919b | ||
|
|
5c434137d3 | ||
|
|
7bf89887e4 | ||
|
|
5901372523 | ||
|
|
537b7084e0 | ||
|
|
45d64117bf | ||
|
|
6c3a99a492 | ||
|
|
ea007adecd | ||
|
|
72cd8ebca8 | ||
|
|
7ab191ed2b | ||
|
|
3a05150fa3 | ||
|
|
e5b414e277 | ||
|
|
d3d4c27f6d | ||
|
|
36a1c3f368 | ||
|
|
2a63a6163a | ||
|
|
02ea5c6d4a | ||
|
|
9562d80bfd | ||
|
|
49f851022d | ||
|
|
b18c678354 | ||
|
|
189c993ada | ||
|
|
69c119d545 | ||
|
|
feb0ca4cf3 | ||
|
|
0383bd74f7 | ||
|
|
60fdec9804 | ||
|
|
483be5d722 | ||
|
|
e44d3fdee4 | ||
|
|
b59a16191a | ||
|
|
f4d6c60b9e | ||
|
|
154f036fe9 | ||
|
|
d3417adbeb | ||
|
|
16c4290f05 | ||
|
|
d23d673c47 | ||
|
|
b80f94b77b | ||
|
|
5f4c9584a9 | ||
|
|
9a085f4091 | ||
|
|
8d7886f7eb | ||
|
|
c21076f2fb | ||
|
|
05fe058151 | ||
|
|
752310fe94 | ||
|
|
75500bbc43 | ||
|
|
84013ffca2 | ||
|
|
9da798dac1 | ||
|
|
d622208b17 | ||
|
|
c5cb18a7ea | ||
|
|
0d73e0cd32 | ||
|
|
9d961f6a52 | ||
|
|
7df3350acb | ||
|
|
858dd99ffa | ||
|
|
757e132321 | ||
|
|
413fe980a8 | ||
|
|
1aba6bc874 | ||
|
|
b5c1f6d732 | ||
|
|
ea50e65ab1 | ||
|
|
e1647fdef0 | ||
|
|
0b362e3393 | ||
|
|
159461356e | ||
|
|
b1206cb663 | ||
|
|
b5558ea3ff | ||
|
|
dcaa38c882 | ||
|
|
429b428f67 | ||
|
|
a12cae7299 | ||
|
|
88e860cf01 | ||
|
|
0616666d5e | ||
|
|
2700d45e4d | ||
|
|
c3a32a1142 | ||
|
|
afb0f80de5 | ||
|
|
4aaa88e353 | ||
|
|
359095a6f8 | ||
|
|
e1c3d61ec6 | ||
|
|
72b7d24b33 | ||
|
|
0ccc2c13ac | ||
|
|
fa414ce6ee | ||
|
|
3f362b605f | ||
|
|
a2f9e2f1da | ||
|
|
3af26e7065 | ||
|
|
d5bdc293f3 | ||
|
|
336eed3a95 | ||
|
|
59b4c0b2d2 | ||
|
|
0934b24591 | ||
|
|
bb2164e1a9 | ||
|
|
2a3590ddd2 | ||
|
|
ceb7623794 | ||
|
|
f183b6d8a6 | ||
|
|
a6174eee8f | ||
|
|
f8aafa0503 | ||
|
|
5e35893883 | ||
|
|
90db765c9a | ||
|
|
e61c98ca59 | ||
|
|
c003c3c324 | ||
|
|
5a3a619d16 | ||
|
|
89addf3f78 | ||
|
|
34cf848baa | ||
|
|
a9171e17bd | ||
|
|
2e64022229 | ||
|
|
8aa70d350e | ||
|
|
ca93524be0 | ||
|
|
7fb1e27617 | ||
|
|
1dd6738964 | ||
|
|
4924eaef80 | ||
|
|
56896d6197 | ||
|
|
ee0f342456 | ||
|
|
90edf1ddd0 | ||
|
|
b69a369d4e | ||
|
|
663d21c6af | ||
|
|
dadc5462e3 | ||
|
|
3aea6cbaec | ||
|
|
8425b6fa81 | ||
|
|
c62f772284 | ||
|
|
ad775f3059 | ||
|
|
c199801fb7 | ||
|
|
2ffcaec724 | ||
|
|
08eb995220 | ||
|
|
bf1d628944 | ||
|
|
f6fb534e04 | ||
|
|
686eb40cb0 | ||
|
|
b03f7b18a0 | ||
|
|
e833bf4ad1 | ||
|
|
805d440450 | ||
|
|
e997255cf3 | ||
|
|
cfcabf6ef1 | ||
|
|
7af6c79076 | ||
|
|
a17c2de228 | ||
|
|
3bb071d80d | ||
|
|
9950d85779 | ||
|
|
e7ec239783 | ||
|
|
3246269937 | ||
|
|
85692f8596 | ||
|
|
c879faf2eb | ||
|
|
933a4a3220 | ||
|
|
a269a39aa4 | ||
|
|
cff9c5ed09 | ||
|
|
a811df9547 | ||
|
|
83cdaaee18 | ||
|
|
5b3e9e595c | ||
|
|
97aa8c06c8 | ||
|
|
933e69e21e | ||
|
|
dc73462ac4 | ||
|
|
ad903533f8 | ||
|
|
62e934c403 | ||
|
|
6561e4c97c | ||
|
|
ac98ef834a | ||
|
|
a246863a89 | ||
|
|
39b5b87c40 | ||
|
|
35595fb4bc | ||
|
|
02fb47d3b5 | ||
|
|
eef50888bb | ||
|
|
9b3974414c | ||
|
|
88deadcf08 | ||
|
|
bad847d89b | ||
|
|
3f446f8236 | ||
|
|
31cbab209c | ||
|
|
b14285b2c8 | ||
|
|
8e28f06e26 | ||
|
|
5b46c58af9 | ||
|
|
dccf86163a | ||
|
|
8767f0e99c | ||
|
|
0a1acd24e3 | ||
|
|
a55160e7c6 | ||
|
|
6436483206 | ||
|
|
409c888d52 | ||
|
|
858b0ec5b4 | ||
|
|
8c9fe6989f | ||
|
|
17d1874381 | ||
|
|
e7802ed3d7 | ||
|
|
f547bb7ab1 | ||
|
|
bfcd1fb977 | ||
|
|
1ab1e4682f | ||
|
|
a854221969 | ||
|
|
ab42b946ef | ||
|
|
8ae4e850da | ||
|
|
4d04ae088c | ||
|
|
5c17a78e46 | ||
|
|
3492dc4e83 | ||
|
|
39b08e5201 | ||
|
|
18527999b5 | ||
|
|
fd520bba70 | ||
|
|
59b894dce4 | ||
|
|
6f6476e851 | ||
|
|
37c1754098 | ||
|
|
1f1a480d51 | ||
|
|
6cf51d71c5 | ||
|
|
06ef9dee44 | ||
|
|
49b4bbbf0b | ||
|
|
fc90bbc27c | ||
|
|
86e42449eb | ||
|
|
45142841d5 | ||
|
|
3d59ee51ac | ||
|
|
6dbcd6293e | ||
|
|
a008cf5dd1 | ||
|
|
23162f6233 | ||
|
|
8540d30196 | ||
|
|
f06da2ba56 | ||
|
|
9e1ebb3902 | ||
|
|
2aec6e1e55 | ||
|
|
6134ed78b4 | ||
|
|
e3cb056858 | ||
|
|
eb2270673d | ||
|
|
db070b125b | ||
|
|
a510d59e64 | ||
|
|
0264308b6d | ||
|
|
2ac3d5c483 | ||
|
|
3f54381d30 | ||
|
|
059a72b9dd | ||
|
|
64496bfbe7 | ||
|
|
b07fd2321d | ||
|
|
1efccda3f5 | ||
|
|
b9231b4de0 | ||
|
|
1a164ebe30 | ||
|
|
8d53180d86 | ||
|
|
62bccd1504 | ||
|
|
969ddb7bef | ||
|
|
8b6d32dd7b | ||
|
|
fd0ec6c6a7 | ||
|
|
de1ef23824 | ||
|
|
f30333e753 | ||
|
|
d84e6a3ffc | ||
|
|
5ec97f4a85 | ||
|
|
8c40119609 | ||
|
|
5b6ae800fd | ||
|
|
064a54eaf0 | ||
|
|
41268ca80b | ||
|
|
f0d9def3dd | ||
|
|
44e5dad6e9 | ||
|
|
83eb88170a | ||
|
|
e7599e1386 | ||
|
|
14888f9da7 | ||
|
|
9675a2777b | ||
|
|
d71a4bf3c3 | ||
|
|
57548641e7 | ||
|
|
553af83139 | ||
|
|
1d6b34a39f | ||
|
|
948a6d1440 | ||
|
|
0dd036574f | ||
|
|
4d201f17f2 | ||
|
|
da32ff954a | ||
|
|
70e49aaaa3 | ||
|
|
4e43938f7f | ||
|
|
f620a887ad | ||
|
|
8fa4d1d26d | ||
|
|
925be5a168 | ||
|
|
caeabfc91b | ||
|
|
5c2f9d91a6 | ||
|
|
55e871aa7d | ||
|
|
b7d53cfca8 | ||
|
|
85a03a6472 | ||
|
|
373cc4bbb1 | ||
|
|
a127b959ea | ||
|
|
476a6e5771 | ||
|
|
037a11aeb8 | ||
|
|
45a54d1608 | ||
|
|
ca85b38bdc | ||
|
|
60a5a11c71 | ||
|
|
7fc6ec5c2c | ||
|
|
7fa7b9d53a | ||
|
|
045a5483f1 | ||
|
|
40dd81eba3 | ||
|
|
9f99fcf808 | ||
|
|
1b11a0ad7a | ||
|
|
8f60bf44e5 | ||
|
|
3b459160d7 | ||
|
|
c97e4adace | ||
|
|
875ce6439d | ||
|
|
c45d579406 | ||
|
|
b6b8f4e069 | ||
|
|
21ceadaf7f | ||
|
|
83fc7f131a | ||
|
|
8e4a9088ea | ||
|
|
7ee2b1a026 | ||
|
|
40c99ab4dc | ||
|
|
1752fae9d9 | ||
|
|
d264a2a363 | ||
|
|
a5e909cfc8 | ||
|
|
bdb0317a9e | ||
|
|
b0464408f1 | ||
|
|
767fa77436 | ||
|
|
89b48cebcf | ||
|
|
dedcb3c51a | ||
|
|
3074766ff2 | ||
|
|
722f2b645f | ||
|
|
f79aa9edd7 | ||
|
|
5127c7f599 | ||
|
|
23dcbbecb2 | ||
|
|
1c84265dc4 | ||
|
|
7ccc689bd3 | ||
|
|
007dce800e | ||
|
|
ba758f3d8b | ||
|
|
a1f9a2786f | ||
|
|
b11a720621 | ||
|
|
aedf475310 | ||
|
|
10f35eb727 | ||
|
|
5c7d52c8e9 | ||
|
|
841d61278b | ||
|
|
534e4c2453 | ||
|
|
062f28387f | ||
|
|
a8a4ffabed | ||
|
|
cde3f8d62c | ||
|
|
8182228a46 | ||
|
|
7fa5daf623 | ||
|
|
221e663e47 | ||
|
|
915ec0e260 | ||
|
|
783f313ed8 | ||
|
|
05575cccfb | ||
|
|
a8e192b85f | ||
|
|
4b591effed | ||
|
|
123c51794b | ||
|
|
53d64a0636 | ||
|
|
64cbb68344 | ||
|
|
00950c79f0 | ||
|
|
746229120c | ||
|
|
133f6ee28f | ||
|
|
08a9d2a64f | ||
|
|
4b7b05ac0f | ||
|
|
5b6426c453 | ||
|
|
235489dcdf | ||
|
|
b4e5c66f9c | ||
|
|
5c58f624de | ||
|
|
89ad88c0ef | ||
|
|
d3036105bb | ||
|
|
3e8862ea5a | ||
|
|
0c59bbb7be | ||
|
|
f4b279d1fd | ||
|
|
c652d196fc | ||
|
|
632b9502b2 | ||
|
|
dfb3fe3749 | ||
|
|
b3ba0fcd6a | ||
|
|
8a98b8448b | ||
|
|
da604e89ac | ||
|
|
624b43733e | ||
|
|
4ba7ec1854 | ||
|
|
1d6b074856 | ||
|
|
695329088b | ||
|
|
f2b20e6e7f | ||
|
|
a5c3aa3744 | ||
|
|
900058a6e5 | ||
|
|
4c51ad3650 | ||
|
|
a0c2a17bab | ||
|
|
851b06b0d2 | ||
|
|
57018a65df | ||
|
|
b44419133a | ||
|
|
8961cf9044 | ||
|
|
5139873f79 | ||
|
|
5caa79eb0d | ||
|
|
9552b2ebc5 | ||
|
|
7e3afe0cb2 | ||
|
|
7b7d91d661 | ||
|
|
44b8d4ceee | ||
|
|
aaa97ebb71 | ||
|
|
d99e5af384 | ||
|
|
c47c7c0a88 | ||
|
|
3e32c0fd90 | ||
|
|
a45d9598cc | ||
|
|
8c8df698d4 | ||
|
|
8d0d782ab5 | ||
|
|
4db4789a06 | ||
|
|
abdd9dc430 | ||
|
|
5193042e6b | ||
|
|
daba737ecb | ||
|
|
bd96701103 | ||
|
|
69c20b74cf | ||
|
|
7297436ab4 | ||
|
|
a329626715 | ||
|
|
50a20d0535 | ||
|
|
c52edc99c0 | ||
|
|
1c6c5d53ae | ||
|
|
1b110e5bd5 | ||
|
|
8872165a99 | ||
|
|
bd79496433 | ||
|
|
820cd720b5 | ||
|
|
31ff1e0492 | ||
|
|
8b429f03c2 | ||
|
|
a68a83940d | ||
|
|
1f95767aeb | ||
|
|
c7298424e5 | ||
|
|
d53f8cf130 | ||
|
|
a7e2281805 | ||
|
|
0ce23d2d60 | ||
|
|
d0fe57970f | ||
|
|
4dcdc57ffd | ||
|
|
1ef1f8d47a | ||
|
|
d688f38a63 | ||
|
|
582db18d83 | ||
|
|
f5a12e01bd | ||
|
|
09fd9c4e04 | ||
|
|
43d5888182 | ||
|
|
646feae0ec | ||
|
|
e73ce99f1d | ||
|
|
af2d8226de | ||
|
|
37c912b598 | ||
|
|
3102272a31 | ||
|
|
3dc695eafb | ||
|
|
6f0721b066 | ||
|
|
3dfe6b1080 | ||
|
|
02e5641227 | ||
|
|
01e4a76caa | ||
|
|
59838a51ca | ||
|
|
224be29a3d | ||
|
|
921052acce | ||
|
|
81f05e1b19 | ||
|
|
b769a66d16 | ||
|
|
5d612fe790 | ||
|
|
61251deffa | ||
|
|
f2931443d9 | ||
|
|
3d8318da14 | ||
|
|
f6bfe0d3f9 | ||
|
|
51c0f14055 | ||
|
|
2a6dc09a9b | ||
|
|
f1c2f4146c | ||
|
|
2f46b3c84e | ||
|
|
5f388abf95 | ||
|
|
7acdb8e660 | ||
|
|
e063b3d102 | ||
|
|
cb722f2634 | ||
|
|
0499d2b08a | ||
|
|
427928e542 | ||
|
|
ceb9d66f17 | ||
|
|
295c5a74ea | ||
|
|
400df69528 | ||
|
|
5bc7b135d5 | ||
|
|
6dce353d78 | ||
|
|
e707e51719 | ||
|
|
1cea9600a2 | ||
|
|
d81808ad7b | ||
|
|
ea9654edec | ||
|
|
ced37f7c76 | ||
|
|
4c6e214bf6 | ||
|
|
d10abd4829 | ||
|
|
25f1640fd6 | ||
|
|
5690adc0de | ||
|
|
f1b25d09da | ||
|
|
96a21a5564 | ||
|
|
0b21bb9807 | ||
|
|
557ee8d472 | ||
|
|
6da3751a8a | ||
|
|
cf708de005 | ||
|
|
b649c4539f | ||
|
|
c63342bc21 | ||
|
|
9f56b277ca | ||
|
|
73c92b9c72 | ||
|
|
c7627ced8e | ||
|
|
63c6412736 | ||
|
|
a7663524e6 | ||
|
|
65feb34242 | ||
|
|
23690a98df | ||
|
|
7449d4e318 | ||
|
|
c6f9e36f4b | ||
|
|
e9cee0abe2 | ||
|
|
9440f23b55 | ||
|
|
c67b4b438c | ||
|
|
1bdb820aed | ||
|
|
a28d77bc65 | ||
|
|
d60f9aa1d8 | ||
|
|
3209c0e430 | ||
|
|
7ef8f0454b | ||
|
|
232b702789 | ||
|
|
694f2a9fae | ||
|
|
77204087bb | ||
|
|
b26760b216 | ||
|
|
3c36950aeb | ||
|
|
bbeb836923 | ||
|
|
a99406f0a9 | ||
|
|
73368b58be | ||
|
|
ca14e77ba3 | ||
|
|
cafdfcda47 | ||
|
|
5c68d513a3 | ||
|
|
fc52560244 | ||
|
|
46f6a49a7a | ||
|
|
c13827e8e1 | ||
|
|
e0a6de2c2b | ||
|
|
afdba00722 | ||
|
|
9084c71aa3 | ||
|
|
8fc5fb6a80 | ||
|
|
5f762c5442 | ||
|
|
8b21ec1ea3 | ||
|
|
e83fbb864e | ||
|
|
f03af17f71 | ||
|
|
2c3e2e639f | ||
|
|
cc85b393dc | ||
|
|
fa6ad214f9 | ||
|
|
3ceb63be1d | ||
|
|
200483d733 | ||
|
|
6b03f3a169 | ||
|
|
612a3bab49 | ||
|
|
3ac08512f3 | ||
|
|
1adc673c44 | ||
|
|
1aa1bd84cf | ||
|
|
4a72267d41 | ||
|
|
7e0f18e3b7 | ||
|
|
aab5d4411b | ||
|
|
e5d83f424a | ||
|
|
bc300d81d9 | ||
|
|
5e6cc86c7e | ||
|
|
d91ea62903 | ||
|
|
f00dc4d3e6 | ||
|
|
6c0c961ca0 | ||
|
|
a99de99202 | ||
|
|
a31ec9d1fe | ||
|
|
a23f032fd2 | ||
|
|
95a8234e2d | ||
|
|
a7e481c827 | ||
|
|
975870e254 | ||
|
|
b7c838fd25 | ||
|
|
3776674eb4 | ||
|
|
4293e27681 | ||
|
|
8564c1a72e | ||
|
|
f9e6ef3fd3 | ||
|
|
e80ee09893 | ||
|
|
025ff527ee | ||
|
|
35fdbb5988 | ||
|
|
401646ace4 | ||
|
|
a62a8852e7 | ||
|
|
04a3df3642 | ||
|
|
ecb041187b | ||
|
|
1fd44e1cfc | ||
|
|
fb29036d0a | ||
|
|
aba2ed4378 | ||
|
|
076e17d670 | ||
|
|
a971cbd519 | ||
|
|
c265794d0e | ||
|
|
ecadb80113 | ||
|
|
9a66357f7a | ||
|
|
63d83a43ad | ||
|
|
7833a0d552 | ||
|
|
966796dfec | ||
|
|
2d19f36901 | ||
|
|
6f70a07970 | ||
|
|
e85ed5a8e3 | ||
|
|
ad416387c2 | ||
|
|
58d8e7f34f | ||
|
|
bb105b5662 | ||
|
|
a71a930125 | ||
|
|
af7e39b7f0 | ||
|
|
c981cb4a41 | ||
|
|
3ea6ef0bbf | ||
|
|
2772a96727 | ||
|
|
50b4a5f1d8 | ||
|
|
e52a6ce734 | ||
|
|
add49e14fb | ||
|
|
ab13895196 | ||
|
|
846afb420b | ||
|
|
dca8a1dab6 | ||
|
|
d1c36c1bcc | ||
|
|
c209c32613 | ||
|
|
9e5e89ac95 | ||
|
|
7cc6b88e4e | ||
|
|
5cd1cba668 | ||
|
|
ca365bac6e | ||
|
|
f1fc48ce5a | ||
|
|
316e440d0d | ||
|
|
95018814a7 | ||
|
|
b52e49d90a | ||
|
|
4acd738353 | ||
|
|
982249f974 | ||
|
|
1f5461fbe5 | ||
|
|
adb7e5663a | ||
|
|
ffc14f2146 | ||
|
|
0c57322051 | ||
|
|
a8b7debf8d | ||
|
|
7ed9787b58 | ||
|
|
eef701615b | ||
|
|
97d8519b8b | ||
|
|
00210f7f0e | ||
|
|
cea4c6c27a | ||
|
|
bc83a39b0f | ||
|
|
aa0575a637 | ||
|
|
4ca7b8a7c1 | ||
|
|
6ec6546cc5 | ||
|
|
c5e04cc824 | ||
|
|
4fc913eae1 | ||
|
|
2d7026ac7a | ||
|
|
1200360588 | ||
|
|
02722fc0be | ||
|
|
2e1de94623 | ||
|
|
10bae69db6 | ||
|
|
6dee3aa1b7 | ||
|
|
e76418d48d | ||
|
|
52e1020a90 | ||
|
|
c170392123 | ||
|
|
d414a91f40 | ||
|
|
6d6fae1ecd | ||
|
|
6d866d4424 | ||
|
|
f4dea6e58c | ||
|
|
c72d10ac85 | ||
|
|
3e696d6847 | ||
|
|
28d6ab692e | ||
|
|
f7747809f2 | ||
|
|
d4b15aee4d | ||
|
|
d0689555f1 | ||
|
|
82d6e3f105 | ||
|
|
244d078b11 | ||
|
|
f89c742c90 | ||
|
|
ce5088ab53 | ||
|
|
0443c8c200 | ||
|
|
8a4161753e | ||
|
|
f2e7071f6d | ||
|
|
25d53ce9a8 | ||
|
|
3b58d229da | ||
|
|
f5ba84d81e | ||
|
|
933c71923e | ||
|
|
bfe59ea57a | ||
|
|
953209ca13 | ||
|
|
b2119ce60e | ||
|
|
e0c750d27e | ||
|
|
b754a045eb | ||
|
|
5a24911fad | ||
|
|
0ec6897fda | ||
|
|
f57898a471 | ||
|
|
3ee29c2256 | ||
|
|
3b0fed55e4 | ||
|
|
31a32eb11d | ||
|
|
64c10b44f5 | ||
|
|
34365239c1 | ||
|
|
bdfeda23b6 | ||
|
|
1aaeac1fe3 | ||
|
|
f5578b0fc1 | ||
|
|
f82c439b26 | ||
|
|
c498cff096 | ||
|
|
3559477247 | ||
|
|
b9241fa1b2 | ||
|
|
9bf5153e6b | ||
|
|
eb6d3cd64e | ||
|
|
8cda2c164d | ||
|
|
c900d09cf8 | ||
|
|
edc8ef4f44 | ||
|
|
9aeb156d92 | ||
|
|
8be07de373 | ||
|
|
93482b0041 | ||
|
|
5b2c55142e | ||
|
|
088a3b7c28 | ||
|
|
b115643034 | ||
|
|
07fc964f9c | ||
|
|
3e51caf111 | ||
|
|
deb1ba339f | ||
|
|
70a1086edf | ||
|
|
c7d975e612 | ||
|
|
7104d6d6dd | ||
|
|
d933997c89 | ||
|
|
bea99bb4c6 | ||
|
|
f38a593434 | ||
|
|
85950991ab | ||
|
|
6fd740f8c0 | ||
|
|
ec2c2d8ccb | ||
|
|
efb2d5ef32 | ||
|
|
acb1e2434b | ||
|
|
8e52abda9a | ||
|
|
9764326242 | ||
|
|
97c33d6c54 | ||
|
|
30d5f3ad3f | ||
|
|
079c0defaf | ||
|
|
cf1afddb9e | ||
|
|
b07b9351c4 | ||
|
|
7911459817 | ||
|
|
ef9b1d5c2d | ||
|
|
fd2780624a | ||
|
|
4f22e88e42 | ||
|
|
0d45fe4a97 | ||
|
|
28ae2766f0 | ||
|
|
edd86024b9 | ||
|
|
f3c78c2c24 | ||
|
|
3f96608398 | ||
|
|
9ed43efe5d | ||
|
|
975180b075 | ||
|
|
c94eb7a48e | ||
|
|
bb1b0da749 | ||
|
|
85c8006977 | ||
|
|
850bdc4a4d | ||
|
|
ca95e32164 | ||
|
|
a141ec85b0 | ||
|
|
1298a96b0e | ||
|
|
1928b150ad | ||
|
|
7426f5484d | ||
|
|
11a8f313b0 | ||
|
|
69b6ef07a1 | ||
|
|
7a1ba9dabf | ||
|
|
48a739c94e | ||
|
|
9680f0cf12 | ||
|
|
5b1c89a0c5 | ||
|
|
8a1ab478a3 | ||
|
|
2ae8d49526 | ||
|
|
a0b673c138 | ||
|
|
c56c445fb7 | ||
|
|
c23275f2fe | ||
|
|
8b28a33b73 | ||
|
|
f8d086a743 | ||
|
|
c06d15de5f | ||
|
|
dcaf1f54e4 | ||
|
|
aa91abb022 | ||
|
|
67d204eb02 | ||
|
|
18e680b298 | ||
|
|
6ef1b072e8 | ||
|
|
ca9ef544ce | ||
|
|
7831a3438d | ||
|
|
1fad90441c | ||
|
|
7f26c5bd45 | ||
|
|
637641cf54 | ||
|
|
d9a316abbb | ||
|
|
093cfa5269 | ||
|
|
61a025de4d | ||
|
|
7df1ae7ed8 | ||
|
|
eb6e75b156 | ||
|
|
8480b3ac3d | ||
|
|
6339a31fec | ||
|
|
71b5bb3f8f | ||
|
|
a9878dbbdf | ||
|
|
b1fb9dd7d3 | ||
|
|
de51fbd7be | ||
|
|
c9412a97d0 | ||
|
|
63c29bdd75 | ||
|
|
bace26063d | ||
|
|
7931eb97b9 | ||
|
|
9df89c7b74 | ||
|
|
78978276c4 | ||
|
|
011eddbbc5 | ||
|
|
4e9f3fe1dc | ||
|
|
9bd48c19ff | ||
|
|
c0516c3665 | ||
|
|
06ff36c836 | ||
|
|
98c16eb1dc | ||
|
|
35d80840e5 | ||
|
|
ad3a778fb9 | ||
|
|
d25d1efe9c | ||
|
|
de58dff423 | ||
|
|
ec26e4d8d1 | ||
|
|
c24da41505 | ||
|
|
f785185e1c | ||
|
|
5ed3ed9a2d | ||
|
|
a3adace60e | ||
|
|
3db4de09e2 | ||
|
|
70b2ee0a84 | ||
|
|
e7d82850c9 | ||
|
|
e6a8f4e6dc | ||
|
|
494e268bc5 | ||
|
|
e1c6f65b7e | ||
|
|
89075c5588 | ||
|
|
c7fa9b8ce7 | ||
|
|
cb70082d31 | ||
|
|
48b9ac8f5b | ||
|
|
0be568bbbd | ||
|
|
ba44fa620f | ||
|
|
dde402afbf | ||
|
|
bbe5142ca9 | ||
|
|
e74ffac5b0 | ||
|
|
cfb8980e3a | ||
|
|
e06e1bdcbe | ||
|
|
d60ced2f61 | ||
|
|
e68689828e | ||
|
|
ba932758c8 | ||
|
|
ee43fa6311 | ||
|
|
ad6b164d51 | ||
|
|
4a5510acb2 | ||
|
|
970dbc4428 | ||
|
|
f8f37325eb | ||
|
|
bb999019ef | ||
|
|
533b6a155a | ||
|
|
4cdc92388c | ||
|
|
ccc6be1e71 | ||
|
|
b355778a92 | ||
|
|
6a12e8f37a | ||
|
|
59adb91f5f | ||
|
|
53677e2f39 | ||
|
|
1c74f43b22 | ||
|
|
b4801970e8 | ||
|
|
7a3a6b512f | ||
|
|
72ea33b6de | ||
|
|
d97192e0ee | ||
|
|
196d9fe4d2 | ||
|
|
e960fcc303 | ||
|
|
f334da95ff | ||
|
|
5d5f311e36 | ||
|
|
d577e97758 | ||
|
|
2dc92e26d3 | ||
|
|
f4994a36a3 | ||
|
|
7a785a8163 | ||
|
|
6ad0d860c7 | ||
|
|
38a2fa55df | ||
|
|
a21b170b52 | ||
|
|
44265b2362 | ||
|
|
069193342b | ||
|
|
54e9a56cda | ||
|
|
39bc9227dc | ||
|
|
ac636670c3 | ||
|
|
2abadc73e4 | ||
|
|
377368f6bf | ||
|
|
4085c10bfc | ||
|
|
657ba11e7e | ||
|
|
a9ae45fe63 | ||
|
|
61bb39b46f | ||
|
|
2ad106f7d7 | ||
|
|
8fd4fe0e55 | ||
|
|
b1c9aedac3 | ||
|
|
a80415be02 | ||
|
|
d9acd0d74b | ||
|
|
7ae09159ba | ||
|
|
a709abd80c | ||
|
|
cd07f39b69 | ||
|
|
f7c11d07a8 | ||
|
|
b07439d402 | ||
|
|
d8eadc2a2d | ||
|
|
3a88d4d3e6 | ||
|
|
012110f008 | ||
|
|
4de274bf62 | ||
|
|
76b89baee3 | ||
|
|
697ae92031 | ||
|
|
c87f92b346 | ||
|
|
6961bb7fd0 | ||
|
|
6e26130744 | ||
|
|
123a375a27 | ||
|
|
2b4b3ca0a5 | ||
|
|
c4a795418f | ||
|
|
91837ebade | ||
|
|
0492e910ea | ||
|
|
36c86e22b1 | ||
|
|
6bdc0c7bb2 | ||
|
|
1e8d8f749a | ||
|
|
2e8e3b0d1e | ||
|
|
15b8613d3c | ||
|
|
8ce266bc94 | ||
|
|
8661d72e45 | ||
|
|
62505f2543 | ||
|
|
37986c58ec | ||
|
|
2968d96fe9 | ||
|
|
e7c8d0e78c | ||
|
|
83cbb34a5b | ||
|
|
7559c7b67e | ||
|
|
02822f4b38 | ||
|
|
96736afb94 | ||
|
|
72ae132fcd | ||
|
|
2250e1bcab | ||
|
|
d9d5b746c3 | ||
|
|
f1ea306291 | ||
|
|
378d62395a | ||
|
|
99c92069b9 | ||
|
|
2a89ef797f | ||
|
|
5838550188 | ||
|
|
e0e01ae3ee | ||
|
|
0983ba8a0f | ||
|
|
0bfa776ce7 | ||
|
|
d2b09936d1 | ||
|
|
68e9f0f7c1 | ||
|
|
c3d345de80 | ||
|
|
385c0e246a | ||
|
|
5ead49a5b7 | ||
|
|
c0760b1347 | ||
|
|
e01b323aee | ||
|
|
6f4866ef63 | ||
|
|
1b6d72661c | ||
|
|
c59d4aea81 | ||
|
|
6260a80738 | ||
|
|
e75d3c8273 | ||
|
|
b7acb475e9 | ||
|
|
42b6bbff7c | ||
|
|
4b8542b35b | ||
|
|
9ad1d6cbfb | ||
|
|
4cdd9acd73 | ||
|
|
f4b0a695d6 | ||
|
|
b525ea1ba4 | ||
|
|
c1fc2c4766 | ||
|
|
5c733932c7 | ||
|
|
d1218616ec | ||
|
|
2bf6a03d56 | ||
|
|
b6ee63c1ea | ||
|
|
6d08efdcd7 | ||
|
|
a0a43a5651 | ||
|
|
3af2f5b032 | ||
|
|
8f54b226b4 | ||
|
|
9f64011b26 | ||
|
|
c5fc54e721 | ||
|
|
fc8a4fc5b6 | ||
|
|
6f9ab232ae | ||
|
|
8cb96f1e45 | ||
|
|
5733acb77a | ||
|
|
e49bcb2a69 | ||
|
|
42e41c399f | ||
|
|
166a3180d3 | ||
|
|
3bf4982f23 | ||
|
|
f4e1cccfac | ||
|
|
7911a8f49e | ||
|
|
64a96fc3ce | ||
|
|
8e2cfbddc5 | ||
|
|
45fae3f0fd | ||
|
|
e45a7824c1 | ||
|
|
5d72c48a76 | ||
|
|
d6169c6fa2 | ||
|
|
9df6d52e2d | ||
|
|
239de8e923 | ||
|
|
7d553a87f3 | ||
|
|
557b42bc56 | ||
|
|
8423914748 | ||
|
|
07dce23794 | ||
|
|
18fd0552db | ||
|
|
d537d48f8e | ||
|
|
b456512bbb |
61
.github/ISSUE_TEMPLATE/bug-issue.yml
vendored
61
.github/ISSUE_TEMPLATE/bug-issue.yml
vendored
@@ -1,61 +0,0 @@
|
|||||||
name: 🐞 Bug report
|
|
||||||
description: Create a new bug report.
|
|
||||||
title: 'bug: <title>'
|
|
||||||
labels: [bug]
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
# ReVanced Manager bug report
|
|
||||||
|
|
||||||
Please check for existing issues [here](https://github.com/revanced/revanced-manager/labels/bug) before creating a new one.
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Bug description
|
|
||||||
description: |
|
|
||||||
- Describe your bug in detail
|
|
||||||
- Add steps to reproduce the bug if possible (Step 1. Download some files. Step 2. ...)
|
|
||||||
- Add images and videos if possible
|
|
||||||
- List selected patches if applicable
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Version of ReVanced Manager and version & name of application you tried to patch
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: dropdown
|
|
||||||
attributes:
|
|
||||||
label: Installation type
|
|
||||||
options:
|
|
||||||
- Non-root
|
|
||||||
- Root
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Device logs
|
|
||||||
description: Export logs in ReVanced Manager settings.
|
|
||||||
render: shell
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Patcher logs
|
|
||||||
description: Export logs in "Patcher" screen.
|
|
||||||
render: shell
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: checkboxes
|
|
||||||
attributes:
|
|
||||||
label: Acknowledgements
|
|
||||||
description: Your issue will be closed if you don't follow the checklist below!
|
|
||||||
options:
|
|
||||||
- label: This request 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
|
|
||||||
- label: The issue is solely related to the ReVanced Manager
|
|
||||||
required: true
|
|
||||||
109
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
109
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
name: 🐞 Bug report
|
||||||
|
description: Report a bug or an issue.
|
||||||
|
title: 'bug: '
|
||||||
|
labels: ['Bug report']
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
<p align="center">
|
||||||
|
<picture>
|
||||||
|
<source
|
||||||
|
width="256px"
|
||||||
|
media="(prefers-color-scheme: dark)"
|
||||||
|
srcset="https://raw.githubusercontent.com/revanced/revanced-manager/main/assets/revanced-headline/revanced-headline-vertical-dark.svg"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
width="256px"
|
||||||
|
src="https://raw.githubusercontent.com/revanced/revanced-manager/main/assets/revanced-headline/revanced-headline-vertical-light.svg"
|
||||||
|
>
|
||||||
|
</picture>
|
||||||
|
<br>
|
||||||
|
<a href="https://revanced.app/">
|
||||||
|
<picture>
|
||||||
|
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/revanced/revanced-manager/main/assets/revanced-logo/revanced-logo.svg" />
|
||||||
|
<img height="24px" src="https://raw.githubusercontent.com/revanced/revanced-manager/main/assets/revanced-logo/revanced-logo.svg" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/ReVanced">
|
||||||
|
<picture>
|
||||||
|
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
|
||||||
|
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<a href="http://revanced.app/discord">
|
||||||
|
<picture>
|
||||||
|
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||||
|
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<a href="https://reddit.com/r/revancedapp">
|
||||||
|
<picture>
|
||||||
|
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||||
|
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<a href="https://t.me/app_revanced">
|
||||||
|
<picture>
|
||||||
|
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||||
|
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<a href="https://x.com/revancedapp">
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
|
||||||
|
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<a href="https://www.youtube.com/@ReVanced">
|
||||||
|
<picture>
|
||||||
|
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||||
|
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
Continuing the legacy of Vanced
|
||||||
|
</p>
|
||||||
|
|
||||||
|
# ReVanced Manager bug report
|
||||||
|
|
||||||
|
Before creating a new bug report, please keep the following in mind:
|
||||||
|
|
||||||
|
- **Do not submit a duplicate bug report**: Search for existing bug reports [here](https://github.com/ReVanced/revanced-manager/issues?q=label%3A%22Bug+report%22).
|
||||||
|
- **Review the contribution guidelines**: Make sure your bug report adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-manager/blob/main/CONTRIBUTING.md).
|
||||||
|
- **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app).
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Bug description
|
||||||
|
description: |
|
||||||
|
- Describe your bug in detail
|
||||||
|
- Add steps to reproduce the bug if possible (Step 1. ... Step 2. ...)
|
||||||
|
- Add images and videos if possible
|
||||||
|
- List used patches, downloader and settings if applicable
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Patch logs
|
||||||
|
description: Patch logs can be exported by clicking on the "Logs" button in the "Patcher" screen, when patching finishes.
|
||||||
|
render: shell
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Debug logs
|
||||||
|
description: Debug logs can be exported by clicking on "Export debug logs" in "Settings" > "Advanced".
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: Acknowledgements
|
||||||
|
description: Your bug report will be closed if you don't follow the checklist below.
|
||||||
|
options:
|
||||||
|
- label: I have checked all open and closed bug reports and this is not a duplicate.
|
||||||
|
required: true
|
||||||
|
- label: I have chosen an appropriate title.
|
||||||
|
required: true
|
||||||
|
- label: All requested information has been provided properly.
|
||||||
|
required: true
|
||||||
|
- label: The bug is only related to ReVanced Manager.
|
||||||
|
required: true
|
||||||
6
.github/ISSUE_TEMPLATE/config.yml
vendored
6
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1 +1,5 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: 🗨 Discussions
|
||||||
|
url: https://github.com/revanced/revanced-suggestions/discussions
|
||||||
|
about: Have something unspecific to ReVanced Manager in mind? Search for or start a new discussion!
|
||||||
42
.github/ISSUE_TEMPLATE/feature-issue.yml
vendored
42
.github/ISSUE_TEMPLATE/feature-issue.yml
vendored
@@ -1,42 +0,0 @@
|
|||||||
name: ⭐ Feature request
|
|
||||||
description: Create a new feature request.
|
|
||||||
title: 'feat: <title>'
|
|
||||||
labels: [feature request]
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
# ReVanced Manager feature request
|
|
||||||
|
|
||||||
Please check for existing feature requests [here](https://github.com/revanced/revanced-manager/labels/bug) before creating a new one.
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Feature description
|
|
||||||
description: Describe your feature in detail.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Motivation
|
|
||||||
description: Explain why the lack of it is a problem.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Additional context
|
|
||||||
description: In case there is something else you want to add.
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: checkboxes
|
|
||||||
attributes:
|
|
||||||
label: Acknowledgements
|
|
||||||
description: Your issue will be closed if you don't follow the checklist below!
|
|
||||||
options:
|
|
||||||
- label: This request 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
|
|
||||||
- label: The issue is solely related to the ReVanced Manager
|
|
||||||
required: true
|
|
||||||
103
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
103
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
<p align="center">
|
||||||
|
<picture>
|
||||||
|
<source
|
||||||
|
width="256px"
|
||||||
|
media="(prefers-color-scheme: dark)"
|
||||||
|
srcset="https://raw.githubusercontent.com/revanced/revanced-manager/main/assets/revanced-headline/revanced-headline-vertical-dark.svg"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
width="256px"
|
||||||
|
src="https://raw.githubusercontent.com/revanced/revanced-manager/main/assets/revanced-headline/revanced-headline-vertical-light.svg"
|
||||||
|
>
|
||||||
|
</picture>
|
||||||
|
<br>
|
||||||
|
<a href="https://revanced.app/">
|
||||||
|
<picture>
|
||||||
|
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/revanced/revanced-manager/main/assets/revanced-logo/revanced-logo.svg" />
|
||||||
|
<img height="24px" src="https://raw.githubusercontent.com/revanced/revanced-manager/main/assets/revanced-logo/revanced-logo.svg" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/ReVanced">
|
||||||
|
<picture>
|
||||||
|
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
|
||||||
|
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<a href="http://revanced.app/discord">
|
||||||
|
<picture>
|
||||||
|
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||||
|
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<a href="https://reddit.com/r/revancedapp">
|
||||||
|
<picture>
|
||||||
|
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||||
|
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<a href="https://t.me/app_revanced">
|
||||||
|
<picture>
|
||||||
|
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||||
|
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<a href="https://x.com/revancedapp">
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
|
||||||
|
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<a href="https://www.youtube.com/@ReVanced">
|
||||||
|
<picture>
|
||||||
|
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||||
|
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
Continuing the legacy of Vanced
|
||||||
|
</p>
|
||||||
|
|
||||||
|
# ReVanced Manager feature request
|
||||||
|
|
||||||
|
Before creating a new feature request, please keep the following in mind:
|
||||||
|
|
||||||
|
- **Do not submit a duplicate feature request**: Search for existing feature requests [here](https://github.com/ReVanced/revanced-manager/issues?q=label%3A%22Feature+request%22).
|
||||||
|
- **Review the contribution guidelines**: Make sure your feature request adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-manager/blob/main/CONTRIBUTING.md).
|
||||||
|
- **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app).
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Feature description
|
||||||
|
description: |
|
||||||
|
- Describe your feature in detail
|
||||||
|
- Add images, videos, links, examples, references, etc. if possible
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Motivation
|
||||||
|
description: |
|
||||||
|
A strong motivation is necessary for a feature request to be considered.
|
||||||
|
|
||||||
|
- Why should this feature be implemented?
|
||||||
|
- What is the explicit use case?
|
||||||
|
- What are the benefits?
|
||||||
|
- What makes this feature important?
|
||||||
|
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: I have checked all open and closed feature requests and this is not a duplicate
|
||||||
|
required: true
|
||||||
|
- label: I have chosen an appropriate title.
|
||||||
|
required: true
|
||||||
|
- label: All requested information has been provided properly.
|
||||||
|
required: true
|
||||||
|
- label: The feature request is only related to ReVanced Manager.
|
||||||
|
required: true
|
||||||
2
.github/config.yaml
vendored
2
.github/config.yaml
vendored
@@ -1,2 +1,2 @@
|
|||||||
firstPRMergeComment: >
|
firstPRMergeComment: >
|
||||||
❤️ Thank you for contributing to ReVanced Manager. Join us on [Discord](https://revanced.app/discord) if you want to receive a contributor role.
|
Thank you for contributing to ReVanced. Join us on [Discord](https://revanced.app/discord) to receive a role for your contribution.
|
||||||
|
|||||||
31
.github/workflows/build_pull_request.yml
vendored
Normal file
31
.github/workflows/build_pull_request.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
name: Build pull request
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
name: Build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Cache Gradle
|
||||||
|
uses: burrunan/gradle-cache-action@v1
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: ./gradlew assembleRelease --no-daemon
|
||||||
|
|
||||||
|
- name: Upload artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: revanced-manager
|
||||||
|
path: |
|
||||||
|
app/build/outputs/apk/release/revanced-manager*.apk
|
||||||
|
app/build/outputs/apk/release/revanced-manager*.apk.asc
|
||||||
26
.github/workflows/open_pull_request.yml
vendored
Normal file
26
.github/workflows/open_pull_request.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
name: Open a PR to main
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
MESSAGE: Merge branch `${{ github.head_ref || github.ref_name }}` to `main`
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pull-request:
|
||||||
|
name: Open pull request
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Open pull request
|
||||||
|
uses: repo-sync/pull-request@v2
|
||||||
|
with:
|
||||||
|
destination_branch: 'main'
|
||||||
|
pr_title: 'chore: ${{ env.MESSAGE }}'
|
||||||
|
pr_body: 'This pull request will ${{ env.MESSAGE }}.'
|
||||||
|
pr_draft: true
|
||||||
44
.github/workflows/pr-build.yml
vendored
44
.github/workflows/pr-build.yml
vendored
@@ -1,44 +0,0 @@
|
|||||||
name: Build pull request
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- ".github/workflows/pr-build.yml"
|
|
||||||
- "app/**"
|
|
||||||
- "gradle/**"
|
|
||||||
- "*.properties"
|
|
||||||
- ".kts"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up JDK 17
|
|
||||||
uses: actions/setup-java@v4
|
|
||||||
with:
|
|
||||||
java-version: '17'
|
|
||||||
distribution: 'temurin'
|
|
||||||
|
|
||||||
- name: Set up Gradle
|
|
||||||
uses: gradle/actions/setup-gradle@v4
|
|
||||||
|
|
||||||
- name: Build with Gradle
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: ./gradlew assembleRelease --no-daemon -PnoProguard -PsignAsDebug
|
|
||||||
|
|
||||||
- name: Set env
|
|
||||||
run: echo "COMMIT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Add hash to APK
|
|
||||||
run: mv app/build/outputs/apk/release/app-release.apk revanced-manager-${{ env.COMMIT_HASH }}.apk
|
|
||||||
|
|
||||||
- name: Upload build
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: revanced-manager
|
|
||||||
path: revanced-manager-${{ env.COMMIT_HASH }}.apk
|
|
||||||
57
.github/workflows/release-build.yml
vendored
57
.github/workflows/release-build.yml
vendored
@@ -1,57 +0,0 @@
|
|||||||
name: Release Build
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "v*"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
id-token: write
|
|
||||||
attestations: write
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Set env
|
|
||||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Set up JDK 17
|
|
||||||
uses: actions/setup-java@v4
|
|
||||||
with:
|
|
||||||
java-version: '17'
|
|
||||||
distribution: 'temurin'
|
|
||||||
|
|
||||||
- name: Set up Gradle
|
|
||||||
uses: gradle/actions/setup-gradle@v4
|
|
||||||
|
|
||||||
- name: Build with Gradle
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: ./gradlew assembleRelease --no-daemon
|
|
||||||
|
|
||||||
- name: Sign APK
|
|
||||||
id: sign_apk
|
|
||||||
uses: ilharp/sign-android-release@v1
|
|
||||||
with:
|
|
||||||
releaseDir: ./app/build/outputs/apk/release/
|
|
||||||
signingKey: ${{ secrets.SIGNING_KEYSTORE }}
|
|
||||||
keyStorePassword: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }}
|
|
||||||
keyAlias: ${{ secrets.SIGNING_KEY_ALIAS }}
|
|
||||||
keyPassword: ${{ secrets.SIGNING_KEY_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Add version to APK
|
|
||||||
run: mv ${{ steps.sign_apk.outputs.signedFile }} revanced-manager-${{ env.RELEASE_VERSION }}.apk
|
|
||||||
|
|
||||||
- name: Generate artifact attestation
|
|
||||||
uses: actions/attest-build-provenance@v1
|
|
||||||
with:
|
|
||||||
subject-path: revanced-manager-${{ env.RELEASE_VERSION }}.apk
|
|
||||||
|
|
||||||
- name: Publish release APK
|
|
||||||
uses: "marvinpinto/action-automatic-releases@latest"
|
|
||||||
with:
|
|
||||||
repo_token: "${{ secrets.GITHUB_TOKEN }}"
|
|
||||||
prerelease: false
|
|
||||||
files: revanced-manager-${{ env.RELEASE_VERSION }}.apk
|
|
||||||
80
.github/workflows/release.yml
vendored
Normal file
80
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
name: Release
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
packages: write
|
||||||
|
id-token: write
|
||||||
|
attestations: write
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Java
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'temurin'
|
||||||
|
java-version: '17'
|
||||||
|
|
||||||
|
- name: Cache Gradle
|
||||||
|
uses: burrunan/gradle-cache-action@v3
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
env:
|
||||||
|
GITHUB_ACTOR: ${{ github.actor }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: ./gradlew assembleRelease
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "lts/*"
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Import GPG key
|
||||||
|
uses: crazy-max/ghaction-import-gpg@v6
|
||||||
|
with:
|
||||||
|
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
|
passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
||||||
|
fingerprint: ${{ vars.GPG_FINGERPRINT }}
|
||||||
|
|
||||||
|
- name: Setup keystore
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets.KEYSTORE }}" | base64 --decode > "app/keystore.jks"
|
||||||
|
|
||||||
|
- name: Release API
|
||||||
|
run: npx multi-semantic-release --tag-format 'api@${version}' --ignore-packages app
|
||||||
|
env:
|
||||||
|
GITHUB_ACTOR: ${{ github.actor }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Release
|
||||||
|
id: release
|
||||||
|
run: |
|
||||||
|
echo "NEW_TAG=$(npx multi-semantic-release --tag-format 'v${version}' --ignore-packages api | tee | grep 'Created tag ' | sed -E 's/.*Created tag ([^ ]+).*/\1/')" >> $GITHUB_OUTPUT
|
||||||
|
env:
|
||||||
|
GITHUB_ACTOR: ${{ github.actor }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
||||||
|
KEYSTORE_ENTRY_ALIAS: ${{ secrets.KEYSTORE_ENTRY_ALIAS }}
|
||||||
|
KEYSTORE_ENTRY_PASSWORD: ${{ secrets.KEYSTORE_ENTRY_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Attest
|
||||||
|
if: steps.release.outputs.NEW_TAG != ''
|
||||||
|
uses: actions/attest-build-provenance@v2
|
||||||
|
with:
|
||||||
|
subject-name: 'ReVanced Manager ${{ steps.release.outputs.NEW_TAG }}'
|
||||||
|
subject-path: app/build/outputs/apk/release/revanced-manager*.apk
|
||||||
@@ -16,4 +16,4 @@ jobs:
|
|||||||
token: ${{ secrets.DOCUMENTATION_REPO_ACCESS_TOKEN }}
|
token: ${{ secrets.DOCUMENTATION_REPO_ACCESS_TOKEN }}
|
||||||
repository: revanced/revanced-documentation
|
repository: revanced/revanced-documentation
|
||||||
event-type: update-documentation
|
event-type: update-documentation
|
||||||
client-payload: '{"repo": "${{ github.event.repository.name }}", "ref": "${{ github.ref }}"}'
|
client-payload: '{"repo": "${{ github.event.repository.name }}", "ref": "${{ github.ref }}"}'
|
||||||
136
.gitignore
vendored
136
.gitignore
vendored
@@ -1,12 +1,132 @@
|
|||||||
|
### Java template
|
||||||
|
# Compiled class file
|
||||||
|
*.class
|
||||||
|
|
||||||
|
# Log file
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# BlueJ files
|
||||||
|
*.ctxt
|
||||||
|
|
||||||
|
# Mobile Tools for Java (J2ME)
|
||||||
|
.mtj.tmp/
|
||||||
|
|
||||||
|
# Package Files #
|
||||||
|
*.jar
|
||||||
|
*.war
|
||||||
|
*.nar
|
||||||
|
*.ear
|
||||||
|
*.zip
|
||||||
|
*.tar.gz
|
||||||
|
*.rar
|
||||||
|
|
||||||
|
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||||
|
hs_err_pid*
|
||||||
|
|
||||||
|
### JetBrains template
|
||||||
|
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||||
|
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||||
|
|
||||||
|
# User-specific stuff
|
||||||
|
.idea/**/workspace.xml
|
||||||
|
.idea/**/tasks.xml
|
||||||
|
.idea/**/usage.statistics.xml
|
||||||
|
.idea/**/dictionaries
|
||||||
|
.idea/**/shelf
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
.idea/**/contentModel.xml
|
||||||
|
|
||||||
|
# Sensitive or high-churn files
|
||||||
|
.idea/**/dataSources/
|
||||||
|
.idea/**/dataSources.ids
|
||||||
|
.idea/**/dataSources.local.xml
|
||||||
|
.idea/**/sqlDataSources.xml
|
||||||
|
.idea/**/dynamic.xml
|
||||||
|
.idea/**/uiDesigner.xml
|
||||||
|
.idea/**/dbnavigator.xml
|
||||||
|
|
||||||
|
# Gradle
|
||||||
|
.idea/**/gradle.xml
|
||||||
|
.idea/**/libraries
|
||||||
|
|
||||||
|
# Gradle and Maven with auto-import
|
||||||
|
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||||
|
# since they will be recreated, and may cause churn. Uncomment if using
|
||||||
|
# auto-import.
|
||||||
|
.idea/artifacts
|
||||||
|
.idea/compiler.xml
|
||||||
|
.idea/jarRepositories.xml
|
||||||
|
.idea/modules.xml
|
||||||
|
.idea/*.iml
|
||||||
|
.idea/modules
|
||||||
*.iml
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
|
||||||
|
# CMake
|
||||||
|
cmake-build-*/
|
||||||
|
|
||||||
|
# Mongo Explorer plugin
|
||||||
|
.idea/**/mongoSettings.xml
|
||||||
|
|
||||||
|
# File-based project format
|
||||||
|
*.iws
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
out/
|
||||||
|
|
||||||
|
# mpeltonen/sbt-idea plugin
|
||||||
|
.idea_modules/
|
||||||
|
|
||||||
|
# JIRA plugin
|
||||||
|
atlassian-ide-plugin.xml
|
||||||
|
|
||||||
|
# Cursive Clojure plugin
|
||||||
|
.idea/replstate.xml
|
||||||
|
|
||||||
|
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||||
|
com_crashlytics_export_strings.xml
|
||||||
|
crashlytics.properties
|
||||||
|
crashlytics-build.properties
|
||||||
|
fabric.properties
|
||||||
|
|
||||||
|
# Editor-based Rest Client
|
||||||
|
.idea/httpRequests
|
||||||
|
|
||||||
|
# Android studio 3.1+ serialized cache file
|
||||||
|
.idea/caches/build_file_checksums.ser
|
||||||
|
|
||||||
|
### Gradle template
|
||||||
.gradle
|
.gradle
|
||||||
/local.properties
|
**/build/
|
||||||
/.idea
|
!src/**/build/
|
||||||
.DS_Store
|
|
||||||
/build
|
# Ignore Gradle GUI config
|
||||||
/captures
|
gradle-app.setting
|
||||||
.externalNativeBuild
|
|
||||||
.cxx
|
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
|
||||||
local.properties
|
!gradle-wrapper.jar
|
||||||
|
|
||||||
|
# Cache of project
|
||||||
|
.gradletasknamecache
|
||||||
|
|
||||||
|
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
|
||||||
|
# gradle/wrapper/gradle-wrapper.properties
|
||||||
|
|
||||||
|
# Potentially copyrighted test APK
|
||||||
|
*.apk
|
||||||
|
|
||||||
|
# Ignore vscode config
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Ignore IDEA files
|
||||||
|
.idea/
|
||||||
|
|
||||||
.kotlin/
|
.kotlin/
|
||||||
|
|
||||||
|
local.properties
|
||||||
|
|
||||||
|
.cxx
|
||||||
@@ -58,20 +58,46 @@
|
|||||||
Continuing the legacy of Vanced
|
Continuing the legacy of Vanced
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
# 🔒 Security Policy
|
# 👋 Contribution guidelines
|
||||||
|
|
||||||
This document describes how to report security vulnerabilities for ReVanced Manager.
|
This document describes how to contribute to ReVanced Manager.
|
||||||
|
|
||||||
## 🚨 Reporting a Vulnerability
|
## 📖 Resources to help you get started
|
||||||
|
|
||||||
Please open an issue in our [advisory tracker](https://github.com/ReVanced/revanced-manager/security/advisories/new) or reach out privately to us on [Discord](https://discord.gg/revanced).
|
* The [documentation](/docs/README.md) provides steps to build ReVanced Manager from source
|
||||||
|
* Our [backlog](https://github.com/orgs/ReVanced/projects/12) is where we keep track of what we're working on
|
||||||
|
* [Issues](https://github.com/ReVanced/revanced-manager/issues) are where we keep track of bugs and feature requests
|
||||||
|
|
||||||
If a vulnerability is confirmed and accepted, you can join our [Discord](https://discord.gg/revanced) server to receive a special contributor role.
|
## 🙏 Submitting a feature request
|
||||||
|
|
||||||
### ⏳ Supported Versions
|
Features can be requested by opening an issue using the
|
||||||
|
[Feature request issue template](https://github.com/ReVanced/revanced-manager/issues/new?assignees=&labels=Feature+request&projects=&template=feature_request.yml&title=feat%3A+).
|
||||||
|
|
||||||
| Version | Branch | Supported |
|
> **Note**
|
||||||
| --------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------------------ |
|
> Requests can be accepted or rejected at the discretion of maintainers of ReVanced Manager.
|
||||||
|  | main | :white_check_mark: |
|
> Good motivation has to be provided for a request to be accepted.
|
||||||
|  | dev | :white_check_mark: |
|
|
||||||
|  | compose-dev | :white_check_mark: |
|
## 🐞 Submitting a bug report
|
||||||
|
|
||||||
|
If you encounter a bug while using ReVanced Manager, open an issue using the
|
||||||
|
[Bug report issue template](https://github.com/ReVanced/revanced-manager/issues/new?assignees=&labels=Bug+report&projects=&template=bug_report.yml&title=bug%3A+).
|
||||||
|
|
||||||
|
## 📝 How to contribute
|
||||||
|
|
||||||
|
1. Before contributing, it is recommended to open an issue to discuss your change
|
||||||
|
with the maintainers of ReVanced Manager. This will help you determine whether your change is acceptable
|
||||||
|
and whether it is worth your time to implement it
|
||||||
|
2. Development happens on the `dev` branch. Fork the repository and create your branch from `dev`
|
||||||
|
3. Commit your changes
|
||||||
|
4. Submit a pull request to the `dev` branch of the repository and reference issues
|
||||||
|
that your pull request closes in the description of your pull request
|
||||||
|
5. Our team will review your pull request and provide feedback. Once your pull request is approved,
|
||||||
|
it will be merged into the `dev` branch and will be included in the next release of ReVanced Manager
|
||||||
|
|
||||||
|
## 🤚 I want to contribute but don't know how to code
|
||||||
|
|
||||||
|
Even if you don't know how to code, you can still contribute by
|
||||||
|
translating ReVanced Manager on [Crowdin](https://translate.revanced.app/).
|
||||||
|
|
||||||
|
❤️ Thank you for considering contributing to ReVanced Manager,
|
||||||
|
ReVanced
|
||||||
119
README.md
119
README.md
@@ -1,55 +1,104 @@
|
|||||||
# ReVanced Manager (Compose Rewrite)
|
<p align="center">
|
||||||
|
<picture>
|
||||||
|
<source
|
||||||
|
width="256px"
|
||||||
|
media="(prefers-color-scheme: dark)"
|
||||||
|
srcset="assets/revanced-headline/revanced-headline-vertical-dark.svg"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
width="256px"
|
||||||
|
src="assets/revanced-headline/revanced-headline-vertical-light.svg"
|
||||||
|
>
|
||||||
|
</picture>
|
||||||
|
<br>
|
||||||
|
<a href="https://revanced.app/">
|
||||||
|
<picture>
|
||||||
|
<source height="24px" media="(prefers-color-scheme: dark)" srcset="assets/revanced-logo/revanced-logo.svg" />
|
||||||
|
<img height="24px" src="assets/revanced-logo/revanced-logo.svg" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/ReVanced">
|
||||||
|
<picture>
|
||||||
|
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
|
||||||
|
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<a href="http://revanced.app/discord">
|
||||||
|
<picture>
|
||||||
|
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||||
|
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<a href="https://reddit.com/r/revancedapp">
|
||||||
|
<picture>
|
||||||
|
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||||
|
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<a href="https://t.me/app_revanced">
|
||||||
|
<picture>
|
||||||
|
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||||
|
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<a href="https://x.com/revancedapp">
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
|
||||||
|
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<a href="https://www.youtube.com/@ReVanced">
|
||||||
|
<picture>
|
||||||
|
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||||
|
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
Continuing the legacy of Vanced
|
||||||
|
</p>
|
||||||
|
|
||||||
[](../../blob/main/LICENSE)
|
# 💊 ReVanced Manager
|
||||||
[](https://github.com/ReVanced/revanced-manager/commits/compose-dev)
|
|
||||||
[](https://github.com/ReVanced/revanced-manager/commits/compose-dev)
|
|
||||||
|
|
||||||
_(Yet another)_ rewrite of the ReVanced Manager using Kotlin and Jetpack Compose.
|

|
||||||
|

|
||||||
|
|
||||||
## Design system
|
Application to use ReVanced on Android
|
||||||
|
|
||||||
In this rewrite, we are adopting the latest Material Design principles and guidelines by using Material 3 and Material You.
|
## ❓ About
|
||||||
|
|
||||||
Material Design is a design system developed by Google that provides a unified visual language for building beautiful and consistent user interfaces across all platforms and devices. Material You is an extension of Material Design that provides even more customization options for users, making it possible for them to personalize their device and create a unique look and feel.
|
ReVanced Manager is an application that uses [ReVanced Patcher](https://github.com/revanced/revanced-patcher) to patch Android apps.
|
||||||
|
|
||||||
### Why Material 3?
|
## 💪 Features
|
||||||
|
|
||||||
* **Consistent design language**
|
Some of the features ReVanced Manager provides are:
|
||||||
* **Improved accessibility**
|
|
||||||
* **Better user experience**
|
|
||||||
|
|
||||||
By using Material 3 and Material You, we are ensuring that the app's user interface is consistent, customizable, accessible, and engaging for our users. This will help to improve the overall user experience and increase user satisfaction with the the manager.
|
- ⬇️ **Download**: Automatically download apps using the ReVanced Manager downloader plugin system
|
||||||
|
- 💉 **Patch**: Select and apply patches to any Android app
|
||||||
## Technology stack
|
- 🛠️ **Customize**: Manage patches, apps, signing, themes, updates, and many more settings
|
||||||
|
|
||||||
* Kotlin: Kotlin is a modern and concise programming language that is fully interoperable with Java and provides improved safety, readability, and maintainability compared to Java.
|
|
||||||
* Jetpack Compose: Jetpack Compose is a modern UI toolkit for Android development that allows developers to build beautiful and performant user interfaces using declarative programming. It provides a unified and efficient way of building UI that is well-integrated with the Android framework.
|
|
||||||
|
|
||||||
## Why Kotlin and Compose?
|
|
||||||
|
|
||||||
* **Improved safety:** Kotlin provides improved safety compared to Java, which reduces the likelihood of common programming mistakes that can cause security vulnerabilities or crashes.
|
|
||||||
* **Concise and readable code:** Kotlin's concise syntax and expressive type system make the code more readable, which makes it easier for developers to understand and maintain the codebase.
|
|
||||||
* **Better performance:** Jetpack Compose uses the power of the Android framework to provide smooth and fast performance, which enhances the user experience.
|
|
||||||
* **Modern and efficient UI development:** Jetpack Compose provides a modern and efficient way of building UI, which makes it easier for developers to create beautiful and performant user interfaces.
|
|
||||||
|
|
||||||
## 🔽 Download
|
## 🔽 Download
|
||||||
|
|
||||||
You can obtain ReVanced Manager by downloading it from either [revanced.app/download](https://revanced.app/download) or [GitHub Releases](https://github.com/ReVanced/revanced-manager/releases)
|
You can download the most recent version of ReVanced Manager at [revanced.app/download](https://revanced.app/download) or from [GitHub releases](https://github.com/ReVanced/revanced-manager/releases/latest).
|
||||||
|
Learn how to use ReVanced Manager by following the [documentation](/docs).
|
||||||
|
|
||||||
## 📝 Prerequisites
|
## 📚 Everything else
|
||||||
|
|
||||||
For a list of prerequisites, refer to [docs/0_prerequisites.md](docs/0_prerequisites.md)
|
### 📙 Contributing
|
||||||
|
|
||||||
## 🔴 Issues
|
Thank you for considering contributing to ReVanced Manager.
|
||||||
|
You can find the contribution guidelines [here](CONTRIBUTING.md).
|
||||||
|
|
||||||
For suggestions and bug reports, open an issue [here](https://github.com/revanced/revanced-manager/issues/new/choose).
|
### 🛠️ Building
|
||||||
|
|
||||||
## 🌐 Translation
|
To build a ReVanced Manager, you can follow the [documentation](/docs).
|
||||||
|
|
||||||
[](https://crowdin.com/project/revanced)
|
### 📄 Documentation
|
||||||
|
|
||||||
We're accepting translations on [Crowdin](https://translate.revanced.app)
|
You can find the documentation for ReVanced Manager [here](/docs).
|
||||||
|
|
||||||
## 🛠️ Building Manager from source
|
## ⚖️ License
|
||||||
|
|
||||||
For instructions on how to build ReVanced Manager from source, refer to [docs/4_building.md](docs/4_building.md)
|
ReVanced Manager is licensed under the GPLv3 license. Please see the [license file](LICENSE) for more information.
|
||||||
|
[tl;dr](https://www.tldrlegal.com/license/gnu-general-public-license-v3-gpl-3) you may copy, distribute and modify ReVanced Manager as long as you track changes/dates in source files.
|
||||||
|
Any modifications to ReVanced Manager must also be made available under the GPL, along with build & install instructions.
|
||||||
|
|||||||
8
adsfund.json
Normal file
8
adsfund.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"info": "This is verification file for ads.fund project",
|
||||||
|
"project": {
|
||||||
|
"name": "ReVanced Manager",
|
||||||
|
"walletAddress": "0x7ab4091e00363654bf84B34151225742cd92FCE5",
|
||||||
|
"tokenAddress": "0xadf954bc6f509b3a32fb5e97ed4ba6c000e37155"
|
||||||
|
}
|
||||||
|
}
|
||||||
39
api/.releaserc
Normal file
39
api/.releaserc
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"branches": [
|
||||||
|
"main",
|
||||||
|
{
|
||||||
|
"name": "dev",
|
||||||
|
"prerelease": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
[
|
||||||
|
"@semantic-release/commit-analyzer",
|
||||||
|
{
|
||||||
|
"releaseRules": [
|
||||||
|
{ "type": "build", "scope": "Needs bump", "release": "patch" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@semantic-release/release-notes-generator",
|
||||||
|
"@semantic-release/changelog",
|
||||||
|
"gradle-semantic-release-plugin",
|
||||||
|
[
|
||||||
|
"@semantic-release/git",
|
||||||
|
{
|
||||||
|
"assets": [
|
||||||
|
"CHANGELOG.md",
|
||||||
|
"gradle.properties"
|
||||||
|
],
|
||||||
|
"message": "chore: Release API v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@saithodev/semantic-release-backmerge",
|
||||||
|
{
|
||||||
|
"backmergeBranches": [{"from": "main", "to": "dev"}],
|
||||||
|
"clearWorkspace": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ public final class app/revanced/manager/plugin/downloader/ConstantsKt {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public final class app/revanced/manager/plugin/downloader/DownloadUrl : android/os/Parcelable {
|
public final class app/revanced/manager/plugin/downloader/DownloadUrl : android/os/Parcelable {
|
||||||
|
public static final field $stable I
|
||||||
public static final field CREATOR Landroid/os/Parcelable$Creator;
|
public static final field CREATOR Landroid/os/Parcelable$Creator;
|
||||||
public fun <init> (Ljava/lang/String;Ljava/util/Map;)V
|
public fun <init> (Ljava/lang/String;Ljava/util/Map;)V
|
||||||
public synthetic fun <init> (Ljava/lang/String;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
|
public synthetic fun <init> (Ljava/lang/String;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||||
@@ -32,9 +33,11 @@ public final class app/revanced/manager/plugin/downloader/DownloadUrl$Creator :
|
|||||||
}
|
}
|
||||||
|
|
||||||
public final class app/revanced/manager/plugin/downloader/Downloader {
|
public final class app/revanced/manager/plugin/downloader/Downloader {
|
||||||
|
public static final field $stable I
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class app/revanced/manager/plugin/downloader/DownloaderBuilder {
|
public final class app/revanced/manager/plugin/downloader/DownloaderBuilder {
|
||||||
|
public static final field $stable I
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class app/revanced/manager/plugin/downloader/DownloaderKt {
|
public final class app/revanced/manager/plugin/downloader/DownloaderKt {
|
||||||
@@ -42,6 +45,7 @@ public final class app/revanced/manager/plugin/downloader/DownloaderKt {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public final class app/revanced/manager/plugin/downloader/DownloaderScope : app/revanced/manager/plugin/downloader/Scope {
|
public final class app/revanced/manager/plugin/downloader/DownloaderScope : app/revanced/manager/plugin/downloader/Scope {
|
||||||
|
public static final field $stable I
|
||||||
public final fun download (Lkotlin/jvm/functions/Function3;)V
|
public final fun download (Lkotlin/jvm/functions/Function3;)V
|
||||||
public final fun get (Lkotlin/jvm/functions/Function4;)V
|
public final fun get (Lkotlin/jvm/functions/Function4;)V
|
||||||
public fun getHostPackageName ()Ljava/lang/String;
|
public fun getHostPackageName ()Ljava/lang/String;
|
||||||
@@ -65,6 +69,7 @@ public abstract interface class app/revanced/manager/plugin/downloader/OutputDow
|
|||||||
}
|
}
|
||||||
|
|
||||||
public final class app/revanced/manager/plugin/downloader/Package : android/os/Parcelable {
|
public final class app/revanced/manager/plugin/downloader/Package : android/os/Parcelable {
|
||||||
|
public static final field $stable I
|
||||||
public static final field CREATOR Landroid/os/Parcelable$Creator;
|
public static final field CREATOR Landroid/os/Parcelable$Creator;
|
||||||
public fun <init> (Ljava/lang/String;Ljava/lang/String;)V
|
public fun <init> (Ljava/lang/String;Ljava/lang/String;)V
|
||||||
public final fun component1 ()Ljava/lang/String;
|
public final fun component1 ()Ljava/lang/String;
|
||||||
@@ -97,22 +102,27 @@ public abstract interface class app/revanced/manager/plugin/downloader/Scope {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public abstract class app/revanced/manager/plugin/downloader/UserInteractionException : java/lang/Exception {
|
public abstract class app/revanced/manager/plugin/downloader/UserInteractionException : java/lang/Exception {
|
||||||
|
public static final field $stable I
|
||||||
public synthetic fun <init> (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
|
public synthetic fun <init> (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract class app/revanced/manager/plugin/downloader/UserInteractionException$Activity : app/revanced/manager/plugin/downloader/UserInteractionException {
|
public abstract class app/revanced/manager/plugin/downloader/UserInteractionException$Activity : app/revanced/manager/plugin/downloader/UserInteractionException {
|
||||||
|
public static final field $stable I
|
||||||
public synthetic fun <init> (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
|
public synthetic fun <init> (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class app/revanced/manager/plugin/downloader/UserInteractionException$Activity$Cancelled : app/revanced/manager/plugin/downloader/UserInteractionException$Activity {
|
public final class app/revanced/manager/plugin/downloader/UserInteractionException$Activity$Cancelled : app/revanced/manager/plugin/downloader/UserInteractionException$Activity {
|
||||||
|
public static final field $stable I
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class app/revanced/manager/plugin/downloader/UserInteractionException$Activity$NotCompleted : app/revanced/manager/plugin/downloader/UserInteractionException$Activity {
|
public final class app/revanced/manager/plugin/downloader/UserInteractionException$Activity$NotCompleted : app/revanced/manager/plugin/downloader/UserInteractionException$Activity {
|
||||||
|
public static final field $stable I
|
||||||
public final fun getIntent ()Landroid/content/Intent;
|
public final fun getIntent ()Landroid/content/Intent;
|
||||||
public final fun getResultCode ()I
|
public final fun getResultCode ()I
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class app/revanced/manager/plugin/downloader/UserInteractionException$RequestDenied : app/revanced/manager/plugin/downloader/UserInteractionException {
|
public final class app/revanced/manager/plugin/downloader/UserInteractionException$RequestDenied : app/revanced/manager/plugin/downloader/UserInteractionException {
|
||||||
|
public static final field $stable I
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class app/revanced/manager/plugin/downloader/webview/APIKt {
|
public final class app/revanced/manager/plugin/downloader/webview/APIKt {
|
||||||
@@ -163,6 +173,7 @@ public abstract interface class app/revanced/manager/plugin/downloader/webview/W
|
|||||||
}
|
}
|
||||||
|
|
||||||
public final class app/revanced/manager/plugin/downloader/webview/WebViewScope : app/revanced/manager/plugin/downloader/Scope {
|
public final class app/revanced/manager/plugin/downloader/webview/WebViewScope : app/revanced/manager/plugin/downloader/Scope {
|
||||||
|
public static final field $stable I
|
||||||
public final fun download (Lkotlin/jvm/functions/Function5;)V
|
public final fun download (Lkotlin/jvm/functions/Function5;)V
|
||||||
public fun getHostPackageName ()Ljava/lang/String;
|
public fun getHostPackageName ()Ljava/lang/String;
|
||||||
public fun getPluginPackageName ()Ljava/lang/String;
|
public fun getPluginPackageName ()Ljava/lang/String;
|
||||||
111
api/build.gradle.kts
Normal file
111
api/build.gradle.kts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.android.library)
|
||||||
|
alias(libs.plugins.kotlin.android)
|
||||||
|
alias(libs.plugins.kotlin.parcelize)
|
||||||
|
alias(libs.plugins.compose.compiler)
|
||||||
|
alias(libs.plugins.binary.compatibility.validator)
|
||||||
|
`maven-publish`
|
||||||
|
signing
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "app.revanced"
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(libs.androidx.ktx)
|
||||||
|
implementation(libs.runtime.ktx)
|
||||||
|
implementation(libs.activity.compose)
|
||||||
|
implementation(libs.appcompat)
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "app.revanced.manager.plugin.downloader"
|
||||||
|
compileSdk = 35
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 26
|
||||||
|
|
||||||
|
consumerProguardFiles("consumer-rules.pro")
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
isMinifyEnabled = false
|
||||||
|
proguardFiles(
|
||||||
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
|
"proguard-rules.pro"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "17"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
aidl = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apiValidation {
|
||||||
|
nonPublicMarkers += "app.revanced.manager.plugin.downloader.PluginHostApi"
|
||||||
|
}
|
||||||
|
|
||||||
|
publishing {
|
||||||
|
repositories {
|
||||||
|
maven {
|
||||||
|
name = "GitHubPackages"
|
||||||
|
url = uri("https://maven.pkg.github.com/revanced/revanced-manager")
|
||||||
|
credentials {
|
||||||
|
username = System.getenv("GITHUB_ACTOR") ?: extra["gpr.user"] as String?
|
||||||
|
password = System.getenv("GITHUB_TOKEN") ?: extra["gpr.key"] as String?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
publications {
|
||||||
|
create<MavenPublication>("Api") {
|
||||||
|
afterEvaluate {
|
||||||
|
from(components["release"])
|
||||||
|
}
|
||||||
|
|
||||||
|
groupId = "app.revanced"
|
||||||
|
artifactId = "revanced-manager-api"
|
||||||
|
version = project.version.toString()
|
||||||
|
|
||||||
|
pom {
|
||||||
|
name = "ReVanced Manager API"
|
||||||
|
description = "API for ReVanced Manager."
|
||||||
|
url = "https://revanced.app"
|
||||||
|
|
||||||
|
licenses {
|
||||||
|
license {
|
||||||
|
name = "GNU General Public License v3.0"
|
||||||
|
url = "https://www.gnu.org/licenses/gpl-3.0.en.html"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
developers {
|
||||||
|
developer {
|
||||||
|
id = "ReVanced"
|
||||||
|
name = "ReVanced"
|
||||||
|
email = "contact@revanced.app"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scm {
|
||||||
|
connection = "scm:git:git://github.com/revanced/revanced-manager.git"
|
||||||
|
developerConnection = "scm:git:git@github.com:revanced/revanced-manager.git"
|
||||||
|
url = "https://github.com/revanced/revanced-manager"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
signing {
|
||||||
|
useGpgCmd()
|
||||||
|
sign(publishing.publications["Api"])
|
||||||
|
}
|
||||||
1
api/gradlew
vendored
Symbolic link
1
api/gradlew
vendored
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../gradlew
|
||||||
11
api/package.json
Normal file
11
api/package.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "api",
|
||||||
|
"private": false,
|
||||||
|
"devDependencies": {
|
||||||
|
"@anolilab/multi-semantic-release": "^1.1.10",
|
||||||
|
"@saithodev/semantic-release-backmerge": "^4.0.1",
|
||||||
|
"@semantic-release/changelog": "^6.0.3",
|
||||||
|
"@semantic-release/git": "^10.0.1",
|
||||||
|
"gradle-semantic-release-plugin": "^1.10.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
50
app/.releaserc
Normal file
50
app/.releaserc
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"branches": [
|
||||||
|
"main",
|
||||||
|
{
|
||||||
|
"name": "dev",
|
||||||
|
"prerelease": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
[
|
||||||
|
"@semantic-release/commit-analyzer",
|
||||||
|
{
|
||||||
|
"releaseRules": [
|
||||||
|
{ "type": "build", "scope": "Needs bump", "release": "patch" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@semantic-release/release-notes-generator",
|
||||||
|
"@semantic-release/changelog",
|
||||||
|
"gradle-semantic-release-plugin",
|
||||||
|
[
|
||||||
|
"@semantic-release/git",
|
||||||
|
{
|
||||||
|
"assets": [
|
||||||
|
"CHANGELOG.md",
|
||||||
|
"gradle.properties"
|
||||||
|
],
|
||||||
|
"message": "chore: Release v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@semantic-release/github",
|
||||||
|
{
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"path": "build/outputs/apk/release/revanced-manager*.apk?(.asc)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"successComment": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@saithodev/semantic-release-backmerge",
|
||||||
|
{
|
||||||
|
"backmergeBranches": [{"from": "main", "to": "dev"}],
|
||||||
|
"clearWorkspace": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import io.github.z4kn4fein.semver.toVersion
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
@@ -8,109 +9,12 @@ plugins {
|
|||||||
alias(libs.plugins.compose.compiler)
|
alias(libs.plugins.compose.compiler)
|
||||||
alias(libs.plugins.devtools)
|
alias(libs.plugins.devtools)
|
||||||
alias(libs.plugins.about.libraries)
|
alias(libs.plugins.about.libraries)
|
||||||
|
signing
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
val outputApkFileName = "${rootProject.name}-$version.apk"
|
||||||
namespace = "app.revanced.manager"
|
|
||||||
compileSdk = 35
|
|
||||||
buildToolsVersion = "35.0.1"
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
applicationId = "app.revanced.manager"
|
|
||||||
minSdk = 26
|
|
||||||
targetSdk = 35
|
|
||||||
versionCode = 1
|
|
||||||
versionName = "0.0.1"
|
|
||||||
vectorDrawables.useSupportLibrary = true
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
|
||||||
debug {
|
|
||||||
applicationIdSuffix = ".debug"
|
|
||||||
resValue("string", "app_name", "ReVanced Manager (dev)")
|
|
||||||
isPseudoLocalesEnabled = true
|
|
||||||
|
|
||||||
buildConfigField("long", "BUILD_ID", "${Random.nextLong()}L")
|
|
||||||
}
|
|
||||||
|
|
||||||
release {
|
|
||||||
if (!project.hasProperty("noProguard")) {
|
|
||||||
isMinifyEnabled = true
|
|
||||||
isShrinkResources = true
|
|
||||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (project.hasProperty("signAsDebug")) {
|
|
||||||
applicationIdSuffix = ".debug"
|
|
||||||
resValue("string", "app_name", "ReVanced Manager Debug")
|
|
||||||
signingConfig = signingConfigs.getByName("debug")
|
|
||||||
|
|
||||||
isPseudoLocalesEnabled = true
|
|
||||||
}
|
|
||||||
|
|
||||||
buildConfigField("long", "BUILD_ID", "0L")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
|
||||||
}
|
|
||||||
|
|
||||||
dependenciesInfo {
|
|
||||||
includeInApk = false
|
|
||||||
includeInBundle = false
|
|
||||||
}
|
|
||||||
|
|
||||||
packaging {
|
|
||||||
resources.excludes.addAll(listOf(
|
|
||||||
"/prebuilt/**",
|
|
||||||
"META-INF/DEPENDENCIES",
|
|
||||||
"META-INF/**.version",
|
|
||||||
"DebugProbesKt.bin",
|
|
||||||
"kotlin-tooling-metadata.json",
|
|
||||||
"org/bouncycastle/pqc/**.properties",
|
|
||||||
"org/bouncycastle/x509/**.properties",
|
|
||||||
))
|
|
||||||
jniLibs {
|
|
||||||
useLegacyPackaging = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ksp {
|
|
||||||
arg("room.schemaLocation", "$projectDir/schemas")
|
|
||||||
}
|
|
||||||
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = "17"
|
|
||||||
}
|
|
||||||
|
|
||||||
buildFeatures {
|
|
||||||
compose = true
|
|
||||||
aidl = true
|
|
||||||
buildConfig = true
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
androidResources {
|
|
||||||
generateLocaleConfig = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
externalNativeBuild {
|
|
||||||
cmake {
|
|
||||||
path = file("src/main/cpp/CMakeLists.txt")
|
|
||||||
version = "3.22.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
kotlin {
|
|
||||||
jvmToolchain(17)
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
// AndroidX Core
|
// AndroidX Core
|
||||||
implementation(libs.androidx.ktx)
|
implementation(libs.androidx.ktx)
|
||||||
implementation(libs.runtime.ktx)
|
implementation(libs.runtime.ktx)
|
||||||
@@ -137,10 +41,6 @@ dependencies {
|
|||||||
// Placeholder
|
// Placeholder
|
||||||
implementation(libs.placeholder.material3)
|
implementation(libs.placeholder.material3)
|
||||||
|
|
||||||
// HTML Scraper
|
|
||||||
implementation(libs.skrapeit.dsl)
|
|
||||||
implementation(libs.skrapeit.parser)
|
|
||||||
|
|
||||||
// Coil (async image loading, network image)
|
// Coil (async image loading, network image)
|
||||||
implementation(libs.coil.compose)
|
implementation(libs.coil.compose)
|
||||||
implementation(libs.coil.appiconloader)
|
implementation(libs.coil.appiconloader)
|
||||||
@@ -161,7 +61,7 @@ dependencies {
|
|||||||
implementation(libs.revanced.library)
|
implementation(libs.revanced.library)
|
||||||
|
|
||||||
// Downloader plugins
|
// Downloader plugins
|
||||||
implementation(project(":downloader-plugin"))
|
implementation(project(":api"))
|
||||||
|
|
||||||
// Native processes
|
// Native processes
|
||||||
implementation(libs.kotlin.process)
|
implementation(libs.kotlin.process)
|
||||||
@@ -209,3 +109,162 @@ dependencies {
|
|||||||
// Compose Icons
|
// Compose Icons
|
||||||
implementation(libs.compose.icons.fontawesome)
|
implementation(libs.compose.icons.fontawesome)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
// Semantic versioning string parser
|
||||||
|
classpath(libs.semver.parser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "app.revanced.manager"
|
||||||
|
compileSdk = 35
|
||||||
|
buildToolsVersion = "35.0.1"
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId = "app.revanced.manager"
|
||||||
|
minSdk = 26
|
||||||
|
targetSdk = 35
|
||||||
|
|
||||||
|
val versionStr = if (version == "unspecified") "1.0.0" else version.toString()
|
||||||
|
versionName = versionStr
|
||||||
|
versionCode = with(versionStr.toVersion()) {
|
||||||
|
major * 10_000_000 +
|
||||||
|
minor * 10_000 +
|
||||||
|
patch * 100 +
|
||||||
|
(preRelease?.substringAfterLast('.')?.toInt() ?: 99)
|
||||||
|
}
|
||||||
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
debug {
|
||||||
|
applicationIdSuffix = ".debug"
|
||||||
|
resValue("string", "app_name", "ReVanced Manager (Debug)")
|
||||||
|
isPseudoLocalesEnabled = true
|
||||||
|
|
||||||
|
buildConfigField("long", "BUILD_ID", "${Random.nextLong()}L")
|
||||||
|
}
|
||||||
|
|
||||||
|
release {
|
||||||
|
if (!project.hasProperty("noProguard")) {
|
||||||
|
isMinifyEnabled = true
|
||||||
|
isShrinkResources = true
|
||||||
|
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||||
|
}
|
||||||
|
|
||||||
|
val keystoreFile = file("keystore.jks")
|
||||||
|
|
||||||
|
if (project.hasProperty("signAsDebug") || !keystoreFile.exists()) {
|
||||||
|
applicationIdSuffix = ".debug_signed"
|
||||||
|
resValue("string", "app_name", "ReVanced Manager (Debug signed)")
|
||||||
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
|
|
||||||
|
isPseudoLocalesEnabled = true
|
||||||
|
} else {
|
||||||
|
signingConfig = signingConfigs.create("release") {
|
||||||
|
storeFile = keystoreFile
|
||||||
|
storePassword = System.getenv("KEYSTORE_PASSWORD")
|
||||||
|
keyAlias = System.getenv("KEYSTORE_ENTRY_ALIAS")
|
||||||
|
keyPassword = System.getenv("KEYSTORE_ENTRY_PASSWORD")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildConfigField("long", "BUILD_ID", "0L")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applicationVariants.all {
|
||||||
|
outputs.all {
|
||||||
|
this as com.android.build.gradle.internal.api.ApkVariantOutputImpl
|
||||||
|
|
||||||
|
outputFileName = outputApkFileName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
dependenciesInfo {
|
||||||
|
includeInApk = false
|
||||||
|
includeInBundle = false
|
||||||
|
}
|
||||||
|
|
||||||
|
packaging {
|
||||||
|
resources.excludes.addAll(
|
||||||
|
listOf(
|
||||||
|
"/prebuilt/**",
|
||||||
|
"META-INF/DEPENDENCIES",
|
||||||
|
"META-INF/**.version",
|
||||||
|
"DebugProbesKt.bin",
|
||||||
|
"kotlin-tooling-metadata.json",
|
||||||
|
"org/bouncycastle/pqc/**.properties",
|
||||||
|
"org/bouncycastle/x509/**.properties",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
jniLibs {
|
||||||
|
useLegacyPackaging = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ksp {
|
||||||
|
arg("room.schemaLocation", "$projectDir/schemas")
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "17"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
compose = true
|
||||||
|
aidl = true
|
||||||
|
buildConfig = true
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
androidResources {
|
||||||
|
generateLocaleConfig = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
externalNativeBuild {
|
||||||
|
cmake {
|
||||||
|
path = file("src/main/cpp/CMakeLists.txt")
|
||||||
|
version = "3.22.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(17)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks {
|
||||||
|
// Needed by gradle-semantic-release-plugin.
|
||||||
|
// Tracking: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435.
|
||||||
|
val publish by registering {
|
||||||
|
group = "publishing"
|
||||||
|
description = "Build the release APK"
|
||||||
|
|
||||||
|
dependsOn("assembleRelease")
|
||||||
|
|
||||||
|
val apk = project.layout.buildDirectory.file("outputs/apk/release/${outputApkFileName}")
|
||||||
|
val ascFile = apk.map { it.asFile.resolveSibling("${it.asFile.name}.asc") }
|
||||||
|
|
||||||
|
inputs.file(apk).withPropertyName("inputApk")
|
||||||
|
outputs.file(ascFile).withPropertyName("outputAsc")
|
||||||
|
|
||||||
|
doLast {
|
||||||
|
signing {
|
||||||
|
useGpgCmd()
|
||||||
|
sign(apk.get().asFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
1
app/gradlew
vendored
Symbolic link
1
app/gradlew
vendored
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../gradlew
|
||||||
11
app/package.json
Normal file
11
app/package.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "app",
|
||||||
|
"private": false,
|
||||||
|
"devDependencies": {
|
||||||
|
"@anolilab/multi-semantic-release": "^1.1.10",
|
||||||
|
"@saithodev/semantic-release-backmerge": "^4.0.1",
|
||||||
|
"@semantic-release/changelog": "^6.0.3",
|
||||||
|
"@semantic-release/git": "^10.0.1",
|
||||||
|
"gradle-semantic-release-plugin": "^1.10.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
67
app/proguard-rules.pro
vendored
67
app/proguard-rules.pro
vendored
@@ -1,63 +1,14 @@
|
|||||||
# Add project specific ProGuard rules here.
|
|
||||||
# You can control the set of applied configuration files using the
|
|
||||||
# proguardFiles setting in build.gradle.kts.kts.
|
|
||||||
#
|
|
||||||
# For more details, see
|
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
|
||||||
|
|
||||||
-dontobfuscate
|
-dontobfuscate
|
||||||
|
|
||||||
# Required for serialization to work properly
|
-keep class app.revanced.manager.patcher.runtime.process.* { *; }
|
||||||
-if @kotlinx.serialization.Serializable class **
|
-keep class app.revanced.manager.plugin.** { *; }
|
||||||
-keepclassmembers class <1> {
|
-keep class app.revanced.patcher.** { *; }
|
||||||
static <1>$Companion Companion;
|
-keep class com.android.tools.smali.** { *; }
|
||||||
}
|
-keep class kotlin.** { *; }
|
||||||
-if @kotlinx.serialization.Serializable class ** {
|
-keepnames class com.android.apksig.internal.** { *; }
|
||||||
static **$* *;
|
-keepnames class org.xmlpull.** { *; }
|
||||||
}
|
|
||||||
-keepclassmembers class <2>$<3> {
|
|
||||||
kotlinx.serialization.KSerializer serializer(...);
|
|
||||||
}
|
|
||||||
-if @kotlinx.serialization.Serializable class ** {
|
|
||||||
public static ** INSTANCE;
|
|
||||||
}
|
|
||||||
-keepclassmembers class <1> {
|
|
||||||
public static <1> INSTANCE;
|
|
||||||
kotlinx.serialization.KSerializer serializer(...);
|
|
||||||
}
|
|
||||||
|
|
||||||
# This required for the process runtime.
|
-dontwarn com.google.j2objc.annotations.*
|
||||||
-keep class app.revanced.manager.patcher.runtime.process.* {
|
|
||||||
*;
|
|
||||||
}
|
|
||||||
# Required for the patcher to function correctly
|
|
||||||
-keep class app.revanced.patcher.** {
|
|
||||||
*;
|
|
||||||
}
|
|
||||||
-keep class brut.** {
|
|
||||||
*;
|
|
||||||
}
|
|
||||||
-keep class org.xmlpull.** {
|
|
||||||
*;
|
|
||||||
}
|
|
||||||
-keep class kotlin.** {
|
|
||||||
*;
|
|
||||||
}
|
|
||||||
-keep class org.jf.** {
|
|
||||||
*;
|
|
||||||
}
|
|
||||||
-keep class com.android.** {
|
|
||||||
*;
|
|
||||||
}
|
|
||||||
-keep class app.revanced.manager.plugin.** {
|
|
||||||
*;
|
|
||||||
}
|
|
||||||
|
|
||||||
-dontwarn com.google.auto.value.**
|
|
||||||
-dontwarn java.awt.**
|
-dontwarn java.awt.**
|
||||||
-dontwarn javax.**
|
-dontwarn javax.**
|
||||||
-dontwarn org.slf4j.**
|
-dontwarn org.slf4j.**
|
||||||
-dontwarn it.skrape.fetcher.*
|
|
||||||
-dontwarn com.google.j2objc.annotations.*
|
|
||||||
|
|
||||||
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
|
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||||
tools:ignore="ScopedStorage" />
|
tools:ignore="ScopedStorage" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
<uses-permission android:name="android.permission.ENFORCE_UPDATE_OWNERSHIP" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".ManagerApplication"
|
android:name=".ManagerApplication"
|
||||||
|
|||||||
@@ -44,13 +44,14 @@ import app.revanced.manager.ui.screen.SettingsScreen
|
|||||||
import app.revanced.manager.ui.screen.UpdateScreen
|
import app.revanced.manager.ui.screen.UpdateScreen
|
||||||
import app.revanced.manager.ui.screen.settings.AboutSettingsScreen
|
import app.revanced.manager.ui.screen.settings.AboutSettingsScreen
|
||||||
import app.revanced.manager.ui.screen.settings.AdvancedSettingsScreen
|
import app.revanced.manager.ui.screen.settings.AdvancedSettingsScreen
|
||||||
import app.revanced.manager.ui.screen.settings.BackupRestoreSettingsScreen
|
import app.revanced.manager.ui.screen.settings.ContributorSettingsScreen
|
||||||
import app.revanced.manager.ui.screen.settings.ContributorScreen
|
import app.revanced.manager.ui.screen.settings.DeveloperSettingsScreen
|
||||||
import app.revanced.manager.ui.screen.settings.DeveloperOptionsScreen
|
|
||||||
import app.revanced.manager.ui.screen.settings.DownloadsSettingsScreen
|
import app.revanced.manager.ui.screen.settings.DownloadsSettingsScreen
|
||||||
import app.revanced.manager.ui.screen.settings.GeneralSettingsScreen
|
import app.revanced.manager.ui.screen.settings.GeneralSettingsScreen
|
||||||
import app.revanced.manager.ui.screen.settings.LicensesScreen
|
import app.revanced.manager.ui.screen.settings.ImportExportSettingsScreen
|
||||||
import app.revanced.manager.ui.screen.settings.update.ChangelogsScreen
|
import app.revanced.manager.ui.screen.settings.LicensesSettingsScreen
|
||||||
|
import app.revanced.manager.ui.screen.settings.update.ChangelogsSettingsScreen
|
||||||
|
import app.revanced.manager.ui.screen.settings.update.UpdatesSettingsScreen
|
||||||
import app.revanced.manager.ui.theme.ReVancedManagerTheme
|
import app.revanced.manager.ui.theme.ReVancedManagerTheme
|
||||||
import app.revanced.manager.ui.theme.Theme
|
import app.revanced.manager.ui.theme.Theme
|
||||||
import app.revanced.manager.ui.viewmodel.MainViewModel
|
import app.revanced.manager.ui.viewmodel.MainViewModel
|
||||||
@@ -163,7 +164,7 @@ private fun ReVancedManager(vm: MainViewModel) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
vm = koinViewModel { parametersOf(it.getComplexArg<Patcher.ViewModelParams>()) }
|
viewModel = koinViewModel { parametersOf(it.getComplexArg<Patcher.ViewModelParams>()) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,7 +234,7 @@ private fun ReVancedManager(vm: MainViewModel) {
|
|||||||
selectedAppInfoVm.updateConfiguration(patches, options)
|
selectedAppInfoVm.updateConfiguration(patches, options)
|
||||||
navController.popBackStack()
|
navController.popBackStack()
|
||||||
},
|
},
|
||||||
vm = koinViewModel { parametersOf(data) }
|
viewModel = koinViewModel { parametersOf(data) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,47 +270,52 @@ private fun ReVancedManager(vm: MainViewModel) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
composable<Settings.General> {
|
composable<Settings.General> {
|
||||||
GeneralSettingsScreen(
|
GeneralSettingsScreen(onBackClick = navController::popBackStack)
|
||||||
onBackClick = navController::popBackStack,
|
|
||||||
onUpdateClick = { navController.navigate(Update()) }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
composable<Settings.Advanced> {
|
composable<Settings.Advanced> {
|
||||||
AdvancedSettingsScreen(onBackClick = navController::popBackStack)
|
AdvancedSettingsScreen(onBackClick = navController::popBackStack)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
composable<Settings.Developer> {
|
||||||
|
DeveloperSettingsScreen(onBackClick = navController::popBackStack)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<Settings.Updates> {
|
||||||
|
UpdatesSettingsScreen(
|
||||||
|
onBackClick = navController::popBackStack,
|
||||||
|
onChangelogClick = { navController.navigate(Settings.Changelogs) },
|
||||||
|
onUpdateClick = { navController.navigate(Update()) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
composable<Settings.Downloads> {
|
composable<Settings.Downloads> {
|
||||||
DownloadsSettingsScreen(onBackClick = navController::popBackStack)
|
DownloadsSettingsScreen(onBackClick = navController::popBackStack)
|
||||||
}
|
}
|
||||||
|
|
||||||
composable<Settings.ImportExport> {
|
composable<Settings.ImportExport> {
|
||||||
BackupRestoreSettingsScreen(onBackClick = navController::popBackStack)
|
ImportExportSettingsScreen(onBackClick = navController::popBackStack)
|
||||||
}
|
}
|
||||||
|
|
||||||
composable<Settings.About> {
|
composable<Settings.About> {
|
||||||
AboutSettingsScreen(
|
AboutSettingsScreen(
|
||||||
onBackClick = navController::popBackStack,
|
onBackClick = navController::popBackStack,
|
||||||
onChangelogClick = { navController.navigate(Settings.Changelogs) },
|
|
||||||
navigate = navController::navigate
|
navigate = navController::navigate
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
composable<Settings.Changelogs> {
|
composable<Settings.Changelogs> {
|
||||||
ChangelogsScreen(onBackClick = navController::popBackStack)
|
ChangelogsSettingsScreen(onBackClick = navController::popBackStack)
|
||||||
}
|
}
|
||||||
|
|
||||||
composable<Settings.Contributors> {
|
composable<Settings.Contributors> {
|
||||||
ContributorScreen(onBackClick = navController::popBackStack)
|
ContributorSettingsScreen(onBackClick = navController::popBackStack)
|
||||||
}
|
}
|
||||||
|
|
||||||
composable<Settings.Licenses> {
|
composable<Settings.Licenses> {
|
||||||
LicensesScreen(onBackClick = navController::popBackStack)
|
LicensesSettingsScreen(onBackClick = navController::popBackStack)
|
||||||
}
|
}
|
||||||
|
|
||||||
composable<Settings.DeveloperOptions> {
|
|
||||||
DeveloperOptionsScreen(onBackClick = navController::popBackStack)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
74
app/src/main/java/app/revanced/manager/data/redux/Redux.kt
Normal file
74
app/src/main/java/app/revanced/manager/data/redux/Redux.kt
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
package app.revanced.manager.data.redux
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import app.revanced.manager.util.tag
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
|
|
||||||
|
// This file implements React Redux-like state management.
|
||||||
|
|
||||||
|
class Store<S>(private val coroutineScope: CoroutineScope, initialState: S) : ActionContext {
|
||||||
|
private val _state = MutableStateFlow(initialState)
|
||||||
|
val state = _state.asStateFlow()
|
||||||
|
|
||||||
|
// Do not touch these without the lock.
|
||||||
|
private var isRunningActions = false
|
||||||
|
private val queueChannel = Channel<Action<S>>(capacity = 10)
|
||||||
|
private val lock = Mutex()
|
||||||
|
|
||||||
|
suspend fun dispatch(action: Action<S>) = lock.withLock {
|
||||||
|
Log.d(tag, "Dispatching $action")
|
||||||
|
queueChannel.send(action)
|
||||||
|
|
||||||
|
if (isRunningActions) return@withLock
|
||||||
|
isRunningActions = true
|
||||||
|
coroutineScope.launch {
|
||||||
|
runActions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
private suspend fun runActions() {
|
||||||
|
while (true) {
|
||||||
|
val action = withTimeoutOrNull(200L) { queueChannel.receive() }
|
||||||
|
if (action == null) {
|
||||||
|
Log.d(tag, "Stopping action runner")
|
||||||
|
lock.withLock {
|
||||||
|
// New actions may be dispatched during the timeout.
|
||||||
|
isRunningActions = !queueChannel.isEmpty
|
||||||
|
if (!isRunningActions) return
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(tag, "Running $action")
|
||||||
|
_state.value = try {
|
||||||
|
with(action) { this@Store.execute(_state.value) }
|
||||||
|
} catch (c: CancellationException) {
|
||||||
|
// This is done without the lock, but cancellation usually means the store is no longer needed.
|
||||||
|
isRunningActions = false
|
||||||
|
throw c
|
||||||
|
} catch (e: Exception) {
|
||||||
|
action.catch(e)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ActionContext
|
||||||
|
|
||||||
|
interface Action<S> {
|
||||||
|
suspend fun ActionContext.execute(current: S): S
|
||||||
|
suspend fun catch(exception: Exception) {
|
||||||
|
Log.e(tag, "Got exception while executing $this", exception)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +1,14 @@
|
|||||||
package app.revanced.manager.data.room.bundles
|
package app.revanced.manager.data.room.bundles
|
||||||
|
|
||||||
import androidx.room.*
|
import androidx.room.*
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface PatchBundleDao {
|
interface PatchBundleDao {
|
||||||
@Query("SELECT * FROM patch_bundles")
|
@Query("SELECT * FROM patch_bundles")
|
||||||
suspend fun all(): List<PatchBundleEntity>
|
suspend fun all(): List<PatchBundleEntity>
|
||||||
|
|
||||||
@Query("SELECT version, auto_update FROM patch_bundles WHERE uid = :uid")
|
|
||||||
fun getPropsById(uid: Int): Flow<BundleProperties?>
|
|
||||||
|
|
||||||
@Query("UPDATE patch_bundles SET version = :patches WHERE uid = :uid")
|
@Query("UPDATE patch_bundles SET version = :patches WHERE uid = :uid")
|
||||||
suspend fun updateVersion(uid: Int, patches: String?)
|
suspend fun updateVersionHash(uid: Int, patches: String?)
|
||||||
|
|
||||||
@Query("UPDATE patch_bundles SET auto_update = :value WHERE uid = :uid")
|
|
||||||
suspend fun setAutoUpdate(uid: Int, value: Boolean)
|
|
||||||
|
|
||||||
@Query("UPDATE patch_bundles SET name = :value WHERE uid = :uid")
|
|
||||||
suspend fun setName(uid: Int, value: String)
|
|
||||||
|
|
||||||
@Query("DELETE FROM patch_bundles WHERE uid != 0")
|
@Query("DELETE FROM patch_bundles WHERE uid != 0")
|
||||||
suspend fun purgeCustomBundles()
|
suspend fun purgeCustomBundles()
|
||||||
@@ -26,12 +16,15 @@ interface PatchBundleDao {
|
|||||||
@Transaction
|
@Transaction
|
||||||
suspend fun reset() {
|
suspend fun reset() {
|
||||||
purgeCustomBundles()
|
purgeCustomBundles()
|
||||||
updateVersion(0, null) // Reset the main source
|
updateVersionHash(0, null) // Reset the main source
|
||||||
}
|
}
|
||||||
|
|
||||||
@Query("DELETE FROM patch_bundles WHERE uid = :uid")
|
@Query("DELETE FROM patch_bundles WHERE uid = :uid")
|
||||||
suspend fun remove(uid: Int)
|
suspend fun remove(uid: Int)
|
||||||
|
|
||||||
@Insert
|
@Query("SELECT name, version, auto_update, source FROM patch_bundles WHERE uid = :uid")
|
||||||
suspend fun add(source: PatchBundleEntity)
|
suspend fun getProps(uid: Int): PatchBundleProperties?
|
||||||
|
|
||||||
|
@Upsert
|
||||||
|
suspend fun upsert(source: PatchBundleEntity)
|
||||||
}
|
}
|
||||||
@@ -33,12 +33,14 @@ sealed class Source {
|
|||||||
data class PatchBundleEntity(
|
data class PatchBundleEntity(
|
||||||
@PrimaryKey val uid: Int,
|
@PrimaryKey val uid: Int,
|
||||||
@ColumnInfo(name = "name") val name: String,
|
@ColumnInfo(name = "name") val name: String,
|
||||||
@ColumnInfo(name = "version") val version: String? = null,
|
@ColumnInfo(name = "version") val versionHash: String? = null,
|
||||||
@ColumnInfo(name = "source") val source: Source,
|
@ColumnInfo(name = "source") val source: Source,
|
||||||
@ColumnInfo(name = "auto_update") val autoUpdate: Boolean
|
@ColumnInfo(name = "auto_update") val autoUpdate: Boolean
|
||||||
)
|
)
|
||||||
|
|
||||||
data class BundleProperties(
|
data class PatchBundleProperties(
|
||||||
@ColumnInfo(name = "version") val version: String? = null,
|
@ColumnInfo(name = "name") val name: String,
|
||||||
|
@ColumnInfo(name = "version") val versionHash: String? = null,
|
||||||
|
@ColumnInfo(name = "source") val source: Source,
|
||||||
@ColumnInfo(name = "auto_update") val autoUpdate: Boolean
|
@ColumnInfo(name = "auto_update") val autoUpdate: Boolean
|
||||||
)
|
)
|
||||||
@@ -27,10 +27,10 @@ abstract class OptionDao {
|
|||||||
abstract suspend fun createOptionGroup(group: OptionGroup)
|
abstract suspend fun createOptionGroup(group: OptionGroup)
|
||||||
|
|
||||||
@Query("DELETE FROM option_groups WHERE patch_bundle = :uid")
|
@Query("DELETE FROM option_groups WHERE patch_bundle = :uid")
|
||||||
abstract suspend fun clearForPatchBundle(uid: Int)
|
abstract suspend fun resetOptionsForPatchBundle(uid: Int)
|
||||||
|
|
||||||
@Query("DELETE FROM option_groups WHERE package_name = :packageName")
|
@Query("DELETE FROM option_groups WHERE package_name = :packageName")
|
||||||
abstract suspend fun clearForPackage(packageName: String)
|
abstract suspend fun resetOptionsForPackage(packageName: String)
|
||||||
|
|
||||||
@Query("DELETE FROM option_groups")
|
@Query("DELETE FROM option_groups")
|
||||||
abstract suspend fun reset()
|
abstract suspend fun reset()
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import androidx.room.Insert
|
|||||||
import androidx.room.MapColumn
|
import androidx.room.MapColumn
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import androidx.room.Transaction
|
import androidx.room.Transaction
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
abstract class SelectionDao {
|
abstract class SelectionDao {
|
||||||
@@ -34,11 +35,14 @@ abstract class SelectionDao {
|
|||||||
@Insert
|
@Insert
|
||||||
abstract suspend fun createSelection(selection: PatchSelection)
|
abstract suspend fun createSelection(selection: PatchSelection)
|
||||||
|
|
||||||
|
@Query("SELECT package_name FROM patch_selections")
|
||||||
|
abstract fun getPackagesWithSelection(): Flow<List<String>>
|
||||||
|
|
||||||
@Query("DELETE FROM patch_selections WHERE patch_bundle = :uid")
|
@Query("DELETE FROM patch_selections WHERE patch_bundle = :uid")
|
||||||
abstract suspend fun clearForPatchBundle(uid: Int)
|
abstract suspend fun resetForPatchBundle(uid: Int)
|
||||||
|
|
||||||
@Query("DELETE FROM patch_selections WHERE package_name = :packageName")
|
@Query("DELETE FROM patch_selections WHERE package_name = :packageName")
|
||||||
abstract suspend fun clearForPackage(packageName: String)
|
abstract suspend fun resetForPackage(packageName: String)
|
||||||
|
|
||||||
@Query("DELETE FROM patch_selections")
|
@Query("DELETE FROM patch_selections")
|
||||||
abstract suspend fun reset()
|
abstract suspend fun reset()
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ val repositoryModule = module {
|
|||||||
createdAtStart()
|
createdAtStart()
|
||||||
}
|
}
|
||||||
singleOf(::NetworkInfo)
|
singleOf(::NetworkInfo)
|
||||||
singleOf(::PatchBundlePersistenceRepository)
|
|
||||||
singleOf(::PatchSelectionRepository)
|
singleOf(::PatchSelectionRepository)
|
||||||
singleOf(::PatchOptionsRepository)
|
singleOf(::PatchOptionsRepository)
|
||||||
singleOf(::PatchBundleRepository) {
|
singleOf(::PatchBundleRepository) {
|
||||||
|
|||||||
@@ -23,4 +23,5 @@ val viewModelModule = module {
|
|||||||
viewModelOf(::InstalledAppsViewModel)
|
viewModelOf(::InstalledAppsViewModel)
|
||||||
viewModelOf(::InstalledAppInfoViewModel)
|
viewModelOf(::InstalledAppInfoViewModel)
|
||||||
viewModelOf(::UpdatesSettingsViewModel)
|
viewModelOf(::UpdatesSettingsViewModel)
|
||||||
|
viewModelOf(::BundleListViewModel)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,29 @@
|
|||||||
package app.revanced.manager.domain.bundles
|
package app.revanced.manager.domain.bundles
|
||||||
|
|
||||||
|
import app.revanced.manager.data.redux.ActionContext
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
class LocalPatchBundle(name: String, id: Int, directory: File) :
|
class LocalPatchBundle(
|
||||||
PatchBundleSource(name, id, directory) {
|
name: String,
|
||||||
suspend fun replace(patches: InputStream) {
|
uid: Int,
|
||||||
|
error: Throwable?,
|
||||||
|
directory: File
|
||||||
|
) : PatchBundleSource(name, uid, error, directory) {
|
||||||
|
suspend fun ActionContext.replace(patches: InputStream) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
patchBundleOutputStream().use { outputStream ->
|
patchBundleOutputStream().use { outputStream ->
|
||||||
patches.copyTo(outputStream)
|
patches.copyTo(outputStream)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reload()?.also {
|
|
||||||
saveVersion(it.readManifestAttribute("Version"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun copy(error: Throwable?, name: String) = LocalPatchBundle(
|
||||||
|
name,
|
||||||
|
uid,
|
||||||
|
error,
|
||||||
|
directory
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,10 @@
|
|||||||
package app.revanced.manager.domain.bundles
|
package app.revanced.manager.domain.bundles
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import app.revanced.manager.data.redux.ActionContext
|
||||||
import app.revanced.manager.R
|
|
||||||
import app.revanced.manager.domain.repository.PatchBundlePersistenceRepository
|
|
||||||
import app.revanced.manager.patcher.patch.PatchBundle
|
import app.revanced.manager.patcher.patch.PatchBundle
|
||||||
import app.revanced.manager.util.tag
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.flow.flowOn
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import org.koin.core.component.KoinComponent
|
|
||||||
import org.koin.core.component.inject
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
|
||||||
@@ -24,24 +12,32 @@ import java.io.OutputStream
|
|||||||
* A [PatchBundle] source.
|
* A [PatchBundle] source.
|
||||||
*/
|
*/
|
||||||
@Stable
|
@Stable
|
||||||
sealed class PatchBundleSource(initialName: String, val uid: Int, directory: File) : KoinComponent {
|
sealed class PatchBundleSource(
|
||||||
protected val configRepository: PatchBundlePersistenceRepository by inject()
|
val name: String,
|
||||||
private val app: Application by inject()
|
val uid: Int,
|
||||||
|
error: Throwable?,
|
||||||
|
protected val directory: File
|
||||||
|
) {
|
||||||
protected val patchesFile = directory.resolve("patches.jar")
|
protected val patchesFile = directory.resolve("patches.jar")
|
||||||
|
|
||||||
private val _state = MutableStateFlow(load())
|
val state = when {
|
||||||
val state = _state.asStateFlow()
|
error != null -> State.Failed(error)
|
||||||
|
!hasInstalled() -> State.Missing
|
||||||
|
else -> State.Available(PatchBundle(patchesFile.absolutePath))
|
||||||
|
}
|
||||||
|
|
||||||
private val _nameFlow = MutableStateFlow(initialName)
|
val patchBundle get() = (state as? State.Available)?.bundle
|
||||||
val nameFlow =
|
val version get() = patchBundle?.manifestAttributes?.version
|
||||||
_nameFlow.map { it.ifEmpty { app.getString(if (isDefault) R.string.bundle_name_default else R.string.bundle_name_fallback) } }
|
val isNameOutOfDate get() = patchBundle?.manifestAttributes?.name?.let { it != name } == true
|
||||||
|
val error get() = (state as? State.Failed)?.throwable
|
||||||
|
|
||||||
suspend fun getName() = nameFlow.first()
|
suspend fun ActionContext.deleteLocalFile() = withContext(Dispatchers.IO) {
|
||||||
|
patchesFile.delete()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
abstract fun copy(error: Throwable? = this.error, name: String = this.name): PatchBundleSource
|
||||||
* Returns true if the bundle has been downloaded to local storage.
|
|
||||||
*/
|
protected fun hasInstalled() = patchesFile.exists()
|
||||||
fun hasInstalled() = patchesFile.exists()
|
|
||||||
|
|
||||||
protected fun patchBundleOutputStream(): OutputStream = with(patchesFile) {
|
protected fun patchBundleOutputStream(): OutputStream = with(patchesFile) {
|
||||||
// Android 14+ requires dex containers to be readonly.
|
// Android 14+ requires dex containers to be readonly.
|
||||||
@@ -53,62 +49,14 @@ sealed class PatchBundleSource(initialName: String, val uid: Int, directory: Fil
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun load(): State {
|
|
||||||
if (!hasInstalled()) return State.Missing
|
|
||||||
|
|
||||||
return try {
|
|
||||||
State.Loaded(PatchBundle(patchesFile))
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
Log.e(tag, "Failed to load patch bundle with UID $uid", t)
|
|
||||||
State.Failed(t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun reload(): PatchBundle? {
|
|
||||||
val newState = load()
|
|
||||||
_state.value = newState
|
|
||||||
|
|
||||||
val bundle = newState.patchBundleOrNull()
|
|
||||||
// Try to read the name from the patch bundle manifest if the bundle does not have a name.
|
|
||||||
if (bundle != null && _nameFlow.value.isEmpty()) {
|
|
||||||
bundle.readManifestAttribute("Name")?.let { setName(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
return bundle
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a flow that emits the [app.revanced.manager.data.room.bundles.BundleProperties] of this [PatchBundleSource].
|
|
||||||
* The flow will emit null if the associated [PatchBundleSource] is deleted.
|
|
||||||
*/
|
|
||||||
fun propsFlow() = configRepository.getProps(uid).flowOn(Dispatchers.Default)
|
|
||||||
suspend fun getProps() = propsFlow().first()!!
|
|
||||||
|
|
||||||
suspend fun currentVersion() = getProps().version
|
|
||||||
protected suspend fun saveVersion(version: String?) =
|
|
||||||
configRepository.updateVersion(uid, version)
|
|
||||||
|
|
||||||
suspend fun setName(name: String) {
|
|
||||||
configRepository.setName(uid, name)
|
|
||||||
_nameFlow.value = name
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed interface State {
|
sealed interface State {
|
||||||
fun patchBundleOrNull(): PatchBundle? = null
|
|
||||||
|
|
||||||
data object Missing : State
|
data object Missing : State
|
||||||
data class Failed(val throwable: Throwable) : State
|
data class Failed(val throwable: Throwable) : State
|
||||||
data class Loaded(val bundle: PatchBundle) : State {
|
data class Available(val bundle: PatchBundle) : State
|
||||||
override fun patchBundleOrNull() = bundle
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object Extensions {
|
companion object Extensions {
|
||||||
val PatchBundleSource.isDefault inline get() = uid == 0
|
val PatchBundleSource.isDefault inline get() = uid == 0
|
||||||
val PatchBundleSource.asRemoteOrNull inline get() = this as? RemotePatchBundle
|
val PatchBundleSource.asRemoteOrNull inline get() = this as? RemotePatchBundle
|
||||||
val PatchBundleSource.nameState
|
|
||||||
@Composable inline get() = nameFlow.collectAsStateWithLifecycle(
|
|
||||||
""
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package app.revanced.manager.domain.bundles
|
package app.revanced.manager.domain.bundles
|
||||||
|
|
||||||
import androidx.compose.runtime.Stable
|
import app.revanced.manager.data.redux.ActionContext
|
||||||
import app.revanced.manager.network.api.ReVancedAPI
|
import app.revanced.manager.network.api.ReVancedAPI
|
||||||
import app.revanced.manager.network.dto.ReVancedAsset
|
import app.revanced.manager.network.dto.ReVancedAsset
|
||||||
import app.revanced.manager.network.service.HttpService
|
import app.revanced.manager.network.service.HttpService
|
||||||
@@ -8,15 +8,24 @@ import app.revanced.manager.network.utils.getOrThrow
|
|||||||
import io.ktor.client.request.url
|
import io.ktor.client.request.url
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@Stable
|
sealed class RemotePatchBundle(
|
||||||
sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpoint: String) :
|
name: String,
|
||||||
PatchBundleSource(name, id, directory) {
|
uid: Int,
|
||||||
|
protected val versionHash: String?,
|
||||||
|
error: Throwable?,
|
||||||
|
directory: File,
|
||||||
|
val endpoint: String,
|
||||||
|
val autoUpdate: Boolean,
|
||||||
|
) : PatchBundleSource(name, uid, error, directory), KoinComponent {
|
||||||
protected val http: HttpService by inject()
|
protected val http: HttpService by inject()
|
||||||
|
|
||||||
protected abstract suspend fun getLatestInfo(): ReVancedAsset
|
protected abstract suspend fun getLatestInfo(): ReVancedAsset
|
||||||
|
abstract fun copy(error: Throwable? = this.error, name: String = this.name, autoUpdate: Boolean = this.autoUpdate): RemotePatchBundle
|
||||||
|
override fun copy(error: Throwable?, name: String): RemotePatchBundle = copy(error, name, this.autoUpdate)
|
||||||
|
|
||||||
private suspend fun download(info: ReVancedAsset) = withContext(Dispatchers.IO) {
|
private suspend fun download(info: ReVancedAsset) = withContext(Dispatchers.IO) {
|
||||||
patchBundleOutputStream().use {
|
patchBundleOutputStream().use {
|
||||||
@@ -25,47 +34,72 @@ sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
saveVersion(info.version)
|
info.version
|
||||||
reload()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun downloadLatest() {
|
/**
|
||||||
download(getLatestInfo())
|
* Downloads the latest version regardless if there is a new update available.
|
||||||
}
|
*/
|
||||||
|
suspend fun ActionContext.downloadLatest() = download(getLatestInfo())
|
||||||
|
|
||||||
suspend fun update(): Boolean = withContext(Dispatchers.IO) {
|
suspend fun ActionContext.update(): String? = withContext(Dispatchers.IO) {
|
||||||
val info = getLatestInfo()
|
val info = getLatestInfo()
|
||||||
if (hasInstalled() && info.version == currentVersion())
|
if (hasInstalled() && info.version == versionHash)
|
||||||
return@withContext false
|
return@withContext null
|
||||||
|
|
||||||
download(info)
|
download(info)
|
||||||
true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun deleteLocalFiles() = withContext(Dispatchers.Default) {
|
|
||||||
patchesFile.delete()
|
|
||||||
reload()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun setAutoUpdate(value: Boolean) = configRepository.setAutoUpdate(uid, value)
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val updateFailMsg = "Failed to update patch bundle(s)"
|
const val updateFailMsg = "Failed to update patches"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class JsonPatchBundle(name: String, id: Int, directory: File, endpoint: String) :
|
class JsonPatchBundle(
|
||||||
RemotePatchBundle(name, id, directory, endpoint) {
|
name: String,
|
||||||
|
uid: Int,
|
||||||
|
versionHash: String?,
|
||||||
|
error: Throwable?,
|
||||||
|
directory: File,
|
||||||
|
endpoint: String,
|
||||||
|
autoUpdate: Boolean,
|
||||||
|
) : RemotePatchBundle(name, uid, versionHash, error, directory, endpoint, autoUpdate) {
|
||||||
override suspend fun getLatestInfo() = withContext(Dispatchers.IO) {
|
override suspend fun getLatestInfo() = withContext(Dispatchers.IO) {
|
||||||
http.request<ReVancedAsset> {
|
http.request<ReVancedAsset> {
|
||||||
url(endpoint)
|
url(endpoint)
|
||||||
}.getOrThrow()
|
}.getOrThrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun copy(error: Throwable?, name: String, autoUpdate: Boolean) = JsonPatchBundle(
|
||||||
|
name,
|
||||||
|
uid,
|
||||||
|
versionHash,
|
||||||
|
error,
|
||||||
|
directory,
|
||||||
|
endpoint,
|
||||||
|
autoUpdate,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
class APIPatchBundle(name: String, id: Int, directory: File, endpoint: String) :
|
class APIPatchBundle(
|
||||||
RemotePatchBundle(name, id, directory, endpoint) {
|
name: String,
|
||||||
|
uid: Int,
|
||||||
|
versionHash: String?,
|
||||||
|
error: Throwable?,
|
||||||
|
directory: File,
|
||||||
|
endpoint: String,
|
||||||
|
autoUpdate: Boolean,
|
||||||
|
) : RemotePatchBundle(name, uid, versionHash, error, directory, endpoint, autoUpdate) {
|
||||||
private val api: ReVancedAPI by inject()
|
private val api: ReVancedAPI by inject()
|
||||||
|
|
||||||
override suspend fun getLatestInfo() = api.getPatchesUpdate().getOrThrow()
|
override suspend fun getLatestInfo() = api.getPatchesUpdate().getOrThrow()
|
||||||
|
override fun copy(error: Throwable?, name: String, autoUpdate: Boolean) = APIPatchBundle(
|
||||||
|
name,
|
||||||
|
uid,
|
||||||
|
versionHash,
|
||||||
|
error,
|
||||||
|
directory,
|
||||||
|
endpoint,
|
||||||
|
autoUpdate,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ package app.revanced.manager.domain.manager
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import app.revanced.manager.domain.manager.base.BasePreferencesManager
|
import app.revanced.manager.domain.manager.base.BasePreferencesManager
|
||||||
import app.revanced.manager.ui.theme.Theme
|
import app.revanced.manager.ui.theme.Theme
|
||||||
|
import app.revanced.manager.util.isDebuggable
|
||||||
|
|
||||||
class PreferencesManager(
|
class PreferencesManager(
|
||||||
context: Context
|
context: Context
|
||||||
@@ -24,8 +25,10 @@ class PreferencesManager(
|
|||||||
|
|
||||||
val disablePatchVersionCompatCheck = booleanPreference("disable_patch_version_compatibility_check", false)
|
val disablePatchVersionCompatCheck = booleanPreference("disable_patch_version_compatibility_check", false)
|
||||||
val disableSelectionWarning = booleanPreference("disable_selection_warning", false)
|
val disableSelectionWarning = booleanPreference("disable_selection_warning", false)
|
||||||
val disableUniversalPatchWarning = booleanPreference("disable_universal_patch_warning", false)
|
val disableUniversalPatchCheck = booleanPreference("disable_patch_universal_check", false)
|
||||||
val suggestedVersionSafeguard = booleanPreference("suggested_version_safeguard", true)
|
val suggestedVersionSafeguard = booleanPreference("suggested_version_safeguard", true)
|
||||||
|
|
||||||
val acknowledgedDownloaderPlugins = stringSetPreference("acknowledged_downloader_plugins", emptySet())
|
val acknowledgedDownloaderPlugins = stringSetPreference("acknowledged_downloader_plugins", emptySet())
|
||||||
|
|
||||||
|
val showDeveloperSettings = booleanPreference("show_developer_settings", context.isDebuggable)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ class DownloadedAppRepository(
|
|||||||
data: Parcelable,
|
data: Parcelable,
|
||||||
expectedPackageName: String,
|
expectedPackageName: String,
|
||||||
expectedVersion: String?,
|
expectedVersion: String?,
|
||||||
|
appCompatibilityCheck: Boolean,
|
||||||
|
patchesCompatibilityCheck: Boolean,
|
||||||
onDownload: suspend (downloadProgress: Pair<Long, Long?>) -> Unit,
|
onDownload: suspend (downloadProgress: Pair<Long, Long?>) -> Unit,
|
||||||
): File {
|
): File {
|
||||||
// Converted integers cannot contain / or .. unlike the package name or version, so they are safer to use here.
|
// Converted integers cannot contain / or .. unlike the package name or version, so they are safer to use here.
|
||||||
@@ -96,7 +98,12 @@ class DownloadedAppRepository(
|
|||||||
val pkgInfo =
|
val pkgInfo =
|
||||||
pm.getPackageInfo(targetFile.toFile()) ?: error("Downloaded APK file is invalid")
|
pm.getPackageInfo(targetFile.toFile()) ?: error("Downloaded APK file is invalid")
|
||||||
if (pkgInfo.packageName != expectedPackageName) error("Downloaded APK has the wrong package name. Expected: $expectedPackageName, Actual: ${pkgInfo.packageName}")
|
if (pkgInfo.packageName != expectedPackageName) error("Downloaded APK has the wrong package name. Expected: $expectedPackageName, Actual: ${pkgInfo.packageName}")
|
||||||
if (expectedVersion != null && pkgInfo.versionName != expectedVersion) error("Downloaded APK has the wrong version. Expected: $expectedVersion, Actual: ${pkgInfo.versionName}")
|
expectedVersion?.let {
|
||||||
|
if (
|
||||||
|
pkgInfo.versionName != expectedVersion &&
|
||||||
|
(appCompatibilityCheck || patchesCompatibilityCheck)
|
||||||
|
) error("The selected app version ($pkgInfo.versionName) doesn't match the suggested version. Please use the suggested version ($expectedVersion), or adjust your settings by disabling \"Require suggested app version\" and enabling \"Disable version compatibility check\".")
|
||||||
|
}
|
||||||
|
|
||||||
// Delete the previous copy (if present).
|
// Delete the previous copy (if present).
|
||||||
dao.get(pkgInfo.packageName, pkgInfo.versionName!!)?.directory?.let {
|
dao.get(pkgInfo.packageName, pkgInfo.versionName!!)?.directory?.let {
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
package app.revanced.manager.domain.repository
|
|
||||||
|
|
||||||
import app.revanced.manager.data.room.AppDatabase
|
|
||||||
import app.revanced.manager.data.room.AppDatabase.Companion.generateUid
|
|
||||||
import app.revanced.manager.data.room.bundles.PatchBundleEntity
|
|
||||||
import app.revanced.manager.data.room.bundles.Source
|
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
||||||
|
|
||||||
class PatchBundlePersistenceRepository(db: AppDatabase) {
|
|
||||||
private val dao = db.patchBundleDao()
|
|
||||||
|
|
||||||
suspend fun loadConfiguration(): List<PatchBundleEntity> {
|
|
||||||
val all = dao.all()
|
|
||||||
if (all.isEmpty()) {
|
|
||||||
dao.add(defaultSource)
|
|
||||||
return listOf(defaultSource)
|
|
||||||
}
|
|
||||||
|
|
||||||
return all
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun reset() = dao.reset()
|
|
||||||
|
|
||||||
suspend fun create(name: String, source: Source, autoUpdate: Boolean = false) =
|
|
||||||
PatchBundleEntity(
|
|
||||||
uid = generateUid(),
|
|
||||||
name = name,
|
|
||||||
version = null,
|
|
||||||
source = source,
|
|
||||||
autoUpdate = autoUpdate
|
|
||||||
).also {
|
|
||||||
dao.add(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun delete(uid: Int) = dao.remove(uid)
|
|
||||||
|
|
||||||
suspend fun updateVersion(uid: Int, version: String?) =
|
|
||||||
dao.updateVersion(uid, version)
|
|
||||||
|
|
||||||
suspend fun setAutoUpdate(uid: Int, value: Boolean) = dao.setAutoUpdate(uid, value)
|
|
||||||
|
|
||||||
suspend fun setName(uid: Int, name: String) = dao.setName(uid, name)
|
|
||||||
|
|
||||||
fun getProps(id: Int) = dao.getPropsById(id).distinctUntilChanged()
|
|
||||||
|
|
||||||
private companion object {
|
|
||||||
val defaultSource = PatchBundleEntity(
|
|
||||||
uid = 0,
|
|
||||||
name = "",
|
|
||||||
version = null,
|
|
||||||
source = Source.API,
|
|
||||||
autoUpdate = false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,55 +3,78 @@ package app.revanced.manager.domain.repository
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import app.revanced.library.mostCommonCompatibleVersions
|
import app.revanced.library.mostCommonCompatibleVersions
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.data.platform.NetworkInfo
|
import app.revanced.manager.data.platform.NetworkInfo
|
||||||
|
import app.revanced.manager.data.redux.Action
|
||||||
|
import app.revanced.manager.data.redux.ActionContext
|
||||||
|
import app.revanced.manager.data.redux.Store
|
||||||
|
import app.revanced.manager.data.room.AppDatabase
|
||||||
|
import app.revanced.manager.data.room.AppDatabase.Companion.generateUid
|
||||||
import app.revanced.manager.data.room.bundles.PatchBundleEntity
|
import app.revanced.manager.data.room.bundles.PatchBundleEntity
|
||||||
|
import app.revanced.manager.data.room.bundles.PatchBundleProperties
|
||||||
|
import app.revanced.manager.data.room.bundles.Source
|
||||||
import app.revanced.manager.domain.bundles.APIPatchBundle
|
import app.revanced.manager.domain.bundles.APIPatchBundle
|
||||||
import app.revanced.manager.domain.bundles.JsonPatchBundle
|
import app.revanced.manager.domain.bundles.JsonPatchBundle
|
||||||
import app.revanced.manager.data.room.bundles.Source as SourceInfo
|
import app.revanced.manager.data.room.bundles.Source as SourceInfo
|
||||||
import app.revanced.manager.domain.bundles.LocalPatchBundle
|
import app.revanced.manager.domain.bundles.LocalPatchBundle
|
||||||
import app.revanced.manager.domain.bundles.RemotePatchBundle
|
import app.revanced.manager.domain.bundles.RemotePatchBundle
|
||||||
import app.revanced.manager.domain.bundles.PatchBundleSource
|
import app.revanced.manager.domain.bundles.PatchBundleSource
|
||||||
|
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.isDefault
|
||||||
import app.revanced.manager.domain.manager.PreferencesManager
|
import app.revanced.manager.domain.manager.PreferencesManager
|
||||||
import app.revanced.manager.patcher.patch.PatchInfo
|
import app.revanced.manager.patcher.patch.PatchInfo
|
||||||
import app.revanced.manager.util.flatMapLatestAndCombine
|
import app.revanced.manager.patcher.patch.PatchBundle
|
||||||
|
import app.revanced.manager.patcher.patch.PatchBundleInfo
|
||||||
|
import app.revanced.manager.util.simpleMessage
|
||||||
import app.revanced.manager.util.tag
|
import app.revanced.manager.util.tag
|
||||||
import app.revanced.manager.util.uiSafe
|
import app.revanced.manager.util.toast
|
||||||
|
import kotlinx.collections.immutable.*
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
import kotlin.collections.joinToString
|
||||||
|
import kotlin.collections.map
|
||||||
|
import kotlin.text.ifEmpty
|
||||||
|
|
||||||
class PatchBundleRepository(
|
class PatchBundleRepository(
|
||||||
private val app: Application,
|
private val app: Application,
|
||||||
private val persistenceRepo: PatchBundlePersistenceRepository,
|
|
||||||
private val networkInfo: NetworkInfo,
|
private val networkInfo: NetworkInfo,
|
||||||
private val prefs: PreferencesManager,
|
private val prefs: PreferencesManager,
|
||||||
|
db: AppDatabase,
|
||||||
) {
|
) {
|
||||||
|
private val dao = db.patchBundleDao()
|
||||||
private val bundlesDir = app.getDir("patch_bundles", Context.MODE_PRIVATE)
|
private val bundlesDir = app.getDir("patch_bundles", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
private val _sources: MutableStateFlow<Map<Int, PatchBundleSource>> =
|
private val store = Store(CoroutineScope(Dispatchers.Default), State())
|
||||||
MutableStateFlow(emptyMap())
|
|
||||||
val sources = _sources.map { it.values.toList() }
|
|
||||||
|
|
||||||
val bundles = sources.flatMapLatestAndCombine(
|
val sources = store.state.map { it.sources.values.toList() }
|
||||||
combiner = {
|
val bundles = store.state.map {
|
||||||
it.mapNotNull { (uid, state) ->
|
it.sources.mapNotNull { (uid, src) ->
|
||||||
val bundle = state.patchBundleOrNull() ?: return@mapNotNull null
|
uid to (src.patchBundle ?: return@mapNotNull null)
|
||||||
uid to bundle
|
}.toMap()
|
||||||
}.toMap()
|
}
|
||||||
|
val bundleInfoFlow = store.state.map { it.info }
|
||||||
|
|
||||||
|
fun scopedBundleInfoFlow(packageName: String, version: String?) = bundleInfoFlow.map {
|
||||||
|
it.map { (_, bundleInfo) ->
|
||||||
|
bundleInfo.forPackage(
|
||||||
|
packageName,
|
||||||
|
version
|
||||||
|
)
|
||||||
}
|
}
|
||||||
) {
|
|
||||||
it.state.map { state -> it.uid to state }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val suggestedVersions = bundles.map {
|
val patchCountsFlow = bundleInfoFlow.map { it.mapValues { (_, info) -> info.patches.size } }
|
||||||
|
|
||||||
|
val suggestedVersions = bundleInfoFlow.map {
|
||||||
val allPatches =
|
val allPatches =
|
||||||
it.values.flatMap { bundle -> bundle.patches.map(PatchInfo::toPatcherPatch) }.toSet()
|
it.values.flatMap { bundle -> bundle.patches.map(PatchInfo::toPatcherPatch) }.toSet()
|
||||||
|
|
||||||
@@ -74,6 +97,100 @@ class PatchBundleRepository(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend inline fun dispatchAction(
|
||||||
|
name: String,
|
||||||
|
crossinline block: suspend ActionContext.(current: State) -> State
|
||||||
|
) {
|
||||||
|
store.dispatch(object : Action<State> {
|
||||||
|
override suspend fun ActionContext.execute(current: State) = block(current)
|
||||||
|
override fun toString() = name
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a reload. Do not call this outside of a store action.
|
||||||
|
*/
|
||||||
|
private suspend fun doReload(): State {
|
||||||
|
val entities = loadFromDb().onEach {
|
||||||
|
Log.d(tag, "Bundle: $it")
|
||||||
|
}
|
||||||
|
|
||||||
|
val sources = entities.associate { it.uid to it.load() }.toPersistentMap()
|
||||||
|
|
||||||
|
val hasOutOfDateNames = sources.values.any { it.isNameOutOfDate }
|
||||||
|
if (hasOutOfDateNames) dispatchAction(
|
||||||
|
"Sync names"
|
||||||
|
) { state ->
|
||||||
|
val nameChanges = state.sources.mapNotNull { (_, src) ->
|
||||||
|
if (!src.isNameOutOfDate) return@mapNotNull null
|
||||||
|
val newName = src.patchBundle?.manifestAttributes?.name?.takeIf { it != src.name }
|
||||||
|
?: return@mapNotNull null
|
||||||
|
|
||||||
|
src.uid to newName
|
||||||
|
}
|
||||||
|
val sources = state.sources.toMutableMap()
|
||||||
|
val info = state.info.toMutableMap()
|
||||||
|
nameChanges.forEach { (uid, name) ->
|
||||||
|
updateDb(uid) { it.copy(name = name) }
|
||||||
|
sources[uid] = sources[uid]!!.copy(name = name)
|
||||||
|
info[uid] = info[uid]?.copy(name = name) ?: return@forEach
|
||||||
|
}
|
||||||
|
|
||||||
|
State(sources.toPersistentMap(), info.toPersistentMap())
|
||||||
|
}
|
||||||
|
val info = loadMetadata(sources).toPersistentMap()
|
||||||
|
|
||||||
|
return State(sources, info)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun reload() = dispatchAction("Full reload") {
|
||||||
|
doReload()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadFromDb(): List<PatchBundleEntity> {
|
||||||
|
val all = dao.all()
|
||||||
|
if (all.isEmpty()) {
|
||||||
|
dao.upsert(defaultSource)
|
||||||
|
return listOf(defaultSource)
|
||||||
|
}
|
||||||
|
|
||||||
|
return all
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadMetadata(sources: Map<Int, PatchBundleSource>): Map<Int, PatchBundleInfo.Global> {
|
||||||
|
// Map bundles -> sources
|
||||||
|
val map = sources.mapNotNull { (_, src) ->
|
||||||
|
(src.patchBundle ?: return@mapNotNull null) to src
|
||||||
|
}.toMap()
|
||||||
|
|
||||||
|
val metadata = try {
|
||||||
|
PatchBundle.Loader.metadata(map.keys)
|
||||||
|
} catch (error: Throwable) {
|
||||||
|
val uids = map.values.map { it.uid }
|
||||||
|
|
||||||
|
dispatchAction("Mark bundles as failed") { state ->
|
||||||
|
state.copy(sources = state.sources.mutate {
|
||||||
|
uids.forEach { uid ->
|
||||||
|
it[uid] = it[uid]?.copy(error = error) ?: return@forEach
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.e(tag, "Failed to load bundles", error)
|
||||||
|
emptyMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata.entries.associate { (bundle, patches) ->
|
||||||
|
val src = map[bundle]!!
|
||||||
|
src.uid to PatchBundleInfo.Global(
|
||||||
|
src.name,
|
||||||
|
bundle.manifestAttributes?.version,
|
||||||
|
src.uid,
|
||||||
|
patches
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun isVersionAllowed(packageName: String, version: String) =
|
suspend fun isVersionAllowed(packageName: String, version: String) =
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
if (!prefs.suggestedVersionSafeguard.get()) return@withContext true
|
if (!prefs.suggestedVersionSafeguard.get()) return@withContext true
|
||||||
@@ -89,96 +206,211 @@ class PatchBundleRepository(
|
|||||||
|
|
||||||
private fun PatchBundleEntity.load(): PatchBundleSource {
|
private fun PatchBundleEntity.load(): PatchBundleSource {
|
||||||
val dir = directoryOf(uid)
|
val dir = directoryOf(uid)
|
||||||
|
val actualName =
|
||||||
|
name.ifEmpty { app.getString(if (uid == 0) R.string.patches_name_default else R.string.patches_name_fallback) }
|
||||||
|
|
||||||
return when (source) {
|
return when (source) {
|
||||||
is SourceInfo.Local -> LocalPatchBundle(name, uid, dir)
|
is SourceInfo.Local -> LocalPatchBundle(actualName, uid, null, dir)
|
||||||
is SourceInfo.API -> APIPatchBundle(name, uid, dir, SourceInfo.API.SENTINEL)
|
is SourceInfo.API -> APIPatchBundle(
|
||||||
is SourceInfo.Remote -> JsonPatchBundle(
|
actualName,
|
||||||
name,
|
|
||||||
uid,
|
uid,
|
||||||
|
versionHash,
|
||||||
|
null,
|
||||||
dir,
|
dir,
|
||||||
source.url.toString()
|
SourceInfo.API.SENTINEL,
|
||||||
|
autoUpdate,
|
||||||
|
)
|
||||||
|
|
||||||
|
is SourceInfo.Remote -> JsonPatchBundle(
|
||||||
|
actualName,
|
||||||
|
uid,
|
||||||
|
versionHash,
|
||||||
|
null,
|
||||||
|
dir,
|
||||||
|
source.url.toString(),
|
||||||
|
autoUpdate,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun reload() = withContext(Dispatchers.Default) {
|
private suspend fun createEntity(name: String, source: Source, autoUpdate: Boolean = false) =
|
||||||
val entities = persistenceRepo.loadConfiguration().onEach {
|
PatchBundleEntity(
|
||||||
Log.d(tag, "Bundle: $it")
|
uid = generateUid(),
|
||||||
|
name = name,
|
||||||
|
versionHash = null,
|
||||||
|
source = source,
|
||||||
|
autoUpdate = autoUpdate
|
||||||
|
).also {
|
||||||
|
dao.upsert(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
_sources.value = entities.associate {
|
/**
|
||||||
it.uid to it.load()
|
* Updates a patch bundle in the database. Do not use this outside an action.
|
||||||
}
|
*/
|
||||||
|
private suspend fun updateDb(
|
||||||
|
uid: Int,
|
||||||
|
block: (PatchBundleProperties) -> PatchBundleProperties
|
||||||
|
) {
|
||||||
|
val previous = dao.getProps(uid)!!
|
||||||
|
val new = block(previous)
|
||||||
|
dao.upsert(
|
||||||
|
PatchBundleEntity(
|
||||||
|
uid = uid,
|
||||||
|
name = new.name,
|
||||||
|
versionHash = new.versionHash,
|
||||||
|
source = new.source,
|
||||||
|
autoUpdate = new.autoUpdate,
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun reset() = withContext(Dispatchers.Default) {
|
suspend fun reset() = dispatchAction("Reset") { state ->
|
||||||
persistenceRepo.reset()
|
dao.reset()
|
||||||
_sources.value = emptyMap()
|
state.sources.keys.forEach { directoryOf(it).deleteRecursively() }
|
||||||
bundlesDir.apply {
|
doReload()
|
||||||
deleteRecursively()
|
|
||||||
mkdirs()
|
|
||||||
}
|
|
||||||
|
|
||||||
reload()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun remove(bundle: PatchBundleSource) = withContext(Dispatchers.Default) {
|
suspend fun remove(vararg bundles: PatchBundleSource) =
|
||||||
persistenceRepo.delete(bundle.uid)
|
dispatchAction("Remove (${bundles.map { it.uid }.joinToString(",")})") { state ->
|
||||||
directoryOf(bundle.uid).deleteRecursively()
|
val sources = state.sources.toMutableMap()
|
||||||
|
val info = state.info.toMutableMap()
|
||||||
|
bundles.forEach {
|
||||||
|
if (it.isDefault) return@forEach
|
||||||
|
|
||||||
_sources.update {
|
dao.remove(it.uid)
|
||||||
it.filterKeys { key ->
|
directoryOf(it.uid).deleteRecursively()
|
||||||
key != bundle.uid
|
sources.remove(it.uid)
|
||||||
|
info.remove(it.uid)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addBundle(patchBundle: PatchBundleSource) =
|
State(sources.toPersistentMap(), info.toPersistentMap())
|
||||||
_sources.update { it.toMutableMap().apply { put(patchBundle.uid, patchBundle) } }
|
|
||||||
|
|
||||||
suspend fun createLocal(patches: InputStream) = withContext(Dispatchers.Default) {
|
|
||||||
val uid = persistenceRepo.create("", SourceInfo.Local).uid
|
|
||||||
val bundle = LocalPatchBundle("", uid, directoryOf(uid))
|
|
||||||
|
|
||||||
bundle.replace(patches)
|
|
||||||
addBundle(bundle)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun createRemote(url: String, autoUpdate: Boolean) = withContext(Dispatchers.Default) {
|
|
||||||
val entity = persistenceRepo.create("", SourceInfo.from(url), autoUpdate)
|
|
||||||
addBundle(entity.load())
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend inline fun <reified T> getBundlesByType() =
|
|
||||||
sources.first().filterIsInstance<T>()
|
|
||||||
|
|
||||||
suspend fun reloadApiBundles() {
|
|
||||||
getBundlesByType<APIPatchBundle>().forEach {
|
|
||||||
it.deleteLocalFiles()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reload()
|
suspend fun createLocal(createStream: suspend () -> InputStream) = dispatchAction("Add bundle") {
|
||||||
}
|
with(createEntity("", SourceInfo.Local).load() as LocalPatchBundle) {
|
||||||
|
try {
|
||||||
suspend fun redownloadRemoteBundles() =
|
createStream().use { patches -> replace(patches) }
|
||||||
getBundlesByType<RemotePatchBundle>().forEach { it.downloadLatest() }
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
suspend fun updateCheck() =
|
Log.e(tag, "Got exception while importing bundle", e)
|
||||||
uiSafe(app, R.string.source_download_fail, "Failed to update bundles") {
|
withContext(Dispatchers.Main) {
|
||||||
coroutineScope {
|
app.toast(app.getString(R.string.patches_replace_fail, e.simpleMessage()))
|
||||||
if (!networkInfo.isSafe()) {
|
|
||||||
Log.d(tag, "Skipping update check because the network is down or metered.")
|
|
||||||
return@coroutineScope
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getBundlesByType<RemotePatchBundle>().forEach {
|
deleteLocalFile()
|
||||||
launch {
|
}
|
||||||
if (!it.getProps().autoUpdate) return@launch
|
}
|
||||||
Log.d(tag, "Updating patch bundle: ${it.getName()}")
|
|
||||||
it.update()
|
doReload()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun createRemote(url: String, autoUpdate: Boolean) =
|
||||||
|
dispatchAction("Add bundle ($url)") { state ->
|
||||||
|
val src = createEntity("", SourceInfo.from(url), autoUpdate).load() as RemotePatchBundle
|
||||||
|
update(src)
|
||||||
|
state.copy(sources = state.sources.put(src.uid, src))
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun reloadApiBundles() = dispatchAction("Reload API bundles") {
|
||||||
|
this@PatchBundleRepository.sources.first().filterIsInstance<APIPatchBundle>().forEach {
|
||||||
|
with(it) { deleteLocalFile() }
|
||||||
|
updateDb(it.uid) { it.copy(versionHash = null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
doReload()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun RemotePatchBundle.setAutoUpdate(value: Boolean) =
|
||||||
|
dispatchAction("Set auto update ($name, $value)") { state ->
|
||||||
|
updateDb(uid) { it.copy(autoUpdate = value) }
|
||||||
|
val newSrc = (state.sources[uid] as? RemotePatchBundle)?.copy(autoUpdate = value)
|
||||||
|
?: return@dispatchAction state
|
||||||
|
|
||||||
|
state.copy(sources = state.sources.put(uid, newSrc))
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun update(vararg sources: RemotePatchBundle, showToast: Boolean = false) {
|
||||||
|
val uids = sources.map { it.uid }.toSet()
|
||||||
|
store.dispatch(Update(showToast = showToast) { it.uid in uids })
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun redownloadRemoteBundles() = store.dispatch(Update(force = true))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates all bundles that should be automatically updated.
|
||||||
|
*/
|
||||||
|
suspend fun updateCheck() = store.dispatch(Update { it.autoUpdate })
|
||||||
|
|
||||||
|
private inner class Update(
|
||||||
|
private val force: Boolean = false,
|
||||||
|
private val showToast: Boolean = false,
|
||||||
|
private val predicate: (bundle: RemotePatchBundle) -> Boolean = { true },
|
||||||
|
) : Action<State> {
|
||||||
|
private suspend fun toast(@StringRes id: Int, vararg args: Any?) =
|
||||||
|
withContext(Dispatchers.Main) { app.toast(app.getString(id, *args)) }
|
||||||
|
|
||||||
|
override fun toString() = if (force) "Redownload remote bundles" else "Update check"
|
||||||
|
|
||||||
|
override suspend fun ActionContext.execute(
|
||||||
|
current: State
|
||||||
|
) = coroutineScope {
|
||||||
|
if (!networkInfo.isSafe()) {
|
||||||
|
Log.d(tag, "Skipping update check because the network is down or metered.")
|
||||||
|
return@coroutineScope current
|
||||||
|
}
|
||||||
|
|
||||||
|
val updated = current.sources.values
|
||||||
|
.filterIsInstance<RemotePatchBundle>()
|
||||||
|
.filter { predicate(it) }
|
||||||
|
.map {
|
||||||
|
async {
|
||||||
|
Log.d(tag, "Updating patch bundle: ${it.name}")
|
||||||
|
|
||||||
|
val newVersion = with(it) {
|
||||||
|
if (force) downloadLatest() else update()
|
||||||
|
} ?: return@async null
|
||||||
|
|
||||||
|
it to newVersion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.awaitAll()
|
||||||
|
.filterNotNull()
|
||||||
|
.toMap()
|
||||||
|
if (updated.isEmpty()) {
|
||||||
|
if (showToast) toast(R.string.patches_update_unavailable)
|
||||||
|
return@coroutineScope current
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updated.forEach { (src, newVersionHash) ->
|
||||||
|
val name = src.patchBundle?.manifestAttributes?.name ?: src.name
|
||||||
|
|
||||||
|
updateDb(src.uid) {
|
||||||
|
it.copy(versionHash = newVersionHash, name = name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showToast) toast(R.string.patches_update_success)
|
||||||
|
doReload()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun catch(exception: Exception) {
|
||||||
|
Log.e(tag, "Failed to update patches", exception)
|
||||||
|
toast(R.string.patches_download_fail, exception.simpleMessage())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class State(
|
||||||
|
val sources: PersistentMap<Int, PatchBundleSource> = persistentMapOf(),
|
||||||
|
val info: PersistentMap<Int, PatchBundleInfo.Global> = persistentMapOf()
|
||||||
|
)
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
val defaultSource = PatchBundleEntity(
|
||||||
|
uid = 0,
|
||||||
|
name = "",
|
||||||
|
versionHash = null,
|
||||||
|
source = Source.API,
|
||||||
|
autoUpdate = false
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -76,7 +76,7 @@ class PatchOptionsRepository(db: AppDatabase) {
|
|||||||
fun getPackagesWithSavedOptions() =
|
fun getPackagesWithSavedOptions() =
|
||||||
dao.getPackagesWithOptions().map(Iterable<String>::toSet).distinctUntilChanged()
|
dao.getPackagesWithOptions().map(Iterable<String>::toSet).distinctUntilChanged()
|
||||||
|
|
||||||
suspend fun clearOptionsForPackage(packageName: String) = dao.clearForPackage(packageName)
|
suspend fun resetOptionsForPackage(packageName: String) = dao.resetOptionsForPackage(packageName)
|
||||||
suspend fun clearOptionsForPatchBundle(uid: Int) = dao.clearForPatchBundle(uid)
|
suspend fun resetOptionsForPatchBundle(uid: Int) = dao.resetOptionsForPatchBundle(uid)
|
||||||
suspend fun reset() = dao.reset()
|
suspend fun reset() = dao.reset()
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,8 @@ package app.revanced.manager.domain.repository
|
|||||||
import app.revanced.manager.data.room.AppDatabase
|
import app.revanced.manager.data.room.AppDatabase
|
||||||
import app.revanced.manager.data.room.AppDatabase.Companion.generateUid
|
import app.revanced.manager.data.room.AppDatabase.Companion.generateUid
|
||||||
import app.revanced.manager.data.room.selection.PatchSelection
|
import app.revanced.manager.data.room.selection.PatchSelection
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
class PatchSelectionRepository(db: AppDatabase) {
|
class PatchSelectionRepository(db: AppDatabase) {
|
||||||
private val dao = db.selectionDao()
|
private val dao = db.selectionDao()
|
||||||
@@ -25,8 +27,15 @@ class PatchSelectionRepository(db: AppDatabase) {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
suspend fun clearSelection(packageName: String) {
|
fun getPackagesWithSavedSelection() =
|
||||||
dao.clearForPackage(packageName)
|
dao.getPackagesWithSelection().map(Iterable<String>::toSet).distinctUntilChanged()
|
||||||
|
|
||||||
|
suspend fun resetSelectionForPackage(packageName: String) {
|
||||||
|
dao.resetForPackage(packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun resetSelectionForPatchBundle(uid: Int) {
|
||||||
|
dao.resetForPatchBundle(uid)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun reset() = dao.reset()
|
suspend fun reset() = dao.reset()
|
||||||
@@ -34,7 +43,7 @@ class PatchSelectionRepository(db: AppDatabase) {
|
|||||||
suspend fun export(bundleUid: Int): SerializedSelection = dao.exportSelection(bundleUid)
|
suspend fun export(bundleUid: Int): SerializedSelection = dao.exportSelection(bundleUid)
|
||||||
|
|
||||||
suspend fun import(bundleUid: Int, selection: SerializedSelection) {
|
suspend fun import(bundleUid: Int, selection: SerializedSelection) {
|
||||||
dao.clearForPatchBundle(bundleUid)
|
dao.resetForPatchBundle(bundleUid)
|
||||||
dao.updateSelections(selection.entries.associate { (packageName, patches) ->
|
dao.updateSelections(selection.entries.associate { (packageName, patches) ->
|
||||||
getOrCreateSelection(bundleUid, packageName) to patches.toSet()
|
getOrCreateSelection(bundleUid, packageName) to patches.toSet()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import kotlinx.parcelize.Parcelize
|
|||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
/**
|
/**
|
||||||
* A container for [Parcelable] data returned from downloaders. Instances of this class can be safely stored in a bundle without needing to set the [ClassLoader].
|
* A container for [Parcelable] data returned from downloader. Instances of this class can be safely stored in a bundle without needing to set the [ClassLoader].
|
||||||
*/
|
*/
|
||||||
class ParceledDownloaderData private constructor(
|
class ParceledDownloaderData private constructor(
|
||||||
val pluginPackageName: String,
|
val pluginPackageName: String,
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import app.revanced.manager.util.tag
|
|||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.call.body
|
import io.ktor.client.call.body
|
||||||
import io.ktor.client.request.HttpRequestBuilder
|
import io.ktor.client.request.HttpRequestBuilder
|
||||||
import io.ktor.client.request.get
|
|
||||||
import io.ktor.client.request.prepareGet
|
import io.ktor.client.request.prepareGet
|
||||||
import io.ktor.client.request.request
|
import io.ktor.client.request.request
|
||||||
import io.ktor.client.statement.bodyAsText
|
import io.ktor.client.statement.bodyAsText
|
||||||
@@ -17,7 +16,6 @@ import io.ktor.http.isSuccess
|
|||||||
import io.ktor.utils.io.ByteReadChannel
|
import io.ktor.utils.io.ByteReadChannel
|
||||||
import io.ktor.utils.io.core.isNotEmpty
|
import io.ktor.utils.io.core.isNotEmpty
|
||||||
import io.ktor.utils.io.core.readBytes
|
import io.ktor.utils.io.core.readBytes
|
||||||
import it.skrape.core.htmlDocument
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
@@ -93,9 +91,5 @@ class HttpService(
|
|||||||
builder: HttpRequestBuilder.() -> Unit
|
builder: HttpRequestBuilder.() -> Unit
|
||||||
) = saveLocation.outputStream().use { streamTo(it, builder) }
|
) = saveLocation.outputStream().use { streamTo(it, builder) }
|
||||||
|
|
||||||
suspend fun getHtml(builder: HttpRequestBuilder.() -> Unit) = htmlDocument(
|
|
||||||
html = http.get(builder).bodyAsText()
|
|
||||||
)
|
|
||||||
|
|
||||||
class HttpException(status: HttpStatusCode) : Exception("Failed to fetch: http status: $status")
|
class HttpException(status: HttpStatusCode) : Exception("Failed to fetch: http status: $status")
|
||||||
}
|
}
|
||||||
@@ -1,56 +1,84 @@
|
|||||||
package app.revanced.manager.patcher.patch
|
package app.revanced.manager.patcher.patch
|
||||||
|
|
||||||
import android.util.Log
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
import app.revanced.manager.util.tag
|
import android.os.Parcelable
|
||||||
import app.revanced.patcher.patch.Patch
|
import app.revanced.patcher.patch.loadPatchesFromDex
|
||||||
import app.revanced.patcher.patch.PatchLoader
|
import kotlinx.parcelize.Parcelize
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.jar.JarFile
|
import java.util.jar.JarFile
|
||||||
|
import kotlin.collections.filter
|
||||||
|
|
||||||
class PatchBundle(val patchesJar: File) {
|
@Parcelize
|
||||||
private val loader = object : Iterable<Patch<*>> {
|
data class PatchBundle(val patchesJar: String) : Parcelable {
|
||||||
private fun load(): Iterable<Patch<*>> {
|
|
||||||
patchesJar.setReadOnly()
|
|
||||||
return PatchLoader.Dex(setOf(patchesJar))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun iterator(): Iterator<Patch<*>> = load().iterator()
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
Log.d(tag, "Loaded patch bundle: $patchesJar")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A list containing the metadata of every patch inside this bundle.
|
|
||||||
*/
|
|
||||||
val patches = loader.map(::PatchInfo)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The [java.util.jar.Manifest] of [patchesJar].
|
* The [java.util.jar.Manifest] of [patchesJar].
|
||||||
*/
|
*/
|
||||||
private val manifest = try {
|
@IgnoredOnParcel
|
||||||
JarFile(patchesJar).use { it.manifest }
|
private val manifest by lazy {
|
||||||
} catch (_: IOException) {
|
try {
|
||||||
null
|
JarFile(patchesJar).use { it.manifest }
|
||||||
}
|
} catch (_: IOException) {
|
||||||
|
null
|
||||||
fun readManifestAttribute(name: String) = manifest?.mainAttributes?.getValue(name)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load all patches compatible with the specified package.
|
|
||||||
*/
|
|
||||||
fun patches(packageName: String) = loader.filter { patch ->
|
|
||||||
val compatiblePackages = patch.compatiblePackages
|
|
||||||
?: // The patch has no compatibility constraints, which means it is universal.
|
|
||||||
return@filter true
|
|
||||||
|
|
||||||
if (!compatiblePackages.any { (name, _) -> name == packageName }) {
|
|
||||||
// Patch is not compatible with this package.
|
|
||||||
return@filter false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@IgnoredOnParcel
|
||||||
|
val manifestAttributes by lazy {
|
||||||
|
if (manifest != null)
|
||||||
|
ManifestAttributes(
|
||||||
|
name = readManifestAttribute("name"),
|
||||||
|
version = readManifestAttribute("version"),
|
||||||
|
description = readManifestAttribute("description"),
|
||||||
|
source = readManifestAttribute("source"),
|
||||||
|
author = readManifestAttribute("author"),
|
||||||
|
contact = readManifestAttribute("contact"),
|
||||||
|
website = readManifestAttribute("website"),
|
||||||
|
license = readManifestAttribute("license")
|
||||||
|
) else
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readManifestAttribute(name: String) = manifest?.mainAttributes?.getValue(name)
|
||||||
|
?.takeIf { it.isNotBlank() } // If empty, set it to null instead.
|
||||||
|
|
||||||
|
data class ManifestAttributes(
|
||||||
|
val name: String?,
|
||||||
|
val version: String?,
|
||||||
|
val description: String?,
|
||||||
|
val source: String?,
|
||||||
|
val author: String?,
|
||||||
|
val contact: String?,
|
||||||
|
val website: String?,
|
||||||
|
val license: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
object Loader {
|
||||||
|
private fun patches(bundles: Iterable<PatchBundle>) =
|
||||||
|
loadPatchesFromDex(
|
||||||
|
bundles.map { File(it.patchesJar) }.toSet()
|
||||||
|
).byPatchesFile.mapKeys { (file, _) ->
|
||||||
|
val absPath = file.absolutePath
|
||||||
|
bundles.single { absPath == it.patchesJar }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun metadata(bundles: Iterable<PatchBundle>) =
|
||||||
|
patches(bundles).mapValues { (_, patches) -> patches.map(::PatchInfo) }
|
||||||
|
|
||||||
|
fun patches(bundles: Iterable<PatchBundle>, packageName: String) =
|
||||||
|
patches(bundles).mapValues { (_, patches) ->
|
||||||
|
patches.filter { patch ->
|
||||||
|
val compatiblePackages = patch.compatiblePackages
|
||||||
|
?: // The patch has no compatibility constraints, which means it is universal.
|
||||||
|
return@filter true
|
||||||
|
|
||||||
|
if (!compatiblePackages.any { (name, _) -> name == packageName }) {
|
||||||
|
// Patch is not compatible with this package.
|
||||||
|
return@filter false
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}.toSet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
package app.revanced.manager.patcher.patch
|
||||||
|
|
||||||
|
import app.revanced.manager.util.PatchSelection
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A base class for storing [PatchBundle] metadata.
|
||||||
|
*/
|
||||||
|
sealed class PatchBundleInfo {
|
||||||
|
/**
|
||||||
|
* The name of the bundle.
|
||||||
|
*/
|
||||||
|
abstract val name: String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The version of the bundle.
|
||||||
|
*/
|
||||||
|
abstract val version: String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The unique ID of the bundle.
|
||||||
|
*/
|
||||||
|
abstract val uid: Int
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The patch list.
|
||||||
|
*/
|
||||||
|
abstract val patches: List<PatchInfo>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Information about a bundle and all the patches it contains.
|
||||||
|
*
|
||||||
|
* @see [PatchBundleInfo]
|
||||||
|
*/
|
||||||
|
data class Global(
|
||||||
|
override val name: String,
|
||||||
|
override val version: String?,
|
||||||
|
override val uid: Int,
|
||||||
|
override val patches: List<PatchInfo>
|
||||||
|
) : PatchBundleInfo() {
|
||||||
|
/**
|
||||||
|
* Create a [PatchBundleInfo.Scoped] that only contains information about patches that are relevant for a specific [packageName].
|
||||||
|
*/
|
||||||
|
fun forPackage(packageName: String, version: String?): Scoped {
|
||||||
|
val relevantPatches = patches.filter { it.compatibleWith(packageName) }
|
||||||
|
val compatible = mutableListOf<PatchInfo>()
|
||||||
|
val incompatible = mutableListOf<PatchInfo>()
|
||||||
|
val universal = mutableListOf<PatchInfo>()
|
||||||
|
|
||||||
|
relevantPatches.forEach {
|
||||||
|
val targetList = when {
|
||||||
|
it.compatiblePackages == null -> universal
|
||||||
|
it.supports(
|
||||||
|
packageName,
|
||||||
|
version
|
||||||
|
) -> compatible
|
||||||
|
|
||||||
|
else -> incompatible
|
||||||
|
}
|
||||||
|
|
||||||
|
targetList.add(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scoped(
|
||||||
|
name,
|
||||||
|
this.version,
|
||||||
|
uid,
|
||||||
|
relevantPatches,
|
||||||
|
compatible,
|
||||||
|
incompatible,
|
||||||
|
universal
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains information about a bundle that is relevant for a specific package name.
|
||||||
|
*
|
||||||
|
* @param compatible Patches that are compatible with the specified package name and version.
|
||||||
|
* @param incompatible Patches that are compatible with the specified package name but not version.
|
||||||
|
* @param universal Patches that are compatible with all packages.
|
||||||
|
* @see [PatchBundleInfo.Global.forPackage]
|
||||||
|
* @see [PatchBundleInfo]
|
||||||
|
*/
|
||||||
|
data class Scoped(
|
||||||
|
override val name: String,
|
||||||
|
override val version: String?,
|
||||||
|
override val uid: Int,
|
||||||
|
override val patches: List<PatchInfo>,
|
||||||
|
val compatible: List<PatchInfo>,
|
||||||
|
val incompatible: List<PatchInfo>,
|
||||||
|
val universal: List<PatchInfo>
|
||||||
|
) : PatchBundleInfo() {
|
||||||
|
fun patchSequence(allowIncompatible: Boolean) = if (allowIncompatible) {
|
||||||
|
patches.asSequence()
|
||||||
|
} else {
|
||||||
|
sequence {
|
||||||
|
yieldAll(compatible)
|
||||||
|
yieldAll(universal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object Extensions {
|
||||||
|
inline fun Iterable<Scoped>.toPatchSelection(
|
||||||
|
allowIncompatible: Boolean,
|
||||||
|
condition: (Int, PatchInfo) -> Boolean
|
||||||
|
): PatchSelection = this.associate { bundle ->
|
||||||
|
val patches =
|
||||||
|
bundle.patchSequence(allowIncompatible)
|
||||||
|
.mapNotNullTo(mutableSetOf()) { patch ->
|
||||||
|
patch.name.takeIf {
|
||||||
|
condition(
|
||||||
|
bundle.uid,
|
||||||
|
patch
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bundle.uid to patches
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Algorithm for determining whether all required options have been set.
|
||||||
|
*/
|
||||||
|
inline fun Iterable<Scoped>.requiredOptionsSet(
|
||||||
|
allowIncompatible: Boolean,
|
||||||
|
crossinline isSelected: (Scoped, PatchInfo) -> Boolean,
|
||||||
|
crossinline optionsForPatch: (Scoped, PatchInfo) -> Map<String, Any?>?
|
||||||
|
) = all bundle@{ bundle ->
|
||||||
|
bundle
|
||||||
|
.patchSequence(allowIncompatible)
|
||||||
|
.filter { isSelected(bundle, it) }
|
||||||
|
.all patch@{
|
||||||
|
if (it.options.isNullOrEmpty()) return@patch true
|
||||||
|
val opts by lazy { optionsForPatch(bundle, it).orEmpty() }
|
||||||
|
|
||||||
|
it.options.all option@{ option ->
|
||||||
|
if (!option.required || option.default != null) return@option true
|
||||||
|
|
||||||
|
option.key in opts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package app.revanced.manager.patcher.runtime
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import app.revanced.manager.patcher.Session
|
import app.revanced.manager.patcher.Session
|
||||||
import app.revanced.manager.patcher.logger.Logger
|
import app.revanced.manager.patcher.logger.Logger
|
||||||
|
import app.revanced.manager.patcher.patch.PatchBundle
|
||||||
import app.revanced.manager.patcher.worker.ProgressEventHandler
|
import app.revanced.manager.patcher.worker.ProgressEventHandler
|
||||||
import app.revanced.manager.ui.model.State
|
import app.revanced.manager.ui.model.State
|
||||||
import app.revanced.manager.util.Options
|
import app.revanced.manager.util.Options
|
||||||
@@ -23,14 +24,17 @@ class CoroutineRuntime(private val context: Context) : Runtime(context) {
|
|||||||
onPatchCompleted: suspend () -> Unit,
|
onPatchCompleted: suspend () -> Unit,
|
||||||
onProgress: ProgressEventHandler,
|
onProgress: ProgressEventHandler,
|
||||||
) {
|
) {
|
||||||
val bundles = bundles()
|
|
||||||
|
|
||||||
val selectedBundles = selectedPatches.keys
|
val selectedBundles = selectedPatches.keys
|
||||||
val allPatches = bundles.filterKeys { selectedBundles.contains(it) }
|
val bundles = bundles()
|
||||||
.mapValues { (_, bundle) -> bundle.patches(packageName) }
|
val uids = bundles.entries.associate { (key, value) -> value to key }
|
||||||
|
|
||||||
|
val allPatches =
|
||||||
|
PatchBundle.Loader.patches(bundles.values, packageName)
|
||||||
|
.mapKeys { (b, _) -> uids[b]!! }
|
||||||
|
.filterKeys { it in selectedBundles }
|
||||||
|
|
||||||
val patchList = selectedPatches.flatMap { (bundle, selected) ->
|
val patchList = selectedPatches.flatMap { (bundle, selected) ->
|
||||||
allPatches[bundle]?.filter { selected.contains(it.name) }
|
allPatches[bundle]?.filter { it.name in selected }
|
||||||
?: throw IllegalArgumentException("Patch bundle $bundle does not exist")
|
?: throw IllegalArgumentException("Patch bundle $bundle does not exist")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val patching = CompletableDeferred<Unit>()
|
val patching = CompletableDeferred<Unit>()
|
||||||
|
val scope = this
|
||||||
|
|
||||||
launch(Dispatchers.IO) {
|
launch(Dispatchers.IO) {
|
||||||
val binder = awaitBinderConnection()
|
val binder = awaitBinderConnection()
|
||||||
@@ -124,7 +125,7 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
|
|||||||
override fun log(level: String, msg: String) = logger.log(enumValueOf(level), msg)
|
override fun log(level: String, msg: String) = logger.log(enumValueOf(level), msg)
|
||||||
|
|
||||||
override fun patchSucceeded() {
|
override fun patchSucceeded() {
|
||||||
launch { onPatchCompleted() }
|
scope.launch { onPatchCompleted() }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun progress(name: String?, state: String?, msg: String?) =
|
override fun progress(name: String?, state: String?, msg: String?) =
|
||||||
@@ -141,8 +142,6 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val bundles = bundles()
|
|
||||||
|
|
||||||
val parameters = Parameters(
|
val parameters = Parameters(
|
||||||
aaptPath = aaptPath,
|
aaptPath = aaptPath,
|
||||||
frameworkDir = frameworkPath,
|
frameworkDir = frameworkPath,
|
||||||
@@ -150,13 +149,11 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
|
|||||||
packageName = packageName,
|
packageName = packageName,
|
||||||
inputFile = inputFile,
|
inputFile = inputFile,
|
||||||
outputFile = outputFile,
|
outputFile = outputFile,
|
||||||
configurations = selectedPatches.map { (id, patches) ->
|
configurations = bundles().map { (uid, bundle) ->
|
||||||
val bundle = bundles[id]!!
|
|
||||||
|
|
||||||
PatchConfiguration(
|
PatchConfiguration(
|
||||||
bundle.patchesJar.absolutePath,
|
bundle,
|
||||||
patches,
|
selectedPatches[uid].orEmpty(),
|
||||||
options[id].orEmpty()
|
options[uid].orEmpty()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -179,7 +176,7 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An [Exception] occured in the remote process while patching.
|
* An [Exception] occurred in the remote process while patching.
|
||||||
*
|
*
|
||||||
* @param originalStackTrace The stack trace of the original [Exception].
|
* @param originalStackTrace The stack trace of the original [Exception].
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package app.revanced.manager.patcher.runtime.process
|
package app.revanced.manager.patcher.runtime.process
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import app.revanced.manager.patcher.patch.PatchBundle
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import kotlinx.parcelize.RawValue
|
import kotlinx.parcelize.RawValue
|
||||||
|
|
||||||
@@ -17,7 +18,7 @@ data class Parameters(
|
|||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class PatchConfiguration(
|
data class PatchConfiguration(
|
||||||
val bundlePath: String,
|
val bundle: PatchBundle,
|
||||||
val patches: Set<String>,
|
val patches: Set<String>,
|
||||||
val options: @RawValue Map<String, Map<String, Any?>>
|
val options: @RawValue Map<String, Map<String, Any?>>
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
package app.revanced.manager.patcher.runtime.process
|
package app.revanced.manager.patcher.runtime.process
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.app.ActivityThread
|
import android.app.ActivityThread
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import app.revanced.manager.BuildConfig
|
import app.revanced.manager.BuildConfig
|
||||||
@@ -54,11 +56,10 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
|
|||||||
|
|
||||||
logger.info("Memory limit: ${Runtime.getRuntime().maxMemory() / (1024 * 1024)}MB")
|
logger.info("Memory limit: ${Runtime.getRuntime().maxMemory() / (1024 * 1024)}MB")
|
||||||
|
|
||||||
|
val allPatches = PatchBundle.Loader.patches(parameters.configurations.map { it.bundle }, parameters.packageName)
|
||||||
val patchList = parameters.configurations.flatMap { config ->
|
val patchList = parameters.configurations.flatMap { config ->
|
||||||
val bundle = PatchBundle(File(config.bundlePath))
|
val patches = (allPatches[config.bundle] ?: return@flatMap emptyList())
|
||||||
|
.filter { it.name in config.patches }
|
||||||
val patches =
|
|
||||||
bundle.patches(parameters.packageName).filter { it.name in config.patches }
|
|
||||||
.associateBy { it.name }
|
.associateBy { it.name }
|
||||||
|
|
||||||
config.options.forEach { (patchName, opts) ->
|
config.options.forEach { (patchName, opts) ->
|
||||||
@@ -95,6 +96,10 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private val longArrayClass = LongArray::class.java
|
||||||
|
private val emptyLongArray = LongArray(0)
|
||||||
|
|
||||||
|
@SuppressLint("PrivateApi")
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun main(args: Array<String>) {
|
fun main(args: Array<String>) {
|
||||||
Looper.prepare()
|
Looper.prepare()
|
||||||
@@ -105,6 +110,15 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
|
|||||||
val systemContext = ActivityThread.systemMain().systemContext as Context
|
val systemContext = ActivityThread.systemMain().systemContext as Context
|
||||||
val appContext = systemContext.createPackageContext(managerPackageName, 0)
|
val appContext = systemContext.createPackageContext(managerPackageName, 0)
|
||||||
|
|
||||||
|
// Avoid annoying logs. See https://github.com/robolectric/robolectric/blob/ad0484c6b32c7d11176c711abeb3cb4a900f9258/robolectric/src/main/java/org/robolectric/android/internal/AndroidTestEnvironment.java#L376-L388
|
||||||
|
Class.forName("android.app.AppCompatCallbacks").apply {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
|
||||||
|
getDeclaredMethod("install", longArrayClass, longArrayClass).also { it.isAccessible = true }(null, emptyLongArray, emptyLongArray)
|
||||||
|
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
getDeclaredMethod("install", longArrayClass).also { it.isAccessible = true }(null, emptyLongArray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val ipcInterface = PatcherProcess(appContext)
|
val ipcInterface = PatcherProcess(appContext)
|
||||||
|
|
||||||
appContext.sendBroadcast(Intent().apply {
|
appContext.sendBroadcast(Intent().apply {
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ import android.os.Parcelable
|
|||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.activity.result.ActivityResult
|
import androidx.activity.result.ActivityResult
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.work.ForegroundInfo
|
import androidx.work.ForegroundInfo
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
|
import app.revanced.manager.MainActivity
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.data.platform.Filesystem
|
import app.revanced.manager.data.platform.Filesystem
|
||||||
import app.revanced.manager.data.room.apps.installed.InstallType
|
import app.revanced.manager.data.room.apps.installed.InstallType
|
||||||
@@ -88,22 +88,25 @@ class PatcherWorker(
|
|||||||
)
|
)
|
||||||
|
|
||||||
private fun createNotification(): Notification {
|
private fun createNotification(): Notification {
|
||||||
val notificationIntent = Intent(applicationContext, PatcherWorker::class.java)
|
val notificationIntent = Intent(applicationContext, MainActivity::class.java).apply {
|
||||||
val pendingIntent: PendingIntent = PendingIntent.getActivity(
|
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||||
|
}
|
||||||
|
val pendingIntent = PendingIntent.getActivity(
|
||||||
applicationContext, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE
|
applicationContext, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE
|
||||||
)
|
)
|
||||||
val channel = NotificationChannel(
|
val channel = NotificationChannel(
|
||||||
"revanced-patcher-patching", "Patching", NotificationManager.IMPORTANCE_HIGH
|
"revanced-patcher-patching", "Patching", NotificationManager.IMPORTANCE_LOW
|
||||||
)
|
)
|
||||||
val notificationManager =
|
val notificationManager =
|
||||||
ContextCompat.getSystemService(applicationContext, NotificationManager::class.java)
|
applicationContext.getSystemService(NotificationManager::class.java)
|
||||||
notificationManager!!.createNotificationChannel(channel)
|
notificationManager.createNotificationChannel(channel)
|
||||||
return Notification.Builder(applicationContext, channel.id)
|
return Notification.Builder(applicationContext, channel.id)
|
||||||
.setContentTitle(applicationContext.getText(R.string.app_name))
|
.setContentTitle(applicationContext.getText(R.string.patcher_notification_title))
|
||||||
.setContentText(applicationContext.getText(R.string.patcher_notification_message))
|
.setContentText(applicationContext.getText(R.string.patcher_notification_text))
|
||||||
.setLargeIcon(Icon.createWithResource(applicationContext, R.drawable.ic_notification))
|
|
||||||
.setSmallIcon(Icon.createWithResource(applicationContext, R.drawable.ic_notification))
|
.setSmallIcon(Icon.createWithResource(applicationContext, R.drawable.ic_notification))
|
||||||
.setContentIntent(pendingIntent).build()
|
.setContentIntent(pendingIntent)
|
||||||
|
.setCategory(Notification.CATEGORY_SERVICE)
|
||||||
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
@@ -158,6 +161,8 @@ class PatcherWorker(
|
|||||||
data,
|
data,
|
||||||
args.packageName,
|
args.packageName,
|
||||||
args.input.version,
|
args.input.version,
|
||||||
|
prefs.suggestedVersionSafeguard.get(),
|
||||||
|
!prefs.disablePatchVersionCompatCheck.get(),
|
||||||
onDownload = args.onDownloadProgress
|
onDownload = args.onDownloadProgress
|
||||||
).also {
|
).also {
|
||||||
args.setInputFile(it)
|
args.setInputFile(it)
|
||||||
|
|||||||
@@ -81,3 +81,41 @@ fun AppTopBar(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun AppTopBar(
|
||||||
|
title: @Composable () -> Unit,
|
||||||
|
onBackClick: (() -> Unit)? = null,
|
||||||
|
backIcon: @Composable (() -> Unit) = @Composable {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(
|
||||||
|
R.string.back
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
actions: @Composable (RowScope.() -> Unit) = {},
|
||||||
|
scrollBehavior: TopAppBarScrollBehavior? = null,
|
||||||
|
applyContainerColor: Boolean = false
|
||||||
|
) {
|
||||||
|
val containerColor = if (applyContainerColor) {
|
||||||
|
MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
|
||||||
|
} else {
|
||||||
|
Color.Unspecified
|
||||||
|
}
|
||||||
|
|
||||||
|
TopAppBar(
|
||||||
|
title = title,
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
navigationIcon = {
|
||||||
|
if (onBackClick != null) {
|
||||||
|
IconButton(onClick = onBackClick) {
|
||||||
|
backIcon()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = actions,
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = containerColor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package app.revanced.manager.ui.component
|
||||||
|
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import app.revanced.manager.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ConfirmDialog(
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onConfirm: () -> Unit,
|
||||||
|
title: String,
|
||||||
|
description: String,
|
||||||
|
icon: ImageVector
|
||||||
|
) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onDismiss) {
|
||||||
|
Text(stringResource(R.string.cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
onConfirm()
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.confirm))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = { Text(title) },
|
||||||
|
icon = { Icon(icon, null) },
|
||||||
|
text = { Text(description) }
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -16,8 +16,6 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.window.Dialog
|
|
||||||
import androidx.compose.ui.window.DialogProperties
|
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.ui.component.bundle.BundleTopBar
|
import app.revanced.manager.ui.component.bundle.BundleTopBar
|
||||||
|
|
||||||
@@ -26,17 +24,13 @@ import app.revanced.manager.ui.component.bundle.BundleTopBar
|
|||||||
fun ExceptionViewerDialog(text: String, onDismiss: () -> Unit) {
|
fun ExceptionViewerDialog(text: String, onDismiss: () -> Unit) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
Dialog(
|
FullscreenDialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
properties = DialogProperties(
|
|
||||||
usePlatformDefaultWidth = false,
|
|
||||||
dismissOnBackPress = true
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
BundleTopBar(
|
BundleTopBar(
|
||||||
title = stringResource(R.string.bundle_error),
|
title = stringResource(R.string.patches_error),
|
||||||
onBackClick = onDismiss,
|
onBackClick = onDismiss,
|
||||||
backIcon = {
|
backIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package app.revanced.manager.ui.component
|
||||||
|
|
||||||
|
import android.view.WindowManager
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.toArgb
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import androidx.compose.ui.window.DialogWindowProvider
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
|
||||||
|
private val properties = DialogProperties(
|
||||||
|
usePlatformDefaultWidth = false,
|
||||||
|
dismissOnBackPress = true,
|
||||||
|
decorFitsSystemWindows = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FullscreenDialog(onDismissRequest: () -> Unit, content: @Composable () -> Unit) {
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
properties = properties
|
||||||
|
) {
|
||||||
|
val view = LocalView.current
|
||||||
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
|
LaunchedEffect(isDarkTheme) {
|
||||||
|
val window = (view.parent as DialogWindowProvider).window
|
||||||
|
window.statusBarColor = Color.Transparent.toArgb()
|
||||||
|
window.navigationBarColor = Color.Transparent.toArgb()
|
||||||
|
window.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)
|
||||||
|
|
||||||
|
val insetsController = WindowCompat.getInsetsController(window, view)
|
||||||
|
insetsController.isAppearanceLightStatusBars = !isDarkTheme
|
||||||
|
insetsController.isAppearanceLightNavigationBars = !isDarkTheme
|
||||||
|
}
|
||||||
|
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
package app.revanced.manager.ui.component.bundle
|
|
||||||
|
|
||||||
import android.webkit.URLUtil
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.automirrored.outlined.ArrowRight
|
|
||||||
import androidx.compose.material.icons.outlined.Extension
|
|
||||||
import androidx.compose.material.icons.outlined.Inventory2
|
|
||||||
import androidx.compose.material.icons.outlined.Sell
|
|
||||||
import androidx.compose.material3.*
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import app.revanced.manager.R
|
|
||||||
import app.revanced.manager.ui.component.ColumnWithScrollbar
|
|
||||||
import app.revanced.manager.ui.component.TextInputDialog
|
|
||||||
import app.revanced.manager.ui.component.haptics.HapticSwitch
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun BaseBundleDialog(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
isDefault: Boolean,
|
|
||||||
name: String?,
|
|
||||||
remoteUrl: String?,
|
|
||||||
onRemoteUrlChange: ((String) -> Unit)? = null,
|
|
||||||
patchCount: Int,
|
|
||||||
version: String?,
|
|
||||||
autoUpdate: Boolean,
|
|
||||||
onAutoUpdateChange: (Boolean) -> Unit,
|
|
||||||
onPatchesClick: () -> Unit,
|
|
||||||
extraFields: @Composable ColumnScope.() -> Unit = {}
|
|
||||||
) {
|
|
||||||
ColumnWithScrollbar(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.then(modifier),
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Outlined.Inventory2,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
|
||||||
modifier = Modifier.size(32.dp)
|
|
||||||
)
|
|
||||||
name?.let {
|
|
||||||
Text(
|
|
||||||
text = it,
|
|
||||||
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight(800)),
|
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(start = 2.dp)
|
|
||||||
) {
|
|
||||||
version?.let {
|
|
||||||
Tag(Icons.Outlined.Sell, it)
|
|
||||||
}
|
|
||||||
Tag(Icons.Outlined.Extension, patchCount.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HorizontalDivider(
|
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
|
||||||
color = MaterialTheme.colorScheme.outlineVariant
|
|
||||||
)
|
|
||||||
|
|
||||||
if (remoteUrl != null) {
|
|
||||||
BundleListItem(
|
|
||||||
headlineText = stringResource(R.string.bundle_auto_update),
|
|
||||||
supportingText = stringResource(R.string.bundle_auto_update_description),
|
|
||||||
trailingContent = {
|
|
||||||
HapticSwitch(
|
|
||||||
checked = autoUpdate,
|
|
||||||
onCheckedChange = onAutoUpdateChange
|
|
||||||
)
|
|
||||||
},
|
|
||||||
modifier = Modifier.clickable {
|
|
||||||
onAutoUpdateChange(!autoUpdate)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
remoteUrl?.takeUnless { isDefault }?.let { url ->
|
|
||||||
var showUrlInputDialog by rememberSaveable {
|
|
||||||
mutableStateOf(false)
|
|
||||||
}
|
|
||||||
if (showUrlInputDialog) {
|
|
||||||
TextInputDialog(
|
|
||||||
initial = url,
|
|
||||||
title = stringResource(R.string.bundle_input_source_url),
|
|
||||||
onDismissRequest = { showUrlInputDialog = false },
|
|
||||||
onConfirm = {
|
|
||||||
showUrlInputDialog = false
|
|
||||||
onRemoteUrlChange?.invoke(it)
|
|
||||||
},
|
|
||||||
validator = {
|
|
||||||
if (it.isEmpty()) return@TextInputDialog false
|
|
||||||
|
|
||||||
URLUtil.isValidUrl(it)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
BundleListItem(
|
|
||||||
modifier = Modifier.clickable(
|
|
||||||
enabled = onRemoteUrlChange != null,
|
|
||||||
onClick = {
|
|
||||||
showUrlInputDialog = true
|
|
||||||
}
|
|
||||||
),
|
|
||||||
headlineText = stringResource(R.string.bundle_input_source_url),
|
|
||||||
supportingText = url.ifEmpty {
|
|
||||||
stringResource(R.string.field_not_set)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val patchesClickable = patchCount > 0
|
|
||||||
BundleListItem(
|
|
||||||
headlineText = stringResource(R.string.patches),
|
|
||||||
supportingText = stringResource(R.string.bundle_view_patches),
|
|
||||||
modifier = Modifier.clickable(
|
|
||||||
enabled = patchesClickable,
|
|
||||||
onClick = onPatchesClick
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
if (patchesClickable) {
|
|
||||||
Icon(
|
|
||||||
Icons.AutoMirrored.Outlined.ArrowRight,
|
|
||||||
stringResource(R.string.patches)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extraFields()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun Tag(
|
|
||||||
icon: ImageVector,
|
|
||||||
text: String
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = icon,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(16.dp),
|
|
||||||
tint = MaterialTheme.colorScheme.outline,
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.outline,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,70 +1,99 @@
|
|||||||
package app.revanced.manager.ui.component.bundle
|
package app.revanced.manager.ui.component.bundle
|
||||||
|
|
||||||
|
import android.webkit.URLUtil.isValidUrl
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.automirrored.outlined.ArrowRight
|
import androidx.compose.material.icons.automirrored.outlined.ArrowRight
|
||||||
|
import androidx.compose.material.icons.automirrored.outlined.Send
|
||||||
|
import androidx.compose.material.icons.outlined.Commit
|
||||||
import androidx.compose.material.icons.outlined.DeleteOutline
|
import androidx.compose.material.icons.outlined.DeleteOutline
|
||||||
|
import androidx.compose.material.icons.outlined.Description
|
||||||
|
import androidx.compose.material.icons.outlined.Gavel
|
||||||
|
import androidx.compose.material.icons.outlined.Language
|
||||||
|
import androidx.compose.material.icons.outlined.Person
|
||||||
|
import androidx.compose.material.icons.outlined.Sell
|
||||||
import androidx.compose.material.icons.outlined.Update
|
import androidx.compose.material.icons.outlined.Update
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.DialogProperties
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
|
import app.revanced.manager.R.string.auto_update
|
||||||
|
import app.revanced.manager.R.string.auto_update_description
|
||||||
|
import app.revanced.manager.R.string.field_not_set
|
||||||
|
import app.revanced.manager.R.string.patches
|
||||||
|
import app.revanced.manager.R.string.patches_url
|
||||||
|
import app.revanced.manager.R.string.view_patches
|
||||||
|
import app.revanced.manager.data.platform.NetworkInfo
|
||||||
import app.revanced.manager.domain.bundles.LocalPatchBundle
|
import app.revanced.manager.domain.bundles.LocalPatchBundle
|
||||||
import app.revanced.manager.domain.bundles.PatchBundleSource
|
import app.revanced.manager.domain.bundles.PatchBundleSource
|
||||||
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull
|
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull
|
||||||
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.isDefault
|
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.isDefault
|
||||||
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState
|
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||||
|
import app.revanced.manager.ui.component.ColumnWithScrollbar
|
||||||
import app.revanced.manager.ui.component.ExceptionViewerDialog
|
import app.revanced.manager.ui.component.ExceptionViewerDialog
|
||||||
|
import app.revanced.manager.ui.component.FullscreenDialog
|
||||||
|
import app.revanced.manager.ui.component.TextInputDialog
|
||||||
|
import app.revanced.manager.ui.component.haptics.HapticSwitch
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koin.compose.koinInject
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun BundleInformationDialog(
|
fun BundleInformationDialog(
|
||||||
|
src: PatchBundleSource,
|
||||||
|
patchCount: Int,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onDeleteRequest: () -> Unit,
|
onDeleteRequest: () -> Unit,
|
||||||
bundle: PatchBundleSource,
|
|
||||||
onUpdate: () -> Unit,
|
onUpdate: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
val bundleRepo = koinInject<PatchBundleRepository>()
|
||||||
|
val networkInfo = koinInject<NetworkInfo>()
|
||||||
|
val hasNetwork = remember { networkInfo.isConnected() }
|
||||||
val composableScope = rememberCoroutineScope()
|
val composableScope = rememberCoroutineScope()
|
||||||
var viewCurrentBundlePatches by remember { mutableStateOf(false) }
|
var viewCurrentBundlePatches by remember { mutableStateOf(false) }
|
||||||
val isLocal = bundle is LocalPatchBundle
|
val isLocal = src is LocalPatchBundle
|
||||||
val state by bundle.state.collectAsStateWithLifecycle()
|
val bundleManifestAttributes = src.patchBundle?.manifestAttributes
|
||||||
val props by remember(bundle) {
|
val (autoUpdate, endpoint) = src.asRemoteOrNull?.let { it.autoUpdate to it.endpoint } ?: (null to null)
|
||||||
bundle.propsFlow()
|
|
||||||
}.collectAsStateWithLifecycle(null)
|
fun onAutoUpdateChange(new: Boolean) = composableScope.launch {
|
||||||
val patchCount = remember(state) {
|
with(bundleRepo) {
|
||||||
state.patchBundleOrNull()?.patches?.size ?: 0
|
src.asRemoteOrNull?.setAutoUpdate(new)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (viewCurrentBundlePatches) {
|
if (viewCurrentBundlePatches) {
|
||||||
BundlePatchesDialog(
|
BundlePatchesDialog(
|
||||||
|
src = src,
|
||||||
onDismissRequest = {
|
onDismissRequest = {
|
||||||
viewCurrentBundlePatches = false
|
viewCurrentBundlePatches = false
|
||||||
},
|
}
|
||||||
bundle = bundle,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Dialog(
|
FullscreenDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
properties = DialogProperties(
|
|
||||||
usePlatformDefaultWidth = false,
|
|
||||||
dismissOnBackPress = true
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
val bundleName by bundle.nameState
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
BundleTopBar(
|
BundleTopBar(
|
||||||
title = stringResource(R.string.patch_bundle_field),
|
title = src.name,
|
||||||
onBackClick = onDismissRequest,
|
onBackClick = onDismissRequest,
|
||||||
backIcon = {
|
backIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -73,7 +102,7 @@ fun BundleInformationDialog(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
if (!bundle.isDefault) {
|
if (!src.isDefault) {
|
||||||
IconButton(onClick = onDeleteRequest) {
|
IconButton(onClick = onDeleteRequest) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Outlined.DeleteOutline,
|
Icons.Outlined.DeleteOutline,
|
||||||
@@ -81,7 +110,7 @@ fun BundleInformationDialog(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!isLocal) {
|
if (!isLocal && hasNetwork) {
|
||||||
IconButton(onClick = onUpdate) {
|
IconButton(onClick = onUpdate) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Outlined.Update,
|
Icons.Outlined.Update,
|
||||||
@@ -93,54 +122,175 @@ fun BundleInformationDialog(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
BaseBundleDialog(
|
ColumnWithScrollbar(
|
||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier
|
||||||
isDefault = bundle.isDefault,
|
.fillMaxWidth()
|
||||||
name = bundleName,
|
.padding(paddingValues),
|
||||||
remoteUrl = bundle.asRemoteOrNull?.endpoint,
|
) {
|
||||||
patchCount = patchCount,
|
Column(
|
||||||
version = props?.version,
|
modifier = Modifier.padding(16.dp),
|
||||||
autoUpdate = props?.autoUpdate ?: false,
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
onAutoUpdateChange = {
|
) {
|
||||||
composableScope.launch {
|
Tag(Icons.Outlined.Sell, src.name)
|
||||||
bundle.asRemoteOrNull?.setAutoUpdate(it)
|
bundleManifestAttributes?.description?.let {
|
||||||
|
Tag(Icons.Outlined.Description, it)
|
||||||
}
|
}
|
||||||
},
|
bundleManifestAttributes?.source?.let {
|
||||||
onPatchesClick = {
|
Tag(Icons.Outlined.Commit, it)
|
||||||
viewCurrentBundlePatches = true
|
}
|
||||||
},
|
bundleManifestAttributes?.author?.let {
|
||||||
extraFields = {
|
Tag(Icons.Outlined.Person, it)
|
||||||
(state as? PatchBundleSource.State.Failed)?.throwable?.let {
|
}
|
||||||
var showDialog by rememberSaveable {
|
bundleManifestAttributes?.contact?.let {
|
||||||
mutableStateOf(false)
|
Tag(Icons.AutoMirrored.Outlined.Send, it)
|
||||||
|
}
|
||||||
|
bundleManifestAttributes?.website?.let {
|
||||||
|
Tag(Icons.Outlined.Language, it, isUrl = true)
|
||||||
|
}
|
||||||
|
bundleManifestAttributes?.license?.let {
|
||||||
|
Tag(Icons.Outlined.Gavel, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
color = MaterialTheme.colorScheme.outlineVariant
|
||||||
|
)
|
||||||
|
|
||||||
|
if (autoUpdate != null) {
|
||||||
|
BundleListItem(
|
||||||
|
headlineText = stringResource(auto_update),
|
||||||
|
supportingText = stringResource(auto_update_description),
|
||||||
|
trailingContent = {
|
||||||
|
HapticSwitch(
|
||||||
|
checked = autoUpdate,
|
||||||
|
onCheckedChange = ::onAutoUpdateChange
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
onAutoUpdateChange(!autoUpdate)
|
||||||
}
|
}
|
||||||
if (showDialog) ExceptionViewerDialog(
|
)
|
||||||
onDismiss = { showDialog = false },
|
}
|
||||||
text = remember(it) { it.stackTraceToString() }
|
|
||||||
)
|
|
||||||
|
|
||||||
BundleListItem(
|
endpoint?.takeUnless { src.isDefault }?.let { url ->
|
||||||
headlineText = stringResource(R.string.bundle_error),
|
var showUrlInputDialog by rememberSaveable {
|
||||||
supportingText = stringResource(R.string.bundle_error_description),
|
mutableStateOf(false)
|
||||||
trailingContent = {
|
}
|
||||||
Icon(
|
if (showUrlInputDialog) {
|
||||||
Icons.AutoMirrored.Outlined.ArrowRight,
|
TextInputDialog(
|
||||||
null
|
initial = url,
|
||||||
)
|
title = stringResource(patches_url),
|
||||||
|
onDismissRequest = { showUrlInputDialog = false },
|
||||||
|
onConfirm = {
|
||||||
|
showUrlInputDialog = false
|
||||||
|
TODO("Not implemented.")
|
||||||
},
|
},
|
||||||
modifier = Modifier.clickable { showDialog = true }
|
validator = {
|
||||||
|
if (it.isEmpty()) return@TextInputDialog false
|
||||||
|
|
||||||
|
isValidUrl(it)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state is PatchBundleSource.State.Missing && !isLocal) {
|
BundleListItem(
|
||||||
BundleListItem(
|
modifier = Modifier.clickable(
|
||||||
headlineText = stringResource(R.string.bundle_error),
|
enabled = false,
|
||||||
supportingText = stringResource(R.string.bundle_not_downloaded),
|
onClick = {
|
||||||
modifier = Modifier.clickable(onClick = onUpdate)
|
showUrlInputDialog = true
|
||||||
|
}
|
||||||
|
),
|
||||||
|
headlineText = stringResource(patches_url),
|
||||||
|
supportingText = url.ifEmpty {
|
||||||
|
stringResource(field_not_set)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val patchesClickable = patchCount > 0
|
||||||
|
BundleListItem(
|
||||||
|
headlineText = stringResource(patches),
|
||||||
|
supportingText = stringResource(view_patches),
|
||||||
|
modifier = Modifier.clickable(
|
||||||
|
enabled = patchesClickable,
|
||||||
|
onClick = {
|
||||||
|
viewCurrentBundlePatches = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
if (patchesClickable) {
|
||||||
|
Icon(
|
||||||
|
Icons.AutoMirrored.Outlined.ArrowRight,
|
||||||
|
stringResource(patches)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
src.error?.let {
|
||||||
|
var showDialog by rememberSaveable {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
if (showDialog) ExceptionViewerDialog(
|
||||||
|
onDismiss = { showDialog = false },
|
||||||
|
text = remember(it) { it.stackTraceToString() }
|
||||||
|
)
|
||||||
|
|
||||||
|
BundleListItem(
|
||||||
|
headlineText = stringResource(R.string.patches_error),
|
||||||
|
supportingText = stringResource(R.string.patches_error_description),
|
||||||
|
trailingContent = {
|
||||||
|
Icon(
|
||||||
|
Icons.AutoMirrored.Outlined.ArrowRight,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier.clickable { showDialog = true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (src.state is PatchBundleSource.State.Missing && !isLocal) {
|
||||||
|
BundleListItem(
|
||||||
|
headlineText = stringResource(R.string.patches_error),
|
||||||
|
supportingText = stringResource(R.string.patches_not_downloaded),
|
||||||
|
modifier = Modifier.clickable(onClick = onUpdate)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Tag(
|
||||||
|
icon: ImageVector,
|
||||||
|
text: String,
|
||||||
|
isUrl: Boolean = false
|
||||||
|
) {
|
||||||
|
val uriHandler = LocalUriHandler.current
|
||||||
|
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = if (isUrl) {
|
||||||
|
Modifier
|
||||||
|
.clickable {
|
||||||
|
try {
|
||||||
|
uriHandler.openUri(text)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else
|
||||||
|
Modifier,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = if (isUrl) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Delete
|
||||||
import androidx.compose.material.icons.outlined.ErrorOutline
|
import androidx.compose.material.icons.outlined.ErrorOutline
|
||||||
import androidx.compose.material.icons.outlined.Warning
|
import androidx.compose.material.icons.outlined.Warning
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
@@ -23,44 +24,49 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.res.pluralStringResource
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.domain.bundles.PatchBundleSource
|
import app.revanced.manager.domain.bundles.PatchBundleSource
|
||||||
|
import app.revanced.manager.ui.component.ConfirmDialog
|
||||||
import app.revanced.manager.ui.component.haptics.HapticCheckbox
|
import app.revanced.manager.ui.component.haptics.HapticCheckbox
|
||||||
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun BundleItem(
|
fun BundleItem(
|
||||||
bundle: PatchBundleSource,
|
src: PatchBundleSource,
|
||||||
onDelete: () -> Unit,
|
patchCount: Int,
|
||||||
onUpdate: () -> Unit,
|
|
||||||
selectable: Boolean,
|
selectable: Boolean,
|
||||||
onSelect: () -> Unit,
|
|
||||||
isBundleSelected: Boolean,
|
isBundleSelected: Boolean,
|
||||||
toggleSelection: (Boolean) -> Unit,
|
toggleSelection: (Boolean) -> Unit,
|
||||||
|
onSelect: () -> Unit,
|
||||||
|
onDelete: () -> Unit,
|
||||||
|
onUpdate: () -> Unit,
|
||||||
) {
|
) {
|
||||||
var viewBundleDialogPage by rememberSaveable { mutableStateOf(false) }
|
var viewBundleDialogPage by rememberSaveable { mutableStateOf(false) }
|
||||||
val state by bundle.state.collectAsStateWithLifecycle()
|
var showDeleteConfirmationDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
val version by remember(bundle) {
|
|
||||||
bundle.propsFlow().map { props -> props?.version }
|
|
||||||
}.collectAsStateWithLifecycle(null)
|
|
||||||
val name by bundle.nameState
|
|
||||||
|
|
||||||
if (viewBundleDialogPage) {
|
if (viewBundleDialogPage) {
|
||||||
BundleInformationDialog(
|
BundleInformationDialog(
|
||||||
|
src = src,
|
||||||
|
patchCount = patchCount,
|
||||||
onDismissRequest = { viewBundleDialogPage = false },
|
onDismissRequest = { viewBundleDialogPage = false },
|
||||||
onDeleteRequest = {
|
onDeleteRequest = { showDeleteConfirmationDialog = true },
|
||||||
viewBundleDialogPage = false
|
|
||||||
onDelete()
|
|
||||||
},
|
|
||||||
bundle = bundle,
|
|
||||||
onUpdate = onUpdate,
|
onUpdate = onUpdate,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showDeleteConfirmationDialog) {
|
||||||
|
ConfirmDialog(
|
||||||
|
onDismiss = { showDeleteConfirmationDialog = false },
|
||||||
|
onConfirm = {
|
||||||
|
onDelete()
|
||||||
|
viewBundleDialogPage = false
|
||||||
|
},
|
||||||
|
title = stringResource(R.string.delete),
|
||||||
|
description = stringResource(R.string.patches_delete_single_dialog_description, src.name),
|
||||||
|
icon = Icons.Outlined.Delete
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
ListItem(
|
ListItem(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.height(64.dp)
|
.height(64.dp)
|
||||||
@@ -78,19 +84,19 @@ fun BundleItem(
|
|||||||
}
|
}
|
||||||
} else null,
|
} else null,
|
||||||
|
|
||||||
headlineContent = { Text(name) },
|
headlineContent = { Text(src.name) },
|
||||||
supportingContent = {
|
supportingContent = {
|
||||||
state.patchBundleOrNull()?.patches?.size?.let { patchCount ->
|
if (src.state is PatchBundleSource.State.Available) {
|
||||||
Text(pluralStringResource(R.plurals.patch_count, patchCount, patchCount))
|
Text(pluralStringResource(R.plurals.patch_count, patchCount, patchCount))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
Row {
|
Row {
|
||||||
val icon = remember(state) {
|
val icon = remember(src.state) {
|
||||||
when (state) {
|
when (src.state) {
|
||||||
is PatchBundleSource.State.Failed -> Icons.Outlined.ErrorOutline to R.string.bundle_error
|
is PatchBundleSource.State.Failed -> Icons.Outlined.ErrorOutline to R.string.patches_error
|
||||||
is PatchBundleSource.State.Missing -> Icons.Outlined.Warning to R.string.bundle_missing
|
is PatchBundleSource.State.Missing -> Icons.Outlined.Warning to R.string.patches_missing
|
||||||
is PatchBundleSource.State.Loaded -> null
|
is PatchBundleSource.State.Available -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +109,7 @@ fun BundleItem(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
version?.let { Text(text = it) }
|
src.version?.let { Text(text = it) }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import androidx.compose.material3.*
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -22,36 +23,37 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog
|
|
||||||
import androidx.compose.ui.window.DialogProperties
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.domain.bundles.PatchBundleSource
|
import app.revanced.manager.domain.bundles.PatchBundleSource
|
||||||
|
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||||
import app.revanced.manager.patcher.patch.PatchInfo
|
import app.revanced.manager.patcher.patch.PatchInfo
|
||||||
import app.revanced.manager.ui.component.ArrowButton
|
import app.revanced.manager.ui.component.ArrowButton
|
||||||
|
import app.revanced.manager.ui.component.FullscreenDialog
|
||||||
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
|
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
|
||||||
|
import kotlinx.coroutines.flow.mapNotNull
|
||||||
|
import org.koin.compose.koinInject
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun BundlePatchesDialog(
|
fun BundlePatchesDialog(
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
bundle: PatchBundleSource,
|
src: PatchBundleSource,
|
||||||
) {
|
) {
|
||||||
var showAllVersions by rememberSaveable { mutableStateOf(false) }
|
var showAllVersions by rememberSaveable { mutableStateOf(false) }
|
||||||
var showOptions by rememberSaveable { mutableStateOf(false) }
|
var showOptions by rememberSaveable { mutableStateOf(false) }
|
||||||
val state by bundle.state.collectAsStateWithLifecycle()
|
val patchBundleRepository: PatchBundleRepository = koinInject()
|
||||||
|
val patches by remember(src.uid) {
|
||||||
|
patchBundleRepository.bundleInfoFlow.mapNotNull { it[src.uid]?.patches }
|
||||||
|
}.collectAsStateWithLifecycle(emptyList())
|
||||||
|
|
||||||
Dialog(
|
FullscreenDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
properties = DialogProperties(
|
|
||||||
usePlatformDefaultWidth = false,
|
|
||||||
dismissOnBackPress = true
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
BundleTopBar(
|
BundleTopBar(
|
||||||
title = stringResource(R.string.bundle_patches),
|
title = stringResource(R.string.patches),
|
||||||
onBackClick = onDismissRequest,
|
onBackClick = onDismissRequest,
|
||||||
backIcon = {
|
backIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -69,16 +71,14 @@ fun BundlePatchesDialog(
|
|||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
contentPadding = PaddingValues(16.dp)
|
contentPadding = PaddingValues(16.dp)
|
||||||
) {
|
) {
|
||||||
state.patchBundleOrNull()?.let { bundle ->
|
items(patches) { patch ->
|
||||||
items(bundle.patches) { patch ->
|
PatchItem(
|
||||||
PatchItem(
|
patch,
|
||||||
patch,
|
showAllVersions,
|
||||||
showAllVersions,
|
onExpandVersions = { showAllVersions = !showAllVersions },
|
||||||
onExpandVersions = { showAllVersions = !showAllVersions },
|
showOptions,
|
||||||
showOptions,
|
onExpandOptions = { showOptions = !showOptions }
|
||||||
onExpandOptions = { showOptions = !showOptions }
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,10 +138,10 @@ fun PatchItem(
|
|||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
PatchInfoChip(
|
PatchInfoChip(
|
||||||
text = "$PACKAGE_ICON ${stringResource(R.string.bundle_view_patches_any_package)}"
|
text = "$PACKAGE_ICON ${stringResource(R.string.patches_view_any_package)}"
|
||||||
)
|
)
|
||||||
PatchInfoChip(
|
PatchInfoChip(
|
||||||
text = "$VERSION_ICON ${stringResource(R.string.bundle_view_patches_any_version)}"
|
text = "$VERSION_ICON ${stringResource(R.string.patches_view_any_version)}"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -12,23 +12,23 @@ import androidx.compose.material3.ModalBottomSheet
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.domain.bundles.PatchBundleSource
|
import app.revanced.manager.domain.bundles.PatchBundleSource
|
||||||
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun BundleSelector(bundles: List<PatchBundleSource>, onFinish: (PatchBundleSource?) -> Unit) {
|
fun BundleSelector(sources: List<PatchBundleSource>, onFinish: (PatchBundleSource?) -> Unit) {
|
||||||
LaunchedEffect(bundles) {
|
LaunchedEffect(sources) {
|
||||||
if (bundles.size == 1) {
|
if (sources.size == 1) {
|
||||||
onFinish(bundles[0])
|
onFinish(sources[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bundles.size < 2) {
|
if (sources.size < 2) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,13 +47,12 @@ fun BundleSelector(bundles: List<PatchBundleSource>, onFinish: (PatchBundleSourc
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Select bundle",
|
text = stringResource(R.string.select),
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
bundles.forEach {
|
sources.forEach {
|
||||||
val name by it.nameState
|
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.Center,
|
horizontalArrangement = Arrangement.Center,
|
||||||
@@ -65,7 +64,7 @@ fun BundleSelector(bundles: List<PatchBundleSource>, onFinish: (PatchBundleSourc
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
name,
|
"${it.name} ${it.version}",
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -23,10 +23,14 @@ import app.revanced.manager.ui.component.AlertDialogExtended
|
|||||||
import app.revanced.manager.ui.component.TextHorizontalPadding
|
import app.revanced.manager.ui.component.TextHorizontalPadding
|
||||||
import app.revanced.manager.ui.component.haptics.HapticCheckbox
|
import app.revanced.manager.ui.component.haptics.HapticCheckbox
|
||||||
import app.revanced.manager.ui.component.haptics.HapticRadioButton
|
import app.revanced.manager.ui.component.haptics.HapticRadioButton
|
||||||
import app.revanced.manager.ui.model.BundleType
|
|
||||||
import app.revanced.manager.util.BIN_MIMETYPE
|
import app.revanced.manager.util.BIN_MIMETYPE
|
||||||
import app.revanced.manager.util.transparentListItemColors
|
import app.revanced.manager.util.transparentListItemColors
|
||||||
|
|
||||||
|
private enum class BundleType {
|
||||||
|
Local,
|
||||||
|
Remote
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ImportPatchBundleDialog(
|
fun ImportPatchBundleDialog(
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
@@ -37,7 +41,7 @@ fun ImportPatchBundleDialog(
|
|||||||
var bundleType by rememberSaveable { mutableStateOf(BundleType.Remote) }
|
var bundleType by rememberSaveable { mutableStateOf(BundleType.Remote) }
|
||||||
var patchBundle by rememberSaveable { mutableStateOf<Uri?>(null) }
|
var patchBundle by rememberSaveable { mutableStateOf<Uri?>(null) }
|
||||||
var remoteUrl by rememberSaveable { mutableStateOf("") }
|
var remoteUrl by rememberSaveable { mutableStateOf("") }
|
||||||
var autoUpdate by rememberSaveable { mutableStateOf(false) }
|
var autoUpdate by rememberSaveable { mutableStateOf(true) }
|
||||||
|
|
||||||
val patchActivityLauncher =
|
val patchActivityLauncher =
|
||||||
rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
|
rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
|
||||||
@@ -77,7 +81,7 @@ fun ImportPatchBundleDialog(
|
|||||||
AlertDialogExtended(
|
AlertDialogExtended(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
title = {
|
title = {
|
||||||
Text(stringResource(if (currentStep == 0) R.string.select else R.string.add_patch_bundle))
|
Text(stringResource(if (currentStep == 0) R.string.select else R.string.add_patches))
|
||||||
},
|
},
|
||||||
text = {
|
text = {
|
||||||
steps[currentStep]()
|
steps[currentStep]()
|
||||||
@@ -117,7 +121,7 @@ fun ImportPatchBundleDialog(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SelectBundleTypeStep(
|
private fun SelectBundleTypeStep(
|
||||||
bundleType: BundleType,
|
bundleType: BundleType,
|
||||||
onBundleTypeSelected: (BundleType) -> Unit
|
onBundleTypeSelected: (BundleType) -> Unit
|
||||||
) {
|
) {
|
||||||
@@ -126,7 +130,7 @@ fun SelectBundleTypeStep(
|
|||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.padding(horizontal = 24.dp),
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
text = stringResource(R.string.select_bundle_type_dialog_description)
|
text = stringResource(R.string.select_patches_type_dialog_description)
|
||||||
)
|
)
|
||||||
Column {
|
Column {
|
||||||
ListItem(
|
ListItem(
|
||||||
@@ -136,7 +140,7 @@ fun SelectBundleTypeStep(
|
|||||||
),
|
),
|
||||||
headlineContent = { Text(stringResource(R.string.enter_url)) },
|
headlineContent = { Text(stringResource(R.string.enter_url)) },
|
||||||
overlineContent = { Text(stringResource(R.string.recommended)) },
|
overlineContent = { Text(stringResource(R.string.recommended)) },
|
||||||
supportingContent = { Text(stringResource(R.string.remote_bundle_description)) },
|
supportingContent = { Text(stringResource(R.string.remote_patches_description)) },
|
||||||
leadingContent = {
|
leadingContent = {
|
||||||
HapticRadioButton(
|
HapticRadioButton(
|
||||||
selected = bundleType == BundleType.Remote,
|
selected = bundleType == BundleType.Remote,
|
||||||
@@ -152,7 +156,7 @@ fun SelectBundleTypeStep(
|
|||||||
onClick = { onBundleTypeSelected(BundleType.Local) }
|
onClick = { onBundleTypeSelected(BundleType.Local) }
|
||||||
),
|
),
|
||||||
headlineContent = { Text(stringResource(R.string.select_from_storage)) },
|
headlineContent = { Text(stringResource(R.string.select_from_storage)) },
|
||||||
supportingContent = { Text(stringResource(R.string.local_bundle_description)) },
|
supportingContent = { Text(stringResource(R.string.local_patches_description)) },
|
||||||
overlineContent = { },
|
overlineContent = { },
|
||||||
leadingContent = {
|
leadingContent = {
|
||||||
HapticRadioButton(
|
HapticRadioButton(
|
||||||
@@ -168,7 +172,7 @@ fun SelectBundleTypeStep(
|
|||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ImportBundleStep(
|
private fun ImportBundleStep(
|
||||||
bundleType: BundleType,
|
bundleType: BundleType,
|
||||||
patchBundle: Uri?,
|
patchBundle: Uri?,
|
||||||
remoteUrl: String,
|
remoteUrl: String,
|
||||||
@@ -185,7 +189,7 @@ fun ImportBundleStep(
|
|||||||
) {
|
) {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
Text(stringResource(R.string.patch_bundle_field))
|
Text(stringResource(R.string.patches))
|
||||||
},
|
},
|
||||||
supportingContent = { Text(stringResource(if (patchBundle != null) R.string.file_field_set else R.string.file_field_not_set)) },
|
supportingContent = { Text(stringResource(if (patchBundle != null) R.string.file_field_set else R.string.file_field_not_set)) },
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
@@ -206,11 +210,11 @@ fun ImportBundleStep(
|
|||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = remoteUrl,
|
value = remoteUrl,
|
||||||
onValueChange = onRemoteUrlChange,
|
onValueChange = onRemoteUrlChange,
|
||||||
label = { Text(stringResource(R.string.bundle_url)) }
|
label = { Text(stringResource(R.string.patches_url)) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(horizontal = 8.dp)
|
modifier = Modifier.padding(horizontal = 8.dp, vertical = 5.dp)
|
||||||
) {
|
) {
|
||||||
ListItem(
|
ListItem(
|
||||||
modifier = Modifier.clickable(
|
modifier = Modifier.clickable(
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import androidx.compose.material3.SwitchDefaults
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HapticSwitch(
|
fun HapticSwitch(
|
||||||
@@ -20,16 +21,19 @@ fun HapticSwitch(
|
|||||||
colors: SwitchColors = SwitchDefaults.colors(),
|
colors: SwitchColors = SwitchDefaults.colors(),
|
||||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
) {
|
) {
|
||||||
|
val view = LocalView.current
|
||||||
Switch(
|
Switch(
|
||||||
checked = checked,
|
checked = checked,
|
||||||
onCheckedChange = { newChecked ->
|
onCheckedChange = { newChecked ->
|
||||||
val useNewConstants = Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
|
val useNewConstants = Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
|
||||||
when {
|
val hapticFeedbackType = when {
|
||||||
newChecked && useNewConstants -> HapticFeedbackConstants.TOGGLE_ON
|
newChecked && useNewConstants -> HapticFeedbackConstants.TOGGLE_ON
|
||||||
newChecked -> HapticFeedbackConstants.VIRTUAL_KEY
|
newChecked -> HapticFeedbackConstants.VIRTUAL_KEY
|
||||||
!newChecked && useNewConstants -> HapticFeedbackConstants.TOGGLE_OFF
|
!newChecked && useNewConstants -> HapticFeedbackConstants.TOGGLE_OFF
|
||||||
!newChecked -> HapticFeedbackConstants.CLOCK_TICK
|
!newChecked -> HapticFeedbackConstants.CLOCK_TICK
|
||||||
|
else -> {HapticFeedbackConstants.VIRTUAL_KEY}
|
||||||
}
|
}
|
||||||
|
view.performHapticFeedback(hapticFeedbackType)
|
||||||
onCheckedChange(newChecked)
|
onCheckedChange(newChecked)
|
||||||
},
|
},
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
|||||||
@@ -21,8 +21,25 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.DragHandle
|
import androidx.compose.material.icons.filled.DragHandle
|
||||||
import androidx.compose.material.icons.outlined.*
|
import androidx.compose.material.icons.outlined.Add
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material.icons.outlined.Delete
|
||||||
|
import androidx.compose.material.icons.outlined.Edit
|
||||||
|
import androidx.compose.material.icons.outlined.Folder
|
||||||
|
import androidx.compose.material.icons.outlined.MoreVert
|
||||||
|
import androidx.compose.material.icons.outlined.Restore
|
||||||
|
import androidx.compose.material.icons.outlined.SelectAll
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisallowComposableCalls
|
import androidx.compose.runtime.DisallowComposableCalls
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
@@ -38,11 +55,15 @@ import androidx.compose.ui.res.pluralStringResource
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.DialogProperties
|
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.data.platform.Filesystem
|
import app.revanced.manager.data.platform.Filesystem
|
||||||
import app.revanced.manager.patcher.patch.Option
|
import app.revanced.manager.patcher.patch.Option
|
||||||
import app.revanced.manager.ui.component.*
|
import app.revanced.manager.ui.component.AlertDialogExtended
|
||||||
|
import app.revanced.manager.ui.component.AppTopBar
|
||||||
|
import app.revanced.manager.ui.component.FloatInputDialog
|
||||||
|
import app.revanced.manager.ui.component.FullscreenDialog
|
||||||
|
import app.revanced.manager.ui.component.IntInputDialog
|
||||||
|
import app.revanced.manager.ui.component.LongInputDialog
|
||||||
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
||||||
import app.revanced.manager.ui.component.haptics.HapticRadioButton
|
import app.revanced.manager.ui.component.haptics.HapticRadioButton
|
||||||
import app.revanced.manager.ui.component.haptics.HapticSwitch
|
import app.revanced.manager.ui.component.haptics.HapticSwitch
|
||||||
@@ -57,26 +78,38 @@ import org.koin.compose.koinInject
|
|||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.get
|
import org.koin.core.component.get
|
||||||
import sh.calvin.reorderable.ReorderableItem
|
import sh.calvin.reorderable.ReorderableItem
|
||||||
import sh.calvin.reorderable.rememberReorderableLazyColumnState
|
import sh.calvin.reorderable.rememberReorderableLazyListState
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
import kotlin.reflect.typeOf
|
import kotlin.reflect.typeOf
|
||||||
import androidx.compose.ui.window.Dialog as ComposeDialog
|
|
||||||
|
|
||||||
private class OptionEditorScope<T : Any>(
|
private class OptionEditorScope<T : Any>(
|
||||||
private val editor: OptionEditor<T>,
|
private val editor: OptionEditor<T>,
|
||||||
val option: Option<T>,
|
val option: Option<T>,
|
||||||
val openDialog: () -> Unit,
|
val openDialog: () -> Unit,
|
||||||
val dismissDialog: () -> Unit,
|
val dismissDialog: () -> Unit,
|
||||||
|
val selectionWarningEnabled: Boolean,
|
||||||
|
val showSelectionWarning: () -> Unit,
|
||||||
val value: T?,
|
val value: T?,
|
||||||
val setValue: (T?) -> Unit,
|
val setValue: (T?) -> Unit
|
||||||
) {
|
) {
|
||||||
fun submitDialog(value: T?) {
|
fun submitDialog(value: T?) {
|
||||||
setValue(value)
|
setValue(value)
|
||||||
dismissDialog()
|
dismissDialog()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clickAction() = editor.clickAction(this)
|
fun checkSafeguard(block: () -> Unit) {
|
||||||
|
if (!option.required && selectionWarningEnabled)
|
||||||
|
showSelectionWarning()
|
||||||
|
else
|
||||||
|
block()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clickAction() {
|
||||||
|
checkSafeguard {
|
||||||
|
editor.clickAction(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ListItemTrailingContent() = editor.ListItemTrailingContent(this)
|
fun ListItemTrailingContent() = editor.ListItemTrailingContent(this)
|
||||||
@@ -90,7 +123,7 @@ private interface OptionEditor<T : Any> {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ListItemTrailingContent(scope: OptionEditorScope<T>) {
|
fun ListItemTrailingContent(scope: OptionEditorScope<T>) {
|
||||||
IconButton(onClick = { clickAction(scope) }) {
|
IconButton(onClick = { scope.checkSafeguard { clickAction(scope) } }) {
|
||||||
Icon(Icons.Outlined.Edit, stringResource(R.string.edit))
|
Icon(Icons.Outlined.Edit, stringResource(R.string.edit))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,11 +151,14 @@ private inline fun <T : Any> WithOptionEditor(
|
|||||||
option: Option<T>,
|
option: Option<T>,
|
||||||
value: T?,
|
value: T?,
|
||||||
noinline setValue: (T?) -> Unit,
|
noinline setValue: (T?) -> Unit,
|
||||||
|
selectionWarningEnabled: Boolean,
|
||||||
crossinline onDismissDialog: @DisallowComposableCalls () -> Unit = {},
|
crossinline onDismissDialog: @DisallowComposableCalls () -> Unit = {},
|
||||||
block: OptionEditorScope<T>.() -> Unit
|
block: OptionEditorScope<T>.() -> Unit
|
||||||
) {
|
) {
|
||||||
var showDialog by rememberSaveable { mutableStateOf(false) }
|
var showDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
val scope = remember(editor, option, value, setValue) {
|
var showSelectionWarningDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val scope = remember(editor, option, value, setValue, selectionWarningEnabled) {
|
||||||
OptionEditorScope(
|
OptionEditorScope(
|
||||||
editor,
|
editor,
|
||||||
option,
|
option,
|
||||||
@@ -131,11 +167,18 @@ private inline fun <T : Any> WithOptionEditor(
|
|||||||
showDialog = false
|
showDialog = false
|
||||||
onDismissDialog()
|
onDismissDialog()
|
||||||
},
|
},
|
||||||
|
selectionWarningEnabled,
|
||||||
|
showSelectionWarning = { showSelectionWarningDialog = true },
|
||||||
value,
|
value,
|
||||||
setValue
|
setValue
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showSelectionWarningDialog)
|
||||||
|
SelectionWarningDialog(
|
||||||
|
onDismiss = { showSelectionWarningDialog = false }
|
||||||
|
)
|
||||||
|
|
||||||
if (showDialog) scope.Dialog()
|
if (showDialog) scope.Dialog()
|
||||||
|
|
||||||
scope.block()
|
scope.block()
|
||||||
@@ -146,6 +189,7 @@ fun <T : Any> OptionItem(
|
|||||||
option: Option<T>,
|
option: Option<T>,
|
||||||
value: T?,
|
value: T?,
|
||||||
setValue: (T?) -> Unit,
|
setValue: (T?) -> Unit,
|
||||||
|
selectionWarningEnabled: Boolean
|
||||||
) {
|
) {
|
||||||
val editor = remember(option.type, option.presets) {
|
val editor = remember(option.type, option.presets) {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
@@ -158,7 +202,7 @@ fun <T : Any> OptionItem(
|
|||||||
else baseOptionEditor
|
else baseOptionEditor
|
||||||
}
|
}
|
||||||
|
|
||||||
WithOptionEditor(editor, option, value, setValue) {
|
WithOptionEditor(editor, option, value, setValue, selectionWarningEnabled) {
|
||||||
ListItem(
|
ListItem(
|
||||||
modifier = Modifier.clickable(onClick = ::clickAction),
|
modifier = Modifier.clickable(onClick = ::clickAction),
|
||||||
headlineContent = { Text(option.title) },
|
headlineContent = { Text(option.title) },
|
||||||
@@ -277,7 +321,7 @@ private object StringOptionEditor : OptionEditor<String> {
|
|||||||
|
|
||||||
private abstract class NumberOptionEditor<T : Number> : OptionEditor<T> {
|
private abstract class NumberOptionEditor<T : Number> : OptionEditor<T> {
|
||||||
@Composable
|
@Composable
|
||||||
protected abstract fun NumberDialog(
|
abstract fun NumberDialog(
|
||||||
title: String,
|
title: String,
|
||||||
current: T?,
|
current: T?,
|
||||||
validator: (T?) -> Boolean,
|
validator: (T?) -> Boolean,
|
||||||
@@ -331,7 +375,14 @@ private object BooleanOptionEditor : OptionEditor<Boolean> {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun ListItemTrailingContent(scope: OptionEditorScope<Boolean>) {
|
override fun ListItemTrailingContent(scope: OptionEditorScope<Boolean>) {
|
||||||
HapticSwitch(checked = scope.current, onCheckedChange = scope.setValue)
|
HapticSwitch(
|
||||||
|
checked = scope.current,
|
||||||
|
onCheckedChange = { value ->
|
||||||
|
scope.checkSafeguard {
|
||||||
|
scope.setValue(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -370,6 +421,7 @@ private class PresetOptionEditor<T : Any>(private val innerEditor: OptionEditor<
|
|||||||
scope.option,
|
scope.option,
|
||||||
scope.value,
|
scope.value,
|
||||||
scope.setValue,
|
scope.setValue,
|
||||||
|
scope.selectionWarningEnabled,
|
||||||
onDismissDialog = scope.dismissDialog
|
onDismissDialog = scope.dismissDialog
|
||||||
) inner@{
|
) inner@{
|
||||||
var hidePresetsDialog by rememberSaveable {
|
var hidePresetsDialog by rememberSaveable {
|
||||||
@@ -476,7 +528,8 @@ private class ListOptionEditor<T : Serializable>(private val elementEditor: Opti
|
|||||||
|
|
||||||
val lazyListState = rememberLazyListState()
|
val lazyListState = rememberLazyListState()
|
||||||
val reorderableLazyColumnState =
|
val reorderableLazyColumnState =
|
||||||
rememberReorderableLazyColumnState(lazyListState) { from, to ->
|
// Update the list
|
||||||
|
rememberReorderableLazyListState(lazyListState) { from, to ->
|
||||||
// Update the list
|
// Update the list
|
||||||
items.add(to.index, items.removeAt(from.index))
|
items.add(to.index, items.removeAt(from.index))
|
||||||
}
|
}
|
||||||
@@ -503,12 +556,8 @@ private class ListOptionEditor<T : Serializable>(private val elementEditor: Opti
|
|||||||
scope.submitDialog(items.mapNotNull { it.value })
|
scope.submitDialog(items.mapNotNull { it.value })
|
||||||
}
|
}
|
||||||
|
|
||||||
ComposeDialog(
|
FullscreenDialog(
|
||||||
onDismissRequest = back,
|
onDismissRequest = back,
|
||||||
properties = DialogProperties(
|
|
||||||
usePlatformDefaultWidth = false,
|
|
||||||
dismissOnBackPress = true
|
|
||||||
),
|
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
@@ -594,7 +643,8 @@ private class ListOptionEditor<T : Serializable>(private val elementEditor: Opti
|
|||||||
elementEditor,
|
elementEditor,
|
||||||
elementOption,
|
elementOption,
|
||||||
value = item.value,
|
value = item.value,
|
||||||
setValue = { items[index] = item.copy(value = it) }
|
setValue = { items[index] = item.copy(value = it) },
|
||||||
|
selectionWarningEnabled = scope.selectionWarningEnabled
|
||||||
) {
|
) {
|
||||||
ListItem(
|
ListItem(
|
||||||
modifier = Modifier.combinedClickable(
|
modifier = Modifier.combinedClickable(
|
||||||
@@ -602,8 +652,10 @@ private class ListOptionEditor<T : Serializable>(private val elementEditor: Opti
|
|||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
onLongClickLabel = stringResource(R.string.select),
|
onLongClickLabel = stringResource(R.string.select),
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
deletionTargets.add(item.key)
|
if (!deleteMode) {
|
||||||
deleteMode = true
|
deletionTargets.add(item.key)
|
||||||
|
deleteMode = true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
if (!deleteMode) {
|
if (!deleteMode) {
|
||||||
|
|||||||
@@ -23,10 +23,9 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.window.Dialog
|
|
||||||
import androidx.compose.ui.window.DialogProperties
|
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.ui.component.AppTopBar
|
import app.revanced.manager.ui.component.AppTopBar
|
||||||
|
import app.revanced.manager.ui.component.FullscreenDialog
|
||||||
import app.revanced.manager.ui.component.GroupHeader
|
import app.revanced.manager.ui.component.GroupHeader
|
||||||
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
|
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
|
||||||
import app.revanced.manager.util.saver.PathSaver
|
import app.revanced.manager.util.saver.PathSaver
|
||||||
@@ -48,12 +47,8 @@ fun PathSelectorDialog(root: Path, onSelect: (Path?) -> Unit) {
|
|||||||
currentDirectory.listDirectoryEntries().filter(Path::isReadable).partition(Path::isDirectory)
|
currentDirectory.listDirectoryEntries().filter(Path::isReadable).partition(Path::isDirectory)
|
||||||
}
|
}
|
||||||
|
|
||||||
Dialog(
|
FullscreenDialog(
|
||||||
onDismissRequest = { onSelect(null) },
|
onDismissRequest = { onSelect(null) },
|
||||||
properties = DialogProperties(
|
|
||||||
usePlatformDefaultWidth = false,
|
|
||||||
dismissOnBackPress = true
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package app.revanced.manager.ui.component.patches
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import app.revanced.manager.R
|
||||||
|
import app.revanced.manager.ui.component.SafeguardDialog
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SelectionWarningDialog(
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
|
SafeguardDialog(
|
||||||
|
onDismiss = onDismiss,
|
||||||
|
title = R.string.warning,
|
||||||
|
body = stringResource(R.string.selection_warning_description),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -7,10 +7,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.CalendarToday
|
import androidx.compose.material.icons.outlined.NewReleases
|
||||||
import androidx.compose.material.icons.outlined.Campaign
|
|
||||||
import androidx.compose.material.icons.outlined.FileDownload
|
|
||||||
import androidx.compose.material.icons.outlined.Sell
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -37,28 +34,18 @@ fun Changelog(
|
|||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.Campaign,
|
imageVector = Icons.Outlined.NewReleases,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(32.dp)
|
.size(32.dp)
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
version.removePrefix("v"),
|
"${version.removePrefix("v")} ($publishDate)",
|
||||||
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight(800)),
|
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight(800)),
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Tag(
|
|
||||||
Icons.Outlined.CalendarToday,
|
|
||||||
publishDate
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Markdown(
|
Markdown(
|
||||||
markdown,
|
markdown,
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package app.revanced.manager.ui.component.settings
|
package app.revanced.manager.ui.component.settings
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.WarningAmber
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -9,7 +11,9 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.domain.manager.base.Preference
|
import app.revanced.manager.domain.manager.base.Preference
|
||||||
|
import app.revanced.manager.ui.component.ConfirmDialog
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -28,13 +32,15 @@ fun SafeguardBooleanItem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (showSafeguardWarning) {
|
if (showSafeguardWarning) {
|
||||||
SafeguardConfirmationDialog(
|
ConfirmDialog(
|
||||||
onDismiss = { showSafeguardWarning = false },
|
onDismiss = { showSafeguardWarning = false },
|
||||||
onConfirm = {
|
onConfirm = {
|
||||||
coroutineScope.launch { preference.update(!value) }
|
coroutineScope.launch { preference.update(!value) }
|
||||||
showSafeguardWarning = false
|
showSafeguardWarning = false
|
||||||
},
|
},
|
||||||
body = stringResource(confirmationText)
|
title = stringResource(id = R.string.warning),
|
||||||
|
description = stringResource(confirmationText),
|
||||||
|
icon = Icons.Outlined.WarningAmber
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
package app.revanced.manager.ui.component.settings
|
|
||||||
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.outlined.WarningAmber
|
|
||||||
import androidx.compose.material3.AlertDialog
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import app.revanced.manager.R
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SafeguardConfirmationDialog(
|
|
||||||
onDismiss: () -> Unit,
|
|
||||||
onConfirm: () -> Unit,
|
|
||||||
body: String,
|
|
||||||
) {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = onDismiss,
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(onClick = onConfirm) {
|
|
||||||
Text(stringResource(R.string.yes))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(onClick = onDismiss) {
|
|
||||||
Text(stringResource(R.string.no))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon = {
|
|
||||||
Icon(Icons.Outlined.WarningAmber, null)
|
|
||||||
},
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
text = stringResource(id = R.string.warning),
|
|
||||||
style = MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
text = {
|
|
||||||
Text(body)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,17 @@
|
|||||||
package app.revanced.manager.ui.component.settings
|
package app.revanced.manager.ui.component.settings
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.animateContentSize
|
||||||
|
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.ExpandLess
|
||||||
|
import androidx.compose.material.icons.filled.ExpandMore
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.ListItemColors
|
import androidx.compose.material3.ListItemColors
|
||||||
import androidx.compose.material3.ListItemDefaults
|
import androidx.compose.material3.ListItemDefaults
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -10,6 +21,10 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsListItem(
|
fun SettingsListItem(
|
||||||
@@ -67,4 +82,48 @@ fun SettingsListItem(
|
|||||||
colors = colors,
|
colors = colors,
|
||||||
tonalElevation = tonalElevation,
|
tonalElevation = tonalElevation,
|
||||||
shadowElevation = shadowElevation
|
shadowElevation = shadowElevation
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ExpandableSettingListItem(
|
||||||
|
headlineContent: String,
|
||||||
|
supportingContent: String,
|
||||||
|
expandableContent: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.animateContentSize()
|
||||||
|
) {
|
||||||
|
SettingsListItem(
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable{ expanded = !expanded },
|
||||||
|
headlineContent = headlineContent,
|
||||||
|
supportingContent = supportingContent,
|
||||||
|
trailingContent = {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (expanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
AnimatedVisibility(visible = expanded) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 12.dp, start = 16.dp, end = 16.dp)
|
||||||
|
.animateContentSize(
|
||||||
|
animationSpec = tween(
|
||||||
|
durationMillis = 500,
|
||||||
|
easing = FastOutSlowInEasing
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
expandableContent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,112 +0,0 @@
|
|||||||
package app.revanced.manager.ui.model
|
|
||||||
|
|
||||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
|
||||||
import app.revanced.manager.patcher.patch.PatchInfo
|
|
||||||
import app.revanced.manager.util.PatchSelection
|
|
||||||
import app.revanced.manager.util.flatMapLatestAndCombine
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A data class that contains patch bundle metadata for use by UI code.
|
|
||||||
*/
|
|
||||||
data class BundleInfo(
|
|
||||||
val name: String,
|
|
||||||
val uid: Int,
|
|
||||||
val supported: List<PatchInfo>,
|
|
||||||
val unsupported: List<PatchInfo>,
|
|
||||||
val universal: List<PatchInfo>
|
|
||||||
) {
|
|
||||||
val all = sequence {
|
|
||||||
yieldAll(supported)
|
|
||||||
yieldAll(unsupported)
|
|
||||||
yieldAll(universal)
|
|
||||||
}
|
|
||||||
|
|
||||||
val patchCount get() = supported.size + unsupported.size + universal.size
|
|
||||||
|
|
||||||
fun patchSequence(allowUnsupported: Boolean) = if (allowUnsupported) {
|
|
||||||
all
|
|
||||||
} else {
|
|
||||||
sequence {
|
|
||||||
yieldAll(supported)
|
|
||||||
yieldAll(universal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object Extensions {
|
|
||||||
inline fun Iterable<BundleInfo>.toPatchSelection(
|
|
||||||
allowUnsupported: Boolean,
|
|
||||||
condition: (Int, PatchInfo) -> Boolean
|
|
||||||
): PatchSelection = this.associate { bundle ->
|
|
||||||
val patches =
|
|
||||||
bundle.patchSequence(allowUnsupported)
|
|
||||||
.mapNotNullTo(mutableSetOf()) { patch ->
|
|
||||||
patch.name.takeIf {
|
|
||||||
condition(
|
|
||||||
bundle.uid,
|
|
||||||
patch
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bundle.uid to patches
|
|
||||||
}
|
|
||||||
|
|
||||||
fun PatchBundleRepository.bundleInfoFlow(packageName: String, version: String?) =
|
|
||||||
sources.flatMapLatestAndCombine(
|
|
||||||
combiner = { it.filterNotNull() }
|
|
||||||
) { source ->
|
|
||||||
// Regenerate bundle information whenever this source updates.
|
|
||||||
source.state.map { state ->
|
|
||||||
val bundle = state.patchBundleOrNull() ?: return@map null
|
|
||||||
|
|
||||||
val supported = mutableListOf<PatchInfo>()
|
|
||||||
val unsupported = mutableListOf<PatchInfo>()
|
|
||||||
val universal = mutableListOf<PatchInfo>()
|
|
||||||
|
|
||||||
bundle.patches.filter { it.compatibleWith(packageName) }.forEach {
|
|
||||||
val targetList = when {
|
|
||||||
it.compatiblePackages == null -> universal
|
|
||||||
it.supports(
|
|
||||||
packageName,
|
|
||||||
version
|
|
||||||
) -> supported
|
|
||||||
|
|
||||||
else -> unsupported
|
|
||||||
}
|
|
||||||
|
|
||||||
targetList.add(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
BundleInfo(source.getName(), source.uid, supported, unsupported, universal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Algorithm for determining whether all required options have been set.
|
|
||||||
*/
|
|
||||||
inline fun Iterable<BundleInfo>.requiredOptionsSet(
|
|
||||||
crossinline isSelected: (BundleInfo, PatchInfo) -> Boolean,
|
|
||||||
crossinline optionsForPatch: (BundleInfo, PatchInfo) -> Map<String, Any?>?
|
|
||||||
) = all bundle@{ bundle ->
|
|
||||||
bundle
|
|
||||||
.all
|
|
||||||
.filter { isSelected(bundle, it) }
|
|
||||||
.all patch@{
|
|
||||||
if (it.options.isNullOrEmpty()) return@patch true
|
|
||||||
val opts by lazy { optionsForPatch(bundle, it).orEmpty() }
|
|
||||||
|
|
||||||
it.options.all option@{ option ->
|
|
||||||
if (!option.required || option.default != null) return@option true
|
|
||||||
|
|
||||||
option.key in opts
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class BundleType {
|
|
||||||
Local,
|
|
||||||
Remote
|
|
||||||
}
|
|
||||||
@@ -70,6 +70,9 @@ object Settings {
|
|||||||
@Serializable
|
@Serializable
|
||||||
data object Advanced : Destination
|
data object Advanced : Destination
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object Updates : Destination
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data object Downloads : Destination
|
data object Downloads : Destination
|
||||||
|
|
||||||
@@ -89,5 +92,5 @@ object Settings {
|
|||||||
data object Licenses : Destination
|
data object Licenses : Destination
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data object DeveloperOptions : Destination
|
data object Developer : Destination
|
||||||
}
|
}
|
||||||
@@ -3,52 +3,74 @@ package app.revanced.manager.ui.screen
|
|||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import app.revanced.manager.domain.bundles.PatchBundleSource
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
|
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
|
||||||
import app.revanced.manager.ui.component.bundle.BundleItem
|
import app.revanced.manager.ui.component.bundle.BundleItem
|
||||||
|
import app.revanced.manager.ui.viewmodel.BundleListViewModel
|
||||||
|
import app.revanced.manager.util.EventEffect
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun BundleListScreen(
|
fun BundleListScreen(
|
||||||
onDelete: (PatchBundleSource) -> Unit,
|
viewModel: BundleListViewModel = koinViewModel(),
|
||||||
onUpdate: (PatchBundleSource) -> Unit,
|
eventsFlow: Flow<BundleListViewModel.Event>,
|
||||||
sources: List<PatchBundleSource>,
|
setSelectedSourceCount: (Int) -> Unit
|
||||||
selectedSources: SnapshotStateList<PatchBundleSource>,
|
|
||||||
bundlesSelectable: Boolean,
|
|
||||||
) {
|
) {
|
||||||
LazyColumnWithScrollbar(
|
val patchCounts by viewModel.patchCounts.collectAsStateWithLifecycle(emptyMap())
|
||||||
modifier = Modifier.fillMaxSize(),
|
val sources by viewModel.sources.collectAsStateWithLifecycle(emptyList())
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.Top,
|
EventEffect(eventsFlow) {
|
||||||
|
viewModel.handleEvent(it)
|
||||||
|
}
|
||||||
|
LaunchedEffect(viewModel.selectedSources.size) {
|
||||||
|
setSelectedSourceCount(viewModel.selectedSources.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
PullToRefreshBox(
|
||||||
|
onRefresh = viewModel::refresh,
|
||||||
|
isRefreshing = viewModel.isRefreshing
|
||||||
) {
|
) {
|
||||||
items(
|
LazyColumnWithScrollbar(
|
||||||
sources,
|
modifier = Modifier.fillMaxSize(),
|
||||||
key = { it.uid }
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) { source ->
|
verticalArrangement = Arrangement.Top,
|
||||||
BundleItem(
|
) {
|
||||||
bundle = source,
|
items(
|
||||||
onDelete = {
|
sources,
|
||||||
onDelete(source)
|
key = { it.uid }
|
||||||
},
|
) { source ->
|
||||||
onUpdate = {
|
BundleItem(
|
||||||
onUpdate(source)
|
src = source,
|
||||||
},
|
patchCount = patchCounts[source.uid] ?: 0,
|
||||||
selectable = bundlesSelectable,
|
onDelete = {
|
||||||
onSelect = {
|
viewModel.delete(source)
|
||||||
selectedSources.add(source)
|
},
|
||||||
},
|
onUpdate = {
|
||||||
isBundleSelected = selectedSources.contains(source),
|
viewModel.update(source)
|
||||||
toggleSelection = { bundleIsNotSelected ->
|
},
|
||||||
if (bundleIsNotSelected) {
|
selectable = viewModel.selectedSources.size > 0,
|
||||||
selectedSources.add(source)
|
onSelect = {
|
||||||
} else {
|
viewModel.selectedSources.add(source.uid)
|
||||||
selectedSources.remove(source)
|
},
|
||||||
|
isBundleSelected = source.uid in viewModel.selectedSources,
|
||||||
|
toggleSelection = { bundleIsNotSelected ->
|
||||||
|
if (bundleIsNotSelected) {
|
||||||
|
viewModel.selectedSources.add(source.uid)
|
||||||
|
} else {
|
||||||
|
viewModel.selectedSources.remove(source.uid)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ import android.net.Uri
|
|||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -20,6 +21,7 @@ import androidx.compose.material.icons.filled.BatteryAlert
|
|||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.outlined.Apps
|
import androidx.compose.material.icons.outlined.Apps
|
||||||
import androidx.compose.material.icons.outlined.BugReport
|
import androidx.compose.material.icons.outlined.BugReport
|
||||||
|
import androidx.compose.material.icons.outlined.Delete
|
||||||
import androidx.compose.material.icons.outlined.DeleteOutline
|
import androidx.compose.material.icons.outlined.DeleteOutline
|
||||||
import androidx.compose.material.icons.outlined.Download
|
import androidx.compose.material.icons.outlined.Download
|
||||||
import androidx.compose.material.icons.outlined.Refresh
|
import androidx.compose.material.icons.outlined.Refresh
|
||||||
@@ -42,6 +44,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
@@ -54,13 +57,13 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.isDefault
|
|
||||||
import app.revanced.manager.patcher.aapt.Aapt
|
import app.revanced.manager.patcher.aapt.Aapt
|
||||||
import app.revanced.manager.ui.component.AlertDialogExtended
|
import app.revanced.manager.ui.component.AlertDialogExtended
|
||||||
import app.revanced.manager.ui.component.AppTopBar
|
import app.revanced.manager.ui.component.AppTopBar
|
||||||
import app.revanced.manager.ui.component.AutoUpdatesDialog
|
import app.revanced.manager.ui.component.AutoUpdatesDialog
|
||||||
import app.revanced.manager.ui.component.AvailableUpdateDialog
|
import app.revanced.manager.ui.component.AvailableUpdateDialog
|
||||||
import app.revanced.manager.ui.component.NotificationCard
|
import app.revanced.manager.ui.component.NotificationCard
|
||||||
|
import app.revanced.manager.ui.component.ConfirmDialog
|
||||||
import app.revanced.manager.ui.component.bundle.BundleTopBar
|
import app.revanced.manager.ui.component.bundle.BundleTopBar
|
||||||
import app.revanced.manager.ui.component.bundle.ImportPatchBundleDialog
|
import app.revanced.manager.ui.component.bundle.ImportPatchBundleDialog
|
||||||
import app.revanced.manager.ui.component.haptics.HapticFloatingActionButton
|
import app.revanced.manager.ui.component.haptics.HapticFloatingActionButton
|
||||||
@@ -76,7 +79,7 @@ enum class DashboardPage(
|
|||||||
val icon: ImageVector
|
val icon: ImageVector
|
||||||
) {
|
) {
|
||||||
DASHBOARD(R.string.tab_apps, Icons.Outlined.Apps),
|
DASHBOARD(R.string.tab_apps, Icons.Outlined.Apps),
|
||||||
BUNDLES(R.string.tab_bundles, Icons.Outlined.Source),
|
BUNDLES(R.string.tab_patches, Icons.Outlined.Source),
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("BatteryLife")
|
@SuppressLint("BatteryLife")
|
||||||
@@ -90,7 +93,8 @@ fun DashboardScreen(
|
|||||||
onDownloaderPluginClick: () -> Unit,
|
onDownloaderPluginClick: () -> Unit,
|
||||||
onAppClick: (String) -> Unit
|
onAppClick: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
val bundlesSelectable by remember { derivedStateOf { vm.selectedSources.size > 0 } }
|
var selectedSourceCount by rememberSaveable { mutableIntStateOf(0) }
|
||||||
|
val bundlesSelectable by remember { derivedStateOf { selectedSourceCount > 0 } }
|
||||||
val availablePatches by vm.availablePatches.collectAsStateWithLifecycle(0)
|
val availablePatches by vm.availablePatches.collectAsStateWithLifecycle(0)
|
||||||
val showNewDownloaderPluginsNotification by vm.newDownloaderPluginsAvailable.collectAsStateWithLifecycle(
|
val showNewDownloaderPluginsNotification by vm.newDownloaderPluginsAvailable.collectAsStateWithLifecycle(
|
||||||
false
|
false
|
||||||
@@ -153,11 +157,22 @@ fun DashboardScreen(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var showDeleteConfirmationDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
|
if (showDeleteConfirmationDialog) {
|
||||||
|
ConfirmDialog(
|
||||||
|
onDismiss = { showDeleteConfirmationDialog = false },
|
||||||
|
onConfirm = vm::deleteSources,
|
||||||
|
title = stringResource(R.string.delete),
|
||||||
|
description = stringResource(R.string.patches_delete_multiple_dialog_description),
|
||||||
|
icon = Icons.Outlined.Delete
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
if (bundlesSelectable) {
|
if (bundlesSelectable) {
|
||||||
BundleTopBar(
|
BundleTopBar(
|
||||||
title = stringResource(R.string.bundles_selected, vm.selectedSources.size),
|
title = stringResource(R.string.patches_selected, selectedSourceCount),
|
||||||
onBackClick = vm::cancelSourceSelection,
|
onBackClick = vm::cancelSourceSelection,
|
||||||
backIcon = {
|
backIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -168,8 +183,7 @@ fun DashboardScreen(
|
|||||||
actions = {
|
actions = {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
vm.selectedSources.forEach { if (!it.isDefault) vm.delete(it) }
|
showDeleteConfirmationDialog = true
|
||||||
vm.cancelSourceSelection()
|
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -178,10 +192,7 @@ fun DashboardScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = vm::updateSources
|
||||||
vm.selectedSources.forEach { vm.update(it) }
|
|
||||||
vm.cancelSourceSelection()
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Outlined.Refresh,
|
Icons.Outlined.Refresh,
|
||||||
@@ -223,7 +234,7 @@ fun DashboardScreen(
|
|||||||
when (pagerState.currentPage) {
|
when (pagerState.currentPage) {
|
||||||
DashboardPage.DASHBOARD.ordinal -> {
|
DashboardPage.DASHBOARD.ordinal -> {
|
||||||
if (availablePatches < 1) {
|
if (availablePatches < 1) {
|
||||||
androidContext.toast(androidContext.getString(R.string.patches_unavailable))
|
androidContext.toast(androidContext.getString(R.string.no_patch_found))
|
||||||
composableScope.launch {
|
composableScope.launch {
|
||||||
pagerState.animateScrollToPage(
|
pagerState.animateScrollToPage(
|
||||||
DashboardPage.BUNDLES.ordinal
|
DashboardPage.BUNDLES.ordinal
|
||||||
@@ -264,9 +275,6 @@ fun DashboardScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val showBatteryOptimizationsWarning by vm.showBatteryOptimizationsWarningFlow.collectAsStateWithLifecycle(
|
|
||||||
false
|
|
||||||
)
|
|
||||||
Notifications(
|
Notifications(
|
||||||
if (!Aapt.supportsDevice()) {
|
if (!Aapt.supportsDevice()) {
|
||||||
{
|
{
|
||||||
@@ -278,16 +286,23 @@ fun DashboardScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else null,
|
} else null,
|
||||||
if (showBatteryOptimizationsWarning) {
|
if (vm.showBatteryOptimizationsWarning) {
|
||||||
{
|
{
|
||||||
|
val batteryOptimizationsLauncher =
|
||||||
|
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
|
vm.updateBatteryOptimizationsWarning()
|
||||||
|
}
|
||||||
NotificationCard(
|
NotificationCard(
|
||||||
isWarning = true,
|
isWarning = true,
|
||||||
icon = Icons.Default.BatteryAlert,
|
icon = Icons.Default.BatteryAlert,
|
||||||
text = stringResource(R.string.battery_optimization_notification),
|
text = stringResource(R.string.battery_optimization_notification),
|
||||||
onClick = {
|
onClick = {
|
||||||
androidContext.startActivity(Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
|
batteryOptimizationsLauncher.launch(
|
||||||
data = Uri.parse("package:${androidContext.packageName}")
|
Intent(
|
||||||
})
|
Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
|
||||||
|
Uri.fromParts("package", androidContext.packageName, null)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -329,18 +344,9 @@ fun DashboardScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val sources by vm.sources.collectAsStateWithLifecycle(initialValue = emptyList())
|
|
||||||
|
|
||||||
BundleListScreen(
|
BundleListScreen(
|
||||||
onDelete = {
|
eventsFlow = vm.bundleListEventsFlow,
|
||||||
vm.delete(it)
|
setSelectedSourceCount = { selectedSourceCount = it }
|
||||||
},
|
|
||||||
onUpdate = {
|
|
||||||
vm.update(it)
|
|
||||||
},
|
|
||||||
sources = sources,
|
|
||||||
selectedSources = vm.selectedSources,
|
|
||||||
bundlesSelectable = bundlesSelectable
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,15 +7,28 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
|||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
|
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.outlined.OpenInNew
|
import androidx.compose.material.icons.automirrored.outlined.OpenInNew
|
||||||
|
import androidx.compose.material.icons.outlined.Cancel
|
||||||
import androidx.compose.material.icons.outlined.FileDownload
|
import androidx.compose.material.icons.outlined.FileDownload
|
||||||
import androidx.compose.material.icons.outlined.PostAdd
|
import androidx.compose.material.icons.outlined.PostAdd
|
||||||
import androidx.compose.material.icons.outlined.Save
|
import androidx.compose.material.icons.outlined.Save
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.BottomAppBar
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
@@ -33,6 +46,7 @@ import app.revanced.manager.R
|
|||||||
import app.revanced.manager.data.room.apps.installed.InstallType
|
import app.revanced.manager.data.room.apps.installed.InstallType
|
||||||
import app.revanced.manager.ui.component.AppScaffold
|
import app.revanced.manager.ui.component.AppScaffold
|
||||||
import app.revanced.manager.ui.component.AppTopBar
|
import app.revanced.manager.ui.component.AppTopBar
|
||||||
|
import app.revanced.manager.ui.component.ConfirmDialog
|
||||||
import app.revanced.manager.ui.component.InstallerStatusDialog
|
import app.revanced.manager.ui.component.InstallerStatusDialog
|
||||||
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
||||||
import app.revanced.manager.ui.component.patcher.InstallPickerDialog
|
import app.revanced.manager.ui.component.patcher.InstallPickerDialog
|
||||||
@@ -46,25 +60,34 @@ import app.revanced.manager.util.EventEffect
|
|||||||
@Composable
|
@Composable
|
||||||
fun PatcherScreen(
|
fun PatcherScreen(
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
vm: PatcherViewModel
|
viewModel: PatcherViewModel
|
||||||
) {
|
) {
|
||||||
fun leaveScreen() {
|
fun onLeave() {
|
||||||
vm.onBack()
|
viewModel.onBack()
|
||||||
onBackClick()
|
onBackClick()
|
||||||
}
|
}
|
||||||
BackHandler(onBack = ::leaveScreen)
|
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val exportApkLauncher =
|
val exportApkLauncher =
|
||||||
rememberLauncherForActivityResult(CreateDocument(APK_MIMETYPE), vm::export)
|
rememberLauncherForActivityResult(CreateDocument(APK_MIMETYPE), viewModel::export)
|
||||||
|
|
||||||
val patcherSucceeded by vm.patcherSucceeded.observeAsState(null)
|
val patcherSucceeded by viewModel.patcherSucceeded.observeAsState(null)
|
||||||
val canInstall by remember { derivedStateOf { patcherSucceeded == true && (vm.installedPackageName != null || !vm.isInstalling) } }
|
val canInstall by remember { derivedStateOf { patcherSucceeded == true && (viewModel.installedPackageName != null || !viewModel.isInstalling) } }
|
||||||
var showInstallPicker by rememberSaveable { mutableStateOf(false) }
|
var showInstallPicker by rememberSaveable { mutableStateOf(false) }
|
||||||
|
var showDismissConfirmationDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
|
fun onPageBack() {
|
||||||
|
if(patcherSucceeded == null)
|
||||||
|
showDismissConfirmationDialog = true
|
||||||
|
else
|
||||||
|
onLeave()
|
||||||
|
}
|
||||||
|
|
||||||
|
BackHandler(onBack = ::onPageBack)
|
||||||
|
|
||||||
val steps by remember {
|
val steps by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
vm.steps.groupBy { it.category }
|
viewModel.steps.groupBy { it.category }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,34 +104,44 @@ fun PatcherScreen(
|
|||||||
if (showInstallPicker)
|
if (showInstallPicker)
|
||||||
InstallPickerDialog(
|
InstallPickerDialog(
|
||||||
onDismiss = { showInstallPicker = false },
|
onDismiss = { showInstallPicker = false },
|
||||||
onConfirm = vm::install
|
onConfirm = viewModel::install
|
||||||
)
|
)
|
||||||
|
|
||||||
vm.packageInstallerStatus?.let {
|
if (showDismissConfirmationDialog) {
|
||||||
InstallerStatusDialog(it, vm, vm::dismissPackageInstallerDialog)
|
ConfirmDialog(
|
||||||
|
onDismiss = { showDismissConfirmationDialog = false },
|
||||||
|
onConfirm = ::onLeave,
|
||||||
|
title = stringResource(R.string.patcher_stop_confirm_title),
|
||||||
|
description = stringResource(R.string.patcher_stop_confirm_description),
|
||||||
|
icon = Icons.Outlined.Cancel
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.packageInstallerStatus?.let {
|
||||||
|
InstallerStatusDialog(it, viewModel, viewModel::dismissPackageInstallerDialog)
|
||||||
}
|
}
|
||||||
|
|
||||||
val activityLauncher = rememberLauncherForActivityResult(
|
val activityLauncher = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.StartActivityForResult(),
|
contract = ActivityResultContracts.StartActivityForResult(),
|
||||||
onResult = vm::handleActivityResult
|
onResult = viewModel::handleActivityResult
|
||||||
)
|
)
|
||||||
EventEffect(flow = vm.launchActivityFlow) { intent ->
|
EventEffect(flow = viewModel.launchActivityFlow) { intent ->
|
||||||
activityLauncher.launch(intent)
|
activityLauncher.launch(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
vm.activityPromptDialog?.let { title ->
|
viewModel.activityPromptDialog?.let { title ->
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = vm::rejectInteraction,
|
onDismissRequest = viewModel::rejectInteraction,
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = vm::allowInteraction
|
onClick = viewModel::allowInteraction
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.continue_))
|
Text(stringResource(R.string.continue_))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = vm::rejectInteraction
|
onClick = viewModel::rejectInteraction
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.cancel))
|
Text(stringResource(R.string.cancel))
|
||||||
}
|
}
|
||||||
@@ -125,20 +158,20 @@ fun PatcherScreen(
|
|||||||
AppTopBar(
|
AppTopBar(
|
||||||
title = stringResource(R.string.patcher),
|
title = stringResource(R.string.patcher),
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
onBackClick = ::leaveScreen
|
onBackClick = ::onPageBack
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
BottomAppBar(
|
BottomAppBar(
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { exportApkLauncher.launch("${vm.packageName}.apk") },
|
onClick = { exportApkLauncher.launch("${viewModel.packageName}_${viewModel.version}_revanced_patched.apk") },
|
||||||
enabled = patcherSucceeded == true
|
enabled = patcherSucceeded == true
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Outlined.Save, stringResource(id = R.string.save_apk))
|
Icon(Icons.Outlined.Save, stringResource(id = R.string.save_apk))
|
||||||
}
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { vm.exportLogs(context) },
|
onClick = { viewModel.exportLogs(context) },
|
||||||
enabled = patcherSucceeded != null
|
enabled = patcherSucceeded != null
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Outlined.PostAdd, stringResource(id = R.string.save_logs))
|
Icon(Icons.Outlined.PostAdd, stringResource(id = R.string.save_logs))
|
||||||
@@ -149,11 +182,11 @@ fun PatcherScreen(
|
|||||||
HapticExtendedFloatingActionButton(
|
HapticExtendedFloatingActionButton(
|
||||||
text = {
|
text = {
|
||||||
Text(
|
Text(
|
||||||
stringResource(if (vm.installedPackageName == null) R.string.install_app else R.string.open_app)
|
stringResource(if (viewModel.installedPackageName == null) R.string.install_app else R.string.open_app)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
icon = {
|
icon = {
|
||||||
vm.installedPackageName?.let {
|
viewModel.installedPackageName?.let {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.AutoMirrored.Outlined.OpenInNew,
|
Icons.AutoMirrored.Outlined.OpenInNew,
|
||||||
stringResource(R.string.open_app)
|
stringResource(R.string.open_app)
|
||||||
@@ -164,10 +197,10 @@ fun PatcherScreen(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
if (vm.installedPackageName == null)
|
if (viewModel.installedPackageName == null)
|
||||||
if (vm.isDeviceRooted()) showInstallPicker = true
|
if (viewModel.isDeviceRooted()) showInstallPicker = true
|
||||||
else vm.install(InstallType.DEFAULT)
|
else viewModel.install(InstallType.DEFAULT)
|
||||||
else vm.open()
|
else viewModel.open()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -181,7 +214,7 @@ fun PatcherScreen(
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
LinearProgressIndicator(
|
LinearProgressIndicator(
|
||||||
progress = { vm.progress },
|
progress = { viewModel.progress },
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -197,11 +230,11 @@ fun PatcherScreen(
|
|||||||
Steps(
|
Steps(
|
||||||
category = category,
|
category = category,
|
||||||
steps = steps,
|
steps = steps,
|
||||||
stepCount = if (category == StepCategory.PATCHING) vm.patchesProgress else null,
|
stepCount = if (category == StepCategory.PATCHING) viewModel.patchesProgress else null,
|
||||||
stepProgressProvider = vm
|
stepProgressProvider = viewModel
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,16 +58,14 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.draw.rotate
|
import androidx.compose.ui.draw.rotate
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog
|
|
||||||
import androidx.compose.ui.window.DialogProperties
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.patcher.patch.Option
|
import app.revanced.manager.patcher.patch.Option
|
||||||
import app.revanced.manager.patcher.patch.PatchInfo
|
import app.revanced.manager.patcher.patch.PatchInfo
|
||||||
import app.revanced.manager.ui.component.AppTopBar
|
import app.revanced.manager.ui.component.AppTopBar
|
||||||
import app.revanced.manager.ui.component.CheckedFilterChip
|
import app.revanced.manager.ui.component.CheckedFilterChip
|
||||||
|
import app.revanced.manager.ui.component.FullscreenDialog
|
||||||
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
|
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
|
||||||
import app.revanced.manager.ui.component.SafeguardDialog
|
import app.revanced.manager.ui.component.SafeguardDialog
|
||||||
import app.revanced.manager.ui.component.SearchBar
|
import app.revanced.manager.ui.component.SearchBar
|
||||||
@@ -75,9 +73,10 @@ import app.revanced.manager.ui.component.haptics.HapticCheckbox
|
|||||||
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
||||||
import app.revanced.manager.ui.component.haptics.HapticTab
|
import app.revanced.manager.ui.component.haptics.HapticTab
|
||||||
import app.revanced.manager.ui.component.patches.OptionItem
|
import app.revanced.manager.ui.component.patches.OptionItem
|
||||||
|
import app.revanced.manager.ui.component.patches.SelectionWarningDialog
|
||||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
|
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
|
||||||
|
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_INCOMPATIBLE
|
||||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNIVERSAL
|
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNIVERSAL
|
||||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNSUPPORTED
|
|
||||||
import app.revanced.manager.util.Options
|
import app.revanced.manager.util.Options
|
||||||
import app.revanced.manager.util.PatchSelection
|
import app.revanced.manager.util.PatchSelection
|
||||||
import app.revanced.manager.util.isScrollingUp
|
import app.revanced.manager.util.isScrollingUp
|
||||||
@@ -89,9 +88,9 @@ import kotlinx.coroutines.launch
|
|||||||
fun PatchesSelectorScreen(
|
fun PatchesSelectorScreen(
|
||||||
onSave: (PatchSelection?, Options) -> Unit,
|
onSave: (PatchSelection?, Options) -> Unit,
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
vm: PatchesSelectorViewModel
|
viewModel: PatchesSelectorViewModel
|
||||||
) {
|
) {
|
||||||
val bundles by vm.bundlesFlow.collectAsStateWithLifecycle(initialValue = emptyList())
|
val bundles by viewModel.bundlesFlow.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||||
val pagerState = rememberPagerState(
|
val pagerState = rememberPagerState(
|
||||||
initialPage = 0,
|
initialPage = 0,
|
||||||
initialPageOffsetFraction = 0f
|
initialPageOffsetFraction = 0f
|
||||||
@@ -107,15 +106,15 @@ fun PatchesSelectorScreen(
|
|||||||
}
|
}
|
||||||
var showBottomSheet by rememberSaveable { mutableStateOf(false) }
|
var showBottomSheet by rememberSaveable { mutableStateOf(false) }
|
||||||
val showSaveButton by remember {
|
val showSaveButton by remember {
|
||||||
derivedStateOf { vm.selectionIsValid(bundles) }
|
derivedStateOf { viewModel.selectionIsValid(bundles) }
|
||||||
}
|
}
|
||||||
|
|
||||||
val defaultPatchSelectionCount by vm.defaultSelectionCount
|
val defaultPatchSelectionCount by viewModel.defaultSelectionCount
|
||||||
.collectAsStateWithLifecycle(initialValue = 0)
|
.collectAsStateWithLifecycle(initialValue = 0)
|
||||||
|
|
||||||
val selectedPatchCount by remember {
|
val selectedPatchCount by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
vm.customPatchSelection?.values?.sumOf { it.size } ?: defaultPatchSelectionCount
|
viewModel.customPatchSelection?.values?.sumOf { it.size } ?: defaultPatchSelectionCount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,14 +146,14 @@ fun PatchesSelectorScreen(
|
|||||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
) {
|
) {
|
||||||
CheckedFilterChip(
|
CheckedFilterChip(
|
||||||
selected = vm.filter and SHOW_UNSUPPORTED == 0,
|
selected = viewModel.filter and SHOW_INCOMPATIBLE == 0,
|
||||||
onClick = { vm.toggleFlag(SHOW_UNSUPPORTED) },
|
onClick = { viewModel.toggleFlag(SHOW_INCOMPATIBLE) },
|
||||||
label = { Text(stringResource(R.string.supported)) }
|
label = { Text(stringResource(R.string.this_version)) }
|
||||||
)
|
)
|
||||||
|
|
||||||
CheckedFilterChip(
|
CheckedFilterChip(
|
||||||
selected = vm.filter and SHOW_UNIVERSAL != 0,
|
selected = viewModel.filter and SHOW_UNIVERSAL != 0,
|
||||||
onClick = { vm.toggleFlag(SHOW_UNIVERSAL) },
|
onClick = { viewModel.toggleFlag(SHOW_UNIVERSAL) },
|
||||||
label = { Text(stringResource(R.string.universal)) },
|
label = { Text(stringResource(R.string.universal)) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -162,49 +161,46 @@ fun PatchesSelectorScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (vm.compatibleVersions.isNotEmpty())
|
if (viewModel.compatibleVersions.isNotEmpty())
|
||||||
UnsupportedPatchDialog(
|
IncompatiblePatchDialog(
|
||||||
appVersion = vm.appVersion ?: stringResource(R.string.any_version),
|
appVersion = viewModel.appVersion ?: stringResource(R.string.any_version),
|
||||||
supportedVersions = vm.compatibleVersions,
|
compatibleVersions = viewModel.compatibleVersions,
|
||||||
onDismissRequest = vm::dismissDialogs
|
onDismissRequest = viewModel::dismissDialogs
|
||||||
)
|
)
|
||||||
var showUnsupportedPatchesDialog by rememberSaveable {
|
var showIncompatiblePatchesDialog by rememberSaveable {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
if (showUnsupportedPatchesDialog)
|
if (showIncompatiblePatchesDialog)
|
||||||
UnsupportedPatchesDialog(
|
IncompatiblePatchesDialog(
|
||||||
appVersion = vm.appVersion ?: stringResource(R.string.any_version),
|
appVersion = viewModel.appVersion ?: stringResource(R.string.any_version),
|
||||||
onDismissRequest = { showUnsupportedPatchesDialog = false }
|
onDismissRequest = { showIncompatiblePatchesDialog = false }
|
||||||
)
|
)
|
||||||
|
|
||||||
vm.optionsDialog?.let { (bundle, patch) ->
|
viewModel.optionsDialog?.let { (bundle, patch) ->
|
||||||
OptionsDialog(
|
OptionsDialog(
|
||||||
onDismissRequest = vm::dismissDialogs,
|
onDismissRequest = viewModel::dismissDialogs,
|
||||||
patch = patch,
|
patch = patch,
|
||||||
values = vm.getOptions(bundle, patch),
|
values = viewModel.getOptions(bundle, patch),
|
||||||
reset = { vm.resetOptions(bundle, patch) },
|
reset = { viewModel.resetOptions(bundle, patch) },
|
||||||
set = { key, value -> vm.setOption(bundle, patch, key, value) }
|
set = { key, value -> viewModel.setOption(bundle, patch, key, value) },
|
||||||
|
selectionWarningEnabled = viewModel.selectionWarningEnabled
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
var showSelectionWarning by rememberSaveable {
|
var showSelectionWarning by rememberSaveable { mutableStateOf(false) }
|
||||||
mutableStateOf(false)
|
var showUniversalWarning by rememberSaveable { mutableStateOf(false) }
|
||||||
}
|
|
||||||
if (showSelectionWarning) {
|
if (showSelectionWarning)
|
||||||
SelectionWarningDialog(onDismiss = { showSelectionWarning = false })
|
SelectionWarningDialog(onDismiss = { showSelectionWarning = false })
|
||||||
}
|
|
||||||
vm.pendingUniversalPatchAction?.let {
|
if (showUniversalWarning)
|
||||||
UniversalPatchWarningDialog(
|
UniversalPatchWarningDialog(onDismiss = { showUniversalWarning = false })
|
||||||
onCancel = vm::dismissUniversalPatchWarning,
|
|
||||||
onConfirm = vm::confirmUniversalPatchWarning
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun LazyListScope.patchList(
|
fun LazyListScope.patchList(
|
||||||
uid: Int,
|
uid: Int,
|
||||||
patches: List<PatchInfo>,
|
patches: List<PatchInfo>,
|
||||||
visible: Boolean,
|
visible: Boolean,
|
||||||
supported: Boolean,
|
compatible: Boolean,
|
||||||
header: (@Composable () -> Unit)? = null
|
header: (@Composable () -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
if (patches.isNotEmpty() && visible) {
|
if (patches.isNotEmpty() && visible) {
|
||||||
@@ -221,31 +217,27 @@ fun PatchesSelectorScreen(
|
|||||||
) { patch ->
|
) { patch ->
|
||||||
PatchItem(
|
PatchItem(
|
||||||
patch = patch,
|
patch = patch,
|
||||||
onOptionsDialog = {
|
onOptionsDialog = { viewModel.optionsDialog = uid to patch },
|
||||||
vm.optionsDialog = uid to patch
|
selected = compatible && viewModel.isSelected(
|
||||||
},
|
|
||||||
selected = supported && vm.isSelected(
|
|
||||||
uid,
|
uid,
|
||||||
patch
|
patch
|
||||||
),
|
),
|
||||||
onToggle = {
|
onToggle = {
|
||||||
when {
|
when {
|
||||||
// Open unsupported dialog if the patch is not supported
|
// Open incompatible dialog if the patch is not supported
|
||||||
!supported -> vm.openUnsupportedDialog(patch)
|
!compatible -> viewModel.openIncompatibleDialog(patch)
|
||||||
|
|
||||||
// Show selection warning if enabled
|
// Show selection warning if enabled
|
||||||
vm.selectionWarningEnabled -> showSelectionWarning = true
|
viewModel.selectionWarningEnabled -> showSelectionWarning = true
|
||||||
|
|
||||||
// Set pending universal patch action if the universal patch warning is enabled and there are no compatible packages
|
// Show universal warning if universal patch is selected and the toggle is off
|
||||||
vm.universalPatchWarningEnabled && patch.compatiblePackages == null -> {
|
patch.compatiblePackages == null && viewModel.universalPatchWarningEnabled -> showUniversalWarning = true
|
||||||
vm.pendingUniversalPatchAction = { vm.togglePatch(uid, patch) }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Toggle the patch otherwise
|
// Toggle the patch otherwise
|
||||||
else -> vm.togglePatch(uid, patch)
|
else -> viewModel.togglePatch(uid, patch)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
supported = supported
|
compatible = compatible
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -321,15 +313,15 @@ fun PatchesSelectorScreen(
|
|||||||
|
|
||||||
patchList(
|
patchList(
|
||||||
uid = bundle.uid,
|
uid = bundle.uid,
|
||||||
patches = bundle.supported.searched(),
|
patches = bundle.compatible.searched(),
|
||||||
visible = true,
|
visible = true,
|
||||||
supported = true
|
compatible = true
|
||||||
)
|
)
|
||||||
patchList(
|
patchList(
|
||||||
uid = bundle.uid,
|
uid = bundle.uid,
|
||||||
patches = bundle.universal.searched(),
|
patches = bundle.universal.searched(),
|
||||||
visible = vm.filter and SHOW_UNIVERSAL != 0,
|
visible = viewModel.filter and SHOW_UNIVERSAL != 0,
|
||||||
supported = true
|
compatible = true
|
||||||
) {
|
) {
|
||||||
ListHeader(
|
ListHeader(
|
||||||
title = stringResource(R.string.universal_patches),
|
title = stringResource(R.string.universal_patches),
|
||||||
@@ -338,13 +330,13 @@ fun PatchesSelectorScreen(
|
|||||||
|
|
||||||
patchList(
|
patchList(
|
||||||
uid = bundle.uid,
|
uid = bundle.uid,
|
||||||
patches = bundle.unsupported.searched(),
|
patches = bundle.incompatible.searched(),
|
||||||
visible = vm.filter and SHOW_UNSUPPORTED != 0,
|
visible = viewModel.filter and SHOW_INCOMPATIBLE != 0,
|
||||||
supported = vm.allowIncompatiblePatches
|
compatible = viewModel.allowIncompatiblePatches
|
||||||
) {
|
) {
|
||||||
ListHeader(
|
ListHeader(
|
||||||
title = stringResource(R.string.unsupported_patches),
|
title = stringResource(R.string.incompatible_patches),
|
||||||
onHelpClick = { showUnsupportedPatchesDialog = true }
|
onHelpClick = { showIncompatiblePatchesDialog = true }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -363,22 +355,30 @@ fun PatchesSelectorScreen(
|
|||||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
) {
|
) {
|
||||||
SmallFloatingActionButton(
|
SmallFloatingActionButton(
|
||||||
onClick = vm::reset,
|
onClick = viewModel::reset,
|
||||||
containerColor = MaterialTheme.colorScheme.tertiaryContainer
|
containerColor = MaterialTheme.colorScheme.tertiaryContainer
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Outlined.Restore, stringResource(R.string.reset))
|
Icon(Icons.Outlined.Restore, stringResource(R.string.reset))
|
||||||
}
|
}
|
||||||
HapticExtendedFloatingActionButton(
|
HapticExtendedFloatingActionButton(
|
||||||
text = { Text(stringResource(R.string.save_with_count, selectedPatchCount)) },
|
text = {
|
||||||
|
Text(
|
||||||
|
stringResource(
|
||||||
|
R.string.save_with_count,
|
||||||
|
selectedPatchCount
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
icon = {
|
icon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.Save,
|
imageVector = Icons.Outlined.Save,
|
||||||
contentDescription = stringResource(R.string.save)
|
contentDescription = stringResource(R.string.save)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
expanded = patchLazyListStates.getOrNull(pagerState.currentPage)?.isScrollingUp ?: true,
|
expanded = patchLazyListStates.getOrNull(pagerState.currentPage)?.isScrollingUp
|
||||||
|
?: true,
|
||||||
onClick = {
|
onClick = {
|
||||||
onSave(vm.getCustomSelection(), vm.getOptions())
|
onSave(viewModel.getCustomSelection(), viewModel.getOptions())
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -389,6 +389,7 @@ fun PatchesSelectorScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
|
.padding(top = 16.dp)
|
||||||
) {
|
) {
|
||||||
if (bundles.size > 1) {
|
if (bundles.size > 1) {
|
||||||
ScrollableTabRow(
|
ScrollableTabRow(
|
||||||
@@ -405,7 +406,19 @@ fun PatchesSelectorScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
text = { Text(bundle.name) },
|
text = {
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
Text(
|
||||||
|
text = bundle.name,
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = bundle.version.orEmpty(),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
selectedContentColor = MaterialTheme.colorScheme.primary,
|
selectedContentColor = MaterialTheme.colorScheme.primary,
|
||||||
unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant
|
unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
@@ -427,15 +440,15 @@ fun PatchesSelectorScreen(
|
|||||||
) {
|
) {
|
||||||
patchList(
|
patchList(
|
||||||
uid = bundle.uid,
|
uid = bundle.uid,
|
||||||
patches = bundle.supported,
|
patches = bundle.compatible,
|
||||||
visible = true,
|
visible = true,
|
||||||
supported = true
|
compatible = true
|
||||||
)
|
)
|
||||||
patchList(
|
patchList(
|
||||||
uid = bundle.uid,
|
uid = bundle.uid,
|
||||||
patches = bundle.universal,
|
patches = bundle.universal,
|
||||||
visible = vm.filter and SHOW_UNIVERSAL != 0,
|
visible = viewModel.filter and SHOW_UNIVERSAL != 0,
|
||||||
supported = true
|
compatible = true
|
||||||
) {
|
) {
|
||||||
ListHeader(
|
ListHeader(
|
||||||
title = stringResource(R.string.universal_patches),
|
title = stringResource(R.string.universal_patches),
|
||||||
@@ -443,13 +456,13 @@ fun PatchesSelectorScreen(
|
|||||||
}
|
}
|
||||||
patchList(
|
patchList(
|
||||||
uid = bundle.uid,
|
uid = bundle.uid,
|
||||||
patches = bundle.unsupported,
|
patches = bundle.incompatible,
|
||||||
visible = vm.filter and SHOW_UNSUPPORTED != 0,
|
visible = viewModel.filter and SHOW_INCOMPATIBLE != 0,
|
||||||
supported = vm.allowIncompatiblePatches
|
compatible = viewModel.allowIncompatiblePatches
|
||||||
) {
|
) {
|
||||||
ListHeader(
|
ListHeader(
|
||||||
title = stringResource(R.string.unsupported_patches),
|
title = stringResource(R.string.incompatible_patches),
|
||||||
onHelpClick = { showUnsupportedPatchesDialog = true }
|
onHelpClick = { showIncompatiblePatchesDialog = true }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -460,43 +473,13 @@ fun PatchesSelectorScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SelectionWarningDialog(onDismiss: () -> Unit) {
|
private fun UniversalPatchWarningDialog(
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
SafeguardDialog(
|
SafeguardDialog(
|
||||||
onDismiss = onDismiss,
|
onDismiss = onDismiss,
|
||||||
title = R.string.warning,
|
title = R.string.warning,
|
||||||
body = stringResource(R.string.selection_warning_description),
|
body = stringResource(R.string.universal_patch_warning_description),
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun UniversalPatchWarningDialog(
|
|
||||||
onCancel: () -> Unit,
|
|
||||||
onConfirm: () -> Unit
|
|
||||||
) {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = onCancel,
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(onClick = onConfirm) {
|
|
||||||
Text(stringResource(R.string.continue_))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(onClick = onCancel) {
|
|
||||||
Text(stringResource(R.string.cancel))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon = {
|
|
||||||
Icon(Icons.Outlined.WarningAmber, null)
|
|
||||||
},
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.warning),
|
|
||||||
style = MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
text = {
|
|
||||||
Text(stringResource(R.string.universal_patch_warning_description))
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -506,24 +489,24 @@ private fun PatchItem(
|
|||||||
onOptionsDialog: () -> Unit,
|
onOptionsDialog: () -> Unit,
|
||||||
selected: Boolean,
|
selected: Boolean,
|
||||||
onToggle: () -> Unit,
|
onToggle: () -> Unit,
|
||||||
supported: Boolean = true
|
compatible: Boolean = true
|
||||||
) = ListItem(
|
) = ListItem(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.let { if (!supported) it.alpha(0.5f) else it }
|
.let { if (!compatible) it.alpha(0.5f) else it }
|
||||||
.clickable(onClick = onToggle)
|
.clickable(onClick = onToggle)
|
||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
leadingContent = {
|
leadingContent = {
|
||||||
HapticCheckbox(
|
HapticCheckbox(
|
||||||
checked = selected,
|
checked = selected,
|
||||||
onCheckedChange = { onToggle() },
|
onCheckedChange = { onToggle() },
|
||||||
enabled = supported
|
enabled = compatible
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
headlineContent = { Text(patch.name) },
|
headlineContent = { Text(patch.name) },
|
||||||
supportingContent = patch.description?.let { { Text(it) } },
|
supportingContent = patch.description?.let { { Text(it) } },
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
if (patch.options?.isNotEmpty() == true) {
|
if (patch.options?.isNotEmpty() == true) {
|
||||||
IconButton(onClick = onOptionsDialog, enabled = supported) {
|
IconButton(onClick = onOptionsDialog, enabled = compatible) {
|
||||||
Icon(Icons.Outlined.Settings, null)
|
Icon(Icons.Outlined.Settings, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -559,7 +542,7 @@ fun ListHeader(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun UnsupportedPatchesDialog(
|
private fun IncompatiblePatchesDialog(
|
||||||
appVersion: String,
|
appVersion: String,
|
||||||
onDismissRequest: () -> Unit
|
onDismissRequest: () -> Unit
|
||||||
) = AlertDialog(
|
) = AlertDialog(
|
||||||
@@ -572,11 +555,11 @@ private fun UnsupportedPatchesDialog(
|
|||||||
Text(stringResource(R.string.ok))
|
Text(stringResource(R.string.ok))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
title = { Text(stringResource(R.string.unsupported_patches)) },
|
title = { Text(stringResource(R.string.incompatible_patches)) },
|
||||||
text = {
|
text = {
|
||||||
Text(
|
Text(
|
||||||
stringResource(
|
stringResource(
|
||||||
R.string.unsupported_patches_dialog,
|
R.string.incompatible_patches_dialog,
|
||||||
appVersion
|
appVersion
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -584,9 +567,9 @@ private fun UnsupportedPatchesDialog(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun UnsupportedPatchDialog(
|
private fun IncompatiblePatchDialog(
|
||||||
appVersion: String,
|
appVersion: String,
|
||||||
supportedVersions: List<String>,
|
compatibleVersions: List<String>,
|
||||||
onDismissRequest: () -> Unit
|
onDismissRequest: () -> Unit
|
||||||
) = AlertDialog(
|
) = AlertDialog(
|
||||||
icon = {
|
icon = {
|
||||||
@@ -598,13 +581,13 @@ private fun UnsupportedPatchDialog(
|
|||||||
Text(stringResource(R.string.ok))
|
Text(stringResource(R.string.ok))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
title = { Text(stringResource(R.string.unsupported_patch)) },
|
title = { Text(stringResource(R.string.incompatible_patch)) },
|
||||||
text = {
|
text = {
|
||||||
Text(
|
Text(
|
||||||
stringResource(
|
stringResource(
|
||||||
R.string.app_not_supported,
|
R.string.app_version_not_compatible,
|
||||||
appVersion,
|
appVersion,
|
||||||
supportedVersions.joinToString(", ")
|
compatibleVersions.joinToString(", ")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -618,13 +601,8 @@ private fun OptionsDialog(
|
|||||||
reset: () -> Unit,
|
reset: () -> Unit,
|
||||||
set: (String, Any?) -> Unit,
|
set: (String, Any?) -> Unit,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
) = Dialog(
|
selectionWarningEnabled: Boolean
|
||||||
onDismissRequest = onDismissRequest,
|
) = FullscreenDialog(onDismissRequest = onDismissRequest) {
|
||||||
properties = DialogProperties(
|
|
||||||
usePlatformDefaultWidth = false,
|
|
||||||
dismissOnBackPress = true
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopBar(
|
AppTopBar(
|
||||||
@@ -654,7 +632,8 @@ private fun OptionsDialog(
|
|||||||
value = value,
|
value = value,
|
||||||
setValue = {
|
setValue = {
|
||||||
set(key, it)
|
set(key, it)
|
||||||
}
|
},
|
||||||
|
selectionWarningEnabled = selectionWarningEnabled
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,12 +30,12 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.patcher.patch.Option
|
import app.revanced.manager.patcher.patch.Option
|
||||||
|
import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.requiredOptionsSet
|
||||||
import app.revanced.manager.ui.component.AppTopBar
|
import app.revanced.manager.ui.component.AppTopBar
|
||||||
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
|
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
|
||||||
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
||||||
import app.revanced.manager.ui.component.haptics.HapticTab
|
import app.revanced.manager.ui.component.haptics.HapticTab
|
||||||
import app.revanced.manager.ui.component.patches.OptionItem
|
import app.revanced.manager.ui.component.patches.OptionItem
|
||||||
import app.revanced.manager.ui.model.BundleInfo.Extensions.requiredOptionsSet
|
|
||||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
|
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
|
||||||
import app.revanced.manager.util.Options
|
import app.revanced.manager.util.Options
|
||||||
import app.revanced.manager.util.PatchSelection
|
import app.revanced.manager.util.PatchSelection
|
||||||
@@ -62,6 +62,7 @@ fun RequiredOptionsScreen(
|
|||||||
val showContinueButton by remember {
|
val showContinueButton by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
bundles.requiredOptionsSet(
|
bundles.requiredOptionsSet(
|
||||||
|
allowIncompatible = vm.allowIncompatiblePatches,
|
||||||
isSelected = { bundle, patch -> vm.isSelected(bundle.uid, patch) },
|
isSelected = { bundle, patch -> vm.isSelected(bundle.uid, patch) },
|
||||||
optionsForPatch = { bundle, patch -> vm.getOptions(bundle.uid, patch) }
|
optionsForPatch = { bundle, patch -> vm.getOptions(bundle.uid, patch) }
|
||||||
)
|
)
|
||||||
@@ -153,7 +154,8 @@ fun RequiredOptionsScreen(
|
|||||||
value = value,
|
value = value,
|
||||||
setValue = { new ->
|
setValue = { new ->
|
||||||
vm.setOption(bundle.uid, it, key, new)
|
vm.setOption(bundle.uid, it, key, new)
|
||||||
}
|
},
|
||||||
|
selectionWarningEnabled = vm.selectionWarningEnabled
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
|||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
@@ -12,6 +14,7 @@ import androidx.compose.foundation.lazy.items
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.outlined.ArrowRight
|
import androidx.compose.material.icons.automirrored.outlined.ArrowRight
|
||||||
import androidx.compose.material.icons.filled.AutoFixHigh
|
import androidx.compose.material.icons.filled.AutoFixHigh
|
||||||
|
import androidx.compose.material.icons.outlined.WarningAmber
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
@@ -32,6 +35,7 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
|
import app.revanced.manager.data.platform.NetworkInfo
|
||||||
import app.revanced.manager.data.room.apps.installed.InstallType
|
import app.revanced.manager.data.room.apps.installed.InstallType
|
||||||
import app.revanced.manager.data.room.apps.installed.InstalledApp
|
import app.revanced.manager.data.room.apps.installed.InstalledApp
|
||||||
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
|
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
|
||||||
@@ -40,6 +44,7 @@ import app.revanced.manager.ui.component.AppInfo
|
|||||||
import app.revanced.manager.ui.component.AppTopBar
|
import app.revanced.manager.ui.component.AppTopBar
|
||||||
import app.revanced.manager.ui.component.ColumnWithScrollbar
|
import app.revanced.manager.ui.component.ColumnWithScrollbar
|
||||||
import app.revanced.manager.ui.component.LoadingIndicator
|
import app.revanced.manager.ui.component.LoadingIndicator
|
||||||
|
import app.revanced.manager.ui.component.NotificationCard
|
||||||
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
||||||
import app.revanced.manager.ui.model.SelectedApp
|
import app.revanced.manager.ui.model.SelectedApp
|
||||||
import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel
|
import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel
|
||||||
@@ -50,6 +55,7 @@ import app.revanced.manager.util.enabled
|
|||||||
import app.revanced.manager.util.toast
|
import app.revanced.manager.util.toast
|
||||||
import app.revanced.manager.util.transparentListItemColors
|
import app.revanced.manager.util.transparentListItemColors
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koin.compose.koinInject
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -61,6 +67,9 @@ fun SelectedAppInfoScreen(
|
|||||||
vm: SelectedAppInfoViewModel
|
vm: SelectedAppInfoViewModel
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val networkInfo = koinInject<NetworkInfo>()
|
||||||
|
val networkConnected = remember { networkInfo.isConnected() }
|
||||||
|
val networkMetered = remember { !networkInfo.isUnmetered() }
|
||||||
|
|
||||||
val packageName = vm.selectedApp.packageName
|
val packageName = vm.selectedApp.packageName
|
||||||
val version = vm.selectedApp.version
|
val version = vm.selectedApp.version
|
||||||
@@ -208,6 +217,35 @@ fun SelectedAppInfoScreen(
|
|||||||
modifier = Modifier.padding(horizontal = 24.dp)
|
modifier = Modifier.padding(horizontal = 24.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
val needsInternet =
|
||||||
|
vm.selectedApp.let { it is SelectedApp.Search || it is SelectedApp.Download }
|
||||||
|
|
||||||
|
when {
|
||||||
|
!needsInternet -> {}
|
||||||
|
!networkConnected -> {
|
||||||
|
NotificationCard(
|
||||||
|
isWarning = true,
|
||||||
|
icon = Icons.Outlined.WarningAmber,
|
||||||
|
text = stringResource(R.string.network_unavailable_warning),
|
||||||
|
onDismiss = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
networkMetered -> {
|
||||||
|
NotificationCard(
|
||||||
|
isWarning = true,
|
||||||
|
icon = Icons.Outlined.WarningAmber,
|
||||||
|
text = stringResource(R.string.network_metered_warning),
|
||||||
|
onDismiss = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,57 +1,86 @@
|
|||||||
package app.revanced.manager.ui.screen
|
package app.revanced.manager.ui.screen
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.Download
|
import androidx.compose.material.icons.outlined.*
|
||||||
import androidx.compose.material.icons.outlined.Info
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material.icons.outlined.Settings
|
|
||||||
import androidx.compose.material.icons.outlined.SwapVert
|
|
||||||
import androidx.compose.material.icons.outlined.Tune
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
|
import app.revanced.manager.domain.manager.PreferencesManager
|
||||||
import app.revanced.manager.ui.component.AppTopBar
|
import app.revanced.manager.ui.component.AppTopBar
|
||||||
import app.revanced.manager.ui.component.ColumnWithScrollbar
|
import app.revanced.manager.ui.component.ColumnWithScrollbar
|
||||||
import app.revanced.manager.ui.component.settings.SettingsListItem
|
import app.revanced.manager.ui.component.settings.SettingsListItem
|
||||||
import app.revanced.manager.ui.model.navigation.Settings
|
import app.revanced.manager.ui.model.navigation.Settings
|
||||||
|
import org.koin.compose.koinInject
|
||||||
|
|
||||||
private val settingsSections = listOf(
|
private data class Section(
|
||||||
Triple(
|
@StringRes val name: Int,
|
||||||
R.string.general,
|
@StringRes val description: Int,
|
||||||
R.string.general_description,
|
val image: ImageVector,
|
||||||
Icons.Outlined.Settings
|
val destination: Settings.Destination,
|
||||||
) to Settings.General,
|
|
||||||
Triple(
|
|
||||||
R.string.extensions,
|
|
||||||
R.string.extensions_description,
|
|
||||||
Icons.Outlined.Download
|
|
||||||
) to Settings.Downloads,
|
|
||||||
Triple(
|
|
||||||
R.string.backup_restore,
|
|
||||||
R.string.backup_restore_description,
|
|
||||||
Icons.Outlined.SwapVert
|
|
||||||
) to Settings.ImportExport,
|
|
||||||
Triple(
|
|
||||||
R.string.advanced,
|
|
||||||
R.string.advanced_description,
|
|
||||||
Icons.Outlined.Tune
|
|
||||||
) to Settings.Advanced,
|
|
||||||
Triple(
|
|
||||||
R.string.about,
|
|
||||||
R.string.app_name,
|
|
||||||
Icons.Outlined.Info
|
|
||||||
) to Settings.About,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsScreen(onBackClick: () -> Unit, navigate: (Settings.Destination) -> Unit) {
|
fun SettingsScreen(onBackClick: () -> Unit, navigate: (Settings.Destination) -> Unit) {
|
||||||
|
val prefs: PreferencesManager = koinInject()
|
||||||
|
val showDeveloperSettings by prefs.showDeveloperSettings.getAsState()
|
||||||
|
|
||||||
|
val settingsSections = remember(showDeveloperSettings) {
|
||||||
|
listOfNotNull(
|
||||||
|
Section(
|
||||||
|
R.string.general,
|
||||||
|
R.string.general_description,
|
||||||
|
Icons.Outlined.Settings,
|
||||||
|
Settings.General
|
||||||
|
),
|
||||||
|
Section(
|
||||||
|
R.string.updates,
|
||||||
|
R.string.updates_description,
|
||||||
|
Icons.Outlined.Update,
|
||||||
|
Settings.Updates
|
||||||
|
),
|
||||||
|
Section(
|
||||||
|
R.string.downloads,
|
||||||
|
R.string.downloads_description,
|
||||||
|
Icons.Outlined.Download,
|
||||||
|
Settings.Downloads
|
||||||
|
),
|
||||||
|
Section(
|
||||||
|
R.string.import_export,
|
||||||
|
R.string.import_export_description,
|
||||||
|
Icons.Outlined.SwapVert,
|
||||||
|
Settings.ImportExport
|
||||||
|
),
|
||||||
|
Section(
|
||||||
|
R.string.advanced,
|
||||||
|
R.string.advanced_description,
|
||||||
|
Icons.Outlined.Tune,
|
||||||
|
Settings.Advanced
|
||||||
|
),
|
||||||
|
Section(
|
||||||
|
R.string.about,
|
||||||
|
R.string.app_name,
|
||||||
|
Icons.Outlined.Info,
|
||||||
|
Settings.About
|
||||||
|
),
|
||||||
|
Section(
|
||||||
|
R.string.developer_options,
|
||||||
|
R.string.developer_options_description,
|
||||||
|
Icons.Outlined.Code,
|
||||||
|
Settings.Developer
|
||||||
|
).takeIf { showDeveloperSettings }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopBar(
|
AppTopBar(
|
||||||
@@ -65,12 +94,12 @@ fun SettingsScreen(onBackClick: () -> Unit, navigate: (Settings.Destination) ->
|
|||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
settingsSections.forEach { (titleDescIcon, destination) ->
|
settingsSections.forEach { (name, description, icon, destination) ->
|
||||||
SettingsListItem(
|
SettingsListItem(
|
||||||
modifier = Modifier.clickable { navigate(destination) },
|
modifier = Modifier.clickable { navigate(destination) },
|
||||||
headlineContent = stringResource(titleDescIcon.first),
|
headlineContent = stringResource(name),
|
||||||
supportingContent = stringResource(titleDescIcon.second),
|
supportingContent = stringResource(description),
|
||||||
leadingContent = { Icon(titleDescIcon.third, null) }
|
leadingContent = { Icon(icon, null) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,19 +5,17 @@ import androidx.compose.animation.core.spring
|
|||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Cancel
|
||||||
|
import androidx.compose.material.icons.outlined.InstallMobile
|
||||||
import androidx.compose.material.icons.outlined.Update
|
import androidx.compose.material.icons.outlined.Update
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.HorizontalDivider
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.LinearProgressIndicator
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -28,16 +26,15 @@ import androidx.compose.material3.TopAppBarDefaults
|
|||||||
import androidx.compose.material3.rememberTopAppBarState
|
import androidx.compose.material3.rememberTopAppBarState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import app.revanced.manager.BuildConfig
|
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.network.dto.ReVancedAsset
|
import app.revanced.manager.network.dto.ReVancedAsset
|
||||||
import app.revanced.manager.ui.component.AppTopBar
|
import app.revanced.manager.ui.component.AppTopBar
|
||||||
|
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
||||||
import app.revanced.manager.ui.component.settings.Changelog
|
import app.revanced.manager.ui.component.settings.Changelog
|
||||||
import app.revanced.manager.ui.viewmodel.UpdateViewModel
|
import app.revanced.manager.ui.viewmodel.UpdateViewModel
|
||||||
import app.revanced.manager.ui.viewmodel.UpdateViewModel.State
|
import app.revanced.manager.ui.viewmodel.UpdateViewModel.State
|
||||||
@@ -60,37 +57,81 @@ fun UpdateScreen(
|
|||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopBar(
|
AppTopBar(
|
||||||
title = stringResource(R.string.update),
|
title = {
|
||||||
|
Column {
|
||||||
|
Text(stringResource(vm.state.title))
|
||||||
|
|
||||||
|
if (vm.state == State.DOWNLOADING) {
|
||||||
|
Text(
|
||||||
|
text = "${vm.downloadedSize.div(1000000)} MB / ${
|
||||||
|
vm.totalSize.div(1000000)
|
||||||
|
} MB (${vm.downloadProgress.times(100).toInt()}%)",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.outline
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
onBackClick = onBackClick
|
onBackClick = onBackClick
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
floatingActionButton = {
|
||||||
|
val buttonConfig = when (vm.state) {
|
||||||
|
State.CAN_DOWNLOAD -> Triple(
|
||||||
|
{ vm.downloadUpdate() },
|
||||||
|
R.string.download,
|
||||||
|
Icons.Outlined.InstallMobile
|
||||||
|
)
|
||||||
|
|
||||||
|
State.DOWNLOADING -> Triple(onBackClick, R.string.cancel, Icons.Outlined.Cancel)
|
||||||
|
State.CAN_INSTALL -> Triple(
|
||||||
|
{ vm.installUpdate() },
|
||||||
|
R.string.install_app,
|
||||||
|
Icons.Outlined.InstallMobile
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
buttonConfig?.let { (onClick, textRes, icon) ->
|
||||||
|
HapticExtendedFloatingActionButton(
|
||||||
|
onClick = onClick::invoke,
|
||||||
|
icon = { Icon(icon, null) },
|
||||||
|
text = { Text(stringResource(textRes)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
AnimatedVisibility(visible = vm.showInternetCheckDialog) {
|
|
||||||
MeteredDownloadConfirmationDialog(
|
|
||||||
onDismiss = { vm.showInternetCheckDialog = false },
|
|
||||||
onDownloadAnyways = { vm.downloadUpdate(true) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.padding(paddingValues),
|
||||||
.padding(paddingValues)
|
|
||||||
.padding(vertical = 16.dp, horizontal = 24.dp)
|
|
||||||
.verticalScroll(rememberScrollState()),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(32.dp)
|
|
||||||
) {
|
) {
|
||||||
Header(
|
if (vm.state == State.DOWNLOADING)
|
||||||
vm.state,
|
LinearProgressIndicator(
|
||||||
vm.releaseInfo,
|
progress = { vm.downloadProgress },
|
||||||
DownloadData(vm.downloadProgress, vm.downloadedSize, vm.totalSize)
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
vm.releaseInfo?.let { changelog ->
|
|
||||||
HorizontalDivider()
|
AnimatedVisibility(visible = vm.showInternetCheckDialog) {
|
||||||
Changelog(changelog)
|
MeteredDownloadConfirmationDialog(
|
||||||
} ?: Spacer(modifier = Modifier.weight(1f))
|
onDismiss = { vm.showInternetCheckDialog = false },
|
||||||
Buttons(vm.state, vm::downloadUpdate, vm::installUpdate, onBackClick)
|
onDownloadAnyways = { vm.downloadUpdate(true) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp)
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(32.dp)
|
||||||
|
) {
|
||||||
|
vm.releaseInfo?.let { changelog ->
|
||||||
|
Changelog(changelog)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -123,58 +164,6 @@ private fun MeteredDownloadConfirmationDialog(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun Header(state: State, releaseInfo: ReVancedAsset?, downloadData: DownloadData) {
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(state.title),
|
|
||||||
style = MaterialTheme.typography.headlineMedium
|
|
||||||
)
|
|
||||||
if (state == State.CAN_DOWNLOAD) {
|
|
||||||
Column {
|
|
||||||
Text(
|
|
||||||
text = stringResource(
|
|
||||||
id = R.string.current_version,
|
|
||||||
BuildConfig.VERSION_NAME
|
|
||||||
),
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
releaseInfo?.version?.let {
|
|
||||||
Text(
|
|
||||||
text = stringResource(
|
|
||||||
R.string.new_version,
|
|
||||||
it.replace("v", "")
|
|
||||||
),
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (state == State.DOWNLOADING) {
|
|
||||||
LinearProgressIndicator(
|
|
||||||
progress = { downloadData.downloadProgress },
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text =
|
|
||||||
"${downloadData.downloadedSize.div(1000000)} MB / ${
|
|
||||||
downloadData.totalSize.div(
|
|
||||||
1000000
|
|
||||||
)
|
|
||||||
} MB (${
|
|
||||||
downloadData.downloadProgress.times(
|
|
||||||
100
|
|
||||||
).toInt()
|
|
||||||
}%)",
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.outline,
|
|
||||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ColumnScope.Changelog(releaseInfo: ReVancedAsset) {
|
private fun ColumnScope.Changelog(releaseInfo: ReVancedAsset) {
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
@@ -205,40 +194,4 @@ private fun ColumnScope.Changelog(releaseInfo: ReVancedAsset) {
|
|||||||
publishDate = releaseInfo.createdAt.relativeTime(LocalContext.current)
|
publishDate = releaseInfo.createdAt.relativeTime(LocalContext.current)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun Buttons(
|
|
||||||
state: State,
|
|
||||||
onDownloadClick: () -> Unit,
|
|
||||||
onInstallClick: () -> Unit,
|
|
||||||
onBackClick: () -> Unit
|
|
||||||
) {
|
|
||||||
Row(modifier = Modifier.fillMaxWidth()) {
|
|
||||||
if (state.showCancel) {
|
|
||||||
TextButton(
|
|
||||||
onClick = onBackClick,
|
|
||||||
) {
|
|
||||||
Text(text = stringResource(R.string.cancel))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
|
||||||
if (state == State.CAN_DOWNLOAD) {
|
|
||||||
Button(onClick = onDownloadClick) {
|
|
||||||
Text(text = stringResource(R.string.update))
|
|
||||||
}
|
|
||||||
} else if (state == State.CAN_INSTALL) {
|
|
||||||
Button(
|
|
||||||
onClick = onInstallClick
|
|
||||||
) {
|
|
||||||
Text(text = stringResource(R.string.install_app))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class DownloadData(
|
|
||||||
val downloadProgress: Float,
|
|
||||||
val downloadedSize: Long,
|
|
||||||
val totalSize: Long
|
|
||||||
)
|
|
||||||
@@ -23,16 +23,26 @@ import androidx.compose.material3.IconButton
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedCard
|
import androidx.compose.material3.OutlinedCard
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.SnackbarDuration
|
||||||
|
import androidx.compose.material3.SnackbarHost
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.rememberTopAppBarState
|
import androidx.compose.material3.rememberTopAppBarState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.semantics.hideFromAccessibility
|
||||||
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import app.revanced.manager.BuildConfig
|
import app.revanced.manager.BuildConfig
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
@@ -42,8 +52,10 @@ import app.revanced.manager.ui.component.ColumnWithScrollbar
|
|||||||
import app.revanced.manager.ui.component.settings.SettingsListItem
|
import app.revanced.manager.ui.component.settings.SettingsListItem
|
||||||
import app.revanced.manager.ui.model.navigation.Settings
|
import app.revanced.manager.ui.model.navigation.Settings
|
||||||
import app.revanced.manager.ui.viewmodel.AboutViewModel
|
import app.revanced.manager.ui.viewmodel.AboutViewModel
|
||||||
|
import app.revanced.manager.ui.viewmodel.AboutViewModel.Companion.DEVELOPER_OPTIONS_TAPS
|
||||||
import app.revanced.manager.ui.viewmodel.AboutViewModel.Companion.getSocialIcon
|
import app.revanced.manager.ui.viewmodel.AboutViewModel.Companion.getSocialIcon
|
||||||
import app.revanced.manager.util.openUrl
|
import app.revanced.manager.util.openUrl
|
||||||
|
import app.revanced.manager.util.toast
|
||||||
import com.google.accompanist.drawablepainter.rememberDrawablePainter
|
import com.google.accompanist.drawablepainter.rememberDrawablePainter
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
|
||||||
@@ -51,7 +63,6 @@ import org.koin.androidx.compose.koinViewModel
|
|||||||
@Composable
|
@Composable
|
||||||
fun AboutSettingsScreen(
|
fun AboutSettingsScreen(
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
onChangelogClick: () -> Unit,
|
|
||||||
navigate: (Settings.Destination) -> Unit,
|
navigate: (Settings.Destination) -> Unit,
|
||||||
viewModel: AboutViewModel = koinViewModel()
|
viewModel: AboutViewModel = koinViewModel()
|
||||||
) {
|
) {
|
||||||
@@ -110,11 +121,7 @@ fun AboutSettingsScreen(
|
|||||||
|
|
||||||
val listItems = listOfNotNull(
|
val listItems = listOfNotNull(
|
||||||
Triple(
|
Triple(
|
||||||
stringResource(R.string.changelog),
|
stringResource(R.string.submit_feedback),
|
||||||
stringResource(R.string.changelog_description),
|
|
||||||
third = { onChangelogClick }
|
|
||||||
),
|
|
||||||
Triple(stringResource(R.string.submit_feedback),
|
|
||||||
stringResource(R.string.submit_feedback_description),
|
stringResource(R.string.submit_feedback_description),
|
||||||
third = {
|
third = {
|
||||||
context.openUrl("https://github.com/ReVanced/revanced-manager/issues/new/choose")
|
context.openUrl("https://github.com/ReVanced/revanced-manager/issues/new/choose")
|
||||||
@@ -122,12 +129,14 @@ fun AboutSettingsScreen(
|
|||||||
Triple(
|
Triple(
|
||||||
stringResource(R.string.contributors),
|
stringResource(R.string.contributors),
|
||||||
stringResource(R.string.contributors_description),
|
stringResource(R.string.contributors_description),
|
||||||
third = { navigate(Settings.Contributors) }
|
third = nav@{
|
||||||
),
|
if (!viewModel.isConnected) {
|
||||||
Triple(
|
context.toast(context.getString(R.string.no_network_toast))
|
||||||
stringResource(R.string.developer_options),
|
return@nav
|
||||||
stringResource(R.string.developer_options_description),
|
}
|
||||||
third = { navigate(Settings.DeveloperOptions) }
|
|
||||||
|
navigate(Settings.Contributors)
|
||||||
|
}
|
||||||
),
|
),
|
||||||
Triple(
|
Triple(
|
||||||
stringResource(R.string.opensource_licenses),
|
stringResource(R.string.opensource_licenses),
|
||||||
@@ -137,6 +146,35 @@ fun AboutSettingsScreen(
|
|||||||
)
|
)
|
||||||
|
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
|
val showDeveloperSettings by viewModel.showDeveloperSettings.getAsState()
|
||||||
|
var developerTaps by rememberSaveable { mutableIntStateOf(0) }
|
||||||
|
LaunchedEffect(developerTaps) {
|
||||||
|
if (developerTaps == 0) return@LaunchedEffect
|
||||||
|
if (showDeveloperSettings) {
|
||||||
|
snackbarHostState.showSnackbar(context.getString(R.string.developer_options_already_enabled))
|
||||||
|
developerTaps = 0
|
||||||
|
return@LaunchedEffect
|
||||||
|
}
|
||||||
|
|
||||||
|
val remaining = DEVELOPER_OPTIONS_TAPS - developerTaps
|
||||||
|
if (remaining > 0) {
|
||||||
|
snackbarHostState.showSnackbar(
|
||||||
|
context.getString(
|
||||||
|
R.string.developer_options_taps,
|
||||||
|
remaining
|
||||||
|
),
|
||||||
|
duration = SnackbarDuration.Long
|
||||||
|
)
|
||||||
|
} else if (remaining == 0) {
|
||||||
|
viewModel.showDeveloperSettings.update(true)
|
||||||
|
snackbarHostState.showSnackbar(context.getString(R.string.developer_options_enabled))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the counter
|
||||||
|
developerTaps = 0
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
@@ -146,6 +184,9 @@ fun AboutSettingsScreen(
|
|||||||
onBackClick = onBackClick
|
onBackClick = onBackClick
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
snackbarHost = {
|
||||||
|
SnackbarHost(hostState = snackbarHostState)
|
||||||
|
},
|
||||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
ColumnWithScrollbar(
|
ColumnWithScrollbar(
|
||||||
@@ -156,9 +197,11 @@ fun AboutSettingsScreen(
|
|||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
modifier = Modifier.padding(top = 16.dp),
|
modifier = Modifier
|
||||||
|
.padding(top = 16.dp)
|
||||||
|
.clickable { developerTaps += 1 },
|
||||||
painter = icon,
|
painter = icon,
|
||||||
contentDescription = null
|
contentDescription = stringResource(R.string.app_name)
|
||||||
)
|
)
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
@@ -166,7 +209,11 @@ fun AboutSettingsScreen(
|
|||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.app_name),
|
stringResource(R.string.app_name),
|
||||||
style = MaterialTheme.typography.headlineSmall
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
modifier = Modifier.semantics {
|
||||||
|
// Icon already has this information for the purpose of being clickable.
|
||||||
|
hideFromAccessibility()
|
||||||
|
}
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.version) + " " + BuildConfig.VERSION_NAME + " (" + BuildConfig.VERSION_CODE + ")",
|
text = stringResource(R.string.version) + " " + BuildConfig.VERSION_NAME + " (" + BuildConfig.VERSION_CODE + ")",
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.Api
|
import androidx.compose.material.icons.outlined.Api
|
||||||
import androidx.compose.material.icons.outlined.Edit
|
|
||||||
import androidx.compose.material.icons.outlined.Restore
|
import androidx.compose.material.icons.outlined.Restore
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
@@ -62,7 +61,7 @@ import org.koin.androidx.compose.koinViewModel
|
|||||||
@Composable
|
@Composable
|
||||||
fun AdvancedSettingsScreen(
|
fun AdvancedSettingsScreen(
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
vm: AdvancedSettingsViewModel = koinViewModel()
|
viewModel: AdvancedSettingsViewModel = koinViewModel()
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val memoryLimit = remember {
|
val memoryLimit = remember {
|
||||||
@@ -92,95 +91,79 @@ fun AdvancedSettingsScreen(
|
|||||||
) {
|
) {
|
||||||
GroupHeader(stringResource(R.string.manager))
|
GroupHeader(stringResource(R.string.manager))
|
||||||
|
|
||||||
val apiUrl by vm.prefs.api.getAsState()
|
val apiUrl by viewModel.prefs.api.getAsState()
|
||||||
var showApiUrlDialog by rememberSaveable { mutableStateOf(false) }
|
var showApiUrlDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
if (showApiUrlDialog) {
|
if (showApiUrlDialog) {
|
||||||
APIUrlDialog(
|
APIUrlDialog(
|
||||||
currentUrl = apiUrl,
|
currentUrl = apiUrl,
|
||||||
defaultUrl = vm.prefs.api.default,
|
defaultUrl = viewModel.prefs.api.default,
|
||||||
onSubmit = {
|
onSubmit = {
|
||||||
showApiUrlDialog = false
|
showApiUrlDialog = false
|
||||||
it?.let(vm::setApiSource)
|
it?.let(viewModel::setApiUrl)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
GroupHeader(stringResource(R.string.patcher))
|
|
||||||
BooleanItem(
|
|
||||||
preference = vm.prefs.useProcessRuntime,
|
|
||||||
coroutineScope = vm.viewModelScope,
|
|
||||||
headline = R.string.process_runtime,
|
|
||||||
description = R.string.process_runtime_description,
|
|
||||||
)
|
|
||||||
IntegerItem(
|
|
||||||
preference = vm.prefs.patcherProcessMemoryLimit,
|
|
||||||
coroutineScope = vm.viewModelScope,
|
|
||||||
headline = R.string.process_runtime_memory_limit,
|
|
||||||
description = R.string.process_runtime_memory_limit_description,
|
|
||||||
)
|
|
||||||
|
|
||||||
GroupHeader(stringResource(R.string.manager))
|
|
||||||
SettingsListItem(
|
SettingsListItem(
|
||||||
headlineContent = stringResource(R.string.api_url),
|
headlineContent = stringResource(R.string.api_url),
|
||||||
supportingContent = apiUrl,
|
supportingContent = stringResource(R.string.api_url_description),
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
showApiUrlDialog = true
|
showApiUrlDialog = true
|
||||||
},
|
|
||||||
trailingContent = {
|
|
||||||
IconButton(onClick = { showApiUrlDialog = true }) {
|
|
||||||
Icon(
|
|
||||||
Icons.Outlined.Edit,
|
|
||||||
contentDescription = stringResource(R.string.edit)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
GroupHeader(stringResource(R.string.safeguards))
|
||||||
SafeguardBooleanItem(
|
SafeguardBooleanItem(
|
||||||
preference = vm.prefs.disablePatchVersionCompatCheck,
|
preference = viewModel.prefs.disablePatchVersionCompatCheck,
|
||||||
coroutineScope = vm.viewModelScope,
|
coroutineScope = viewModel.viewModelScope,
|
||||||
headline = R.string.allow_compatibility_mixing,
|
headline = R.string.patch_compat_check,
|
||||||
description = R.string.allow_compatibility_mixing_description,
|
description = R.string.patch_compat_check_description,
|
||||||
confirmationText = R.string.allow_compatibility_mixing_confirmation
|
confirmationText = R.string.patch_compat_check_confirmation
|
||||||
)
|
)
|
||||||
SafeguardBooleanItem(
|
SafeguardBooleanItem(
|
||||||
preference = vm.prefs.disableUniversalPatchWarning,
|
preference = viewModel.prefs.suggestedVersionSafeguard,
|
||||||
coroutineScope = vm.viewModelScope,
|
coroutineScope = viewModel.viewModelScope,
|
||||||
headline = R.string.universal_patches_safeguard,
|
headline = R.string.suggested_version_safeguard,
|
||||||
description = R.string.universal_patches_safeguard_description,
|
description = R.string.suggested_version_safeguard_description,
|
||||||
confirmationText = R.string.universal_patches_safeguard_confirmation
|
confirmationText = R.string.suggested_version_safeguard_confirmation
|
||||||
)
|
)
|
||||||
SafeguardBooleanItem(
|
SafeguardBooleanItem(
|
||||||
preference = vm.prefs.disableSelectionWarning,
|
preference = viewModel.prefs.disableSelectionWarning,
|
||||||
coroutineScope = vm.viewModelScope,
|
coroutineScope = viewModel.viewModelScope,
|
||||||
headline = R.string.patch_selection_safeguard,
|
headline = R.string.patch_selection_safeguard,
|
||||||
description = R.string.patch_selection_safeguard_description,
|
description = R.string.patch_selection_safeguard_description,
|
||||||
confirmationText = R.string.patch_selection_safeguard_confirmation
|
confirmationText = R.string.patch_selection_safeguard_confirmation
|
||||||
)
|
)
|
||||||
|
SafeguardBooleanItem(
|
||||||
GroupHeader(stringResource(R.string.update))
|
preference = viewModel.prefs.disableUniversalPatchCheck,
|
||||||
BooleanItem(
|
coroutineScope = viewModel.viewModelScope,
|
||||||
preference = vm.prefs.showManagerUpdateDialogOnLaunch,
|
headline = R.string.universal_patches_safeguard,
|
||||||
headline = R.string.show_manager_update_dialog_on_launch,
|
description = R.string.universal_patches_safeguard_description,
|
||||||
description = R.string.check_for_update_auto_description
|
confirmationText = R.string.universal_patches_safeguard_confirmation
|
||||||
)
|
)
|
||||||
|
|
||||||
GroupHeader(stringResource(R.string.experimental_features))
|
GroupHeader(stringResource(R.string.patcher))
|
||||||
BooleanItem(
|
BooleanItem(
|
||||||
preference = vm.prefs.useProcessRuntime,
|
preference = viewModel.prefs.useProcessRuntime,
|
||||||
coroutineScope = vm.viewModelScope,
|
coroutineScope = viewModel.viewModelScope,
|
||||||
headline = R.string.process_runtime,
|
headline = R.string.process_runtime,
|
||||||
description = R.string.process_runtime_description,
|
description = R.string.process_runtime_description,
|
||||||
)
|
)
|
||||||
|
IntegerItem(
|
||||||
|
preference = viewModel.prefs.patcherProcessMemoryLimit,
|
||||||
|
coroutineScope = viewModel.viewModelScope,
|
||||||
|
headline = R.string.process_runtime_memory_limit,
|
||||||
|
description = R.string.process_runtime_memory_limit_description,
|
||||||
|
)
|
||||||
|
|
||||||
GroupHeader(stringResource(R.string.debugging))
|
GroupHeader(stringResource(R.string.debugging))
|
||||||
val exportDebugLogsLauncher =
|
val exportDebugLogsLauncher =
|
||||||
rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) {
|
rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) {
|
||||||
it?.let(vm::exportDebugLogs)
|
it?.let(viewModel::exportDebugLogs)
|
||||||
}
|
}
|
||||||
SettingsListItem(
|
SettingsListItem(
|
||||||
headlineContent = stringResource(R.string.debug_logs_export),
|
headlineContent = stringResource(R.string.debug_logs_export),
|
||||||
modifier = Modifier.clickable { exportDebugLogsLauncher.launch(vm.debugLogFileName) }
|
modifier = Modifier.clickable { exportDebugLogsLauncher.launch(viewModel.debugLogFileName) }
|
||||||
)
|
)
|
||||||
val clipboard = remember { context.getSystemService<ClipboardManager>()!! }
|
val clipboard = remember { context.getSystemService<ClipboardManager>()!! }
|
||||||
val deviceContent = """
|
val deviceContent = """
|
||||||
|
|||||||
@@ -1,381 +0,0 @@
|
|||||||
package app.revanced.manager.ui.screen.settings
|
|
||||||
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.outlined.Key
|
|
||||||
import androidx.compose.material3.AlertDialog
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
|
||||||
import androidx.compose.material3.OutlinedTextField
|
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
|
||||||
import androidx.compose.material3.rememberTopAppBarState
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import app.revanced.manager.R
|
|
||||||
import app.revanced.manager.ui.component.AppTopBar
|
|
||||||
import app.revanced.manager.ui.component.ColumnWithScrollbar
|
|
||||||
import app.revanced.manager.ui.component.GroupHeader
|
|
||||||
import app.revanced.manager.ui.component.PasswordField
|
|
||||||
import app.revanced.manager.ui.component.bundle.BundleSelector
|
|
||||||
import app.revanced.manager.ui.component.settings.SettingsListItem
|
|
||||||
import app.revanced.manager.ui.viewmodel.ImportExportViewModel
|
|
||||||
import app.revanced.manager.util.toast
|
|
||||||
import app.revanced.manager.util.uiSafe
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.koin.androidx.compose.koinViewModel
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun BackupRestoreSettingsScreen(
|
|
||||||
onBackClick: () -> Unit,
|
|
||||||
viewModel: ImportExportViewModel = koinViewModel()
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
val importKeystoreLauncher =
|
|
||||||
rememberLauncherForActivityResult(contract = ActivityResultContracts.GetContent()) {
|
|
||||||
it?.let { uri -> viewModel.startKeystoreImport(uri) }
|
|
||||||
}
|
|
||||||
val exportKeystoreLauncher =
|
|
||||||
rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("*/*")) {
|
|
||||||
it?.let(viewModel::exportKeystore)
|
|
||||||
}
|
|
||||||
|
|
||||||
val patchBundles by viewModel.patchBundles.collectAsStateWithLifecycle(initialValue = emptyList())
|
|
||||||
val packagesWithOptions by viewModel.packagesWithOptions.collectAsStateWithLifecycle(initialValue = emptySet())
|
|
||||||
|
|
||||||
viewModel.selectionAction?.let { action ->
|
|
||||||
val launcher = rememberLauncherForActivityResult(action.activityContract) { uri ->
|
|
||||||
if (uri == null) {
|
|
||||||
viewModel.clearSelectionAction()
|
|
||||||
} else {
|
|
||||||
viewModel.executeSelectionAction(uri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (viewModel.selectedBundle == null) {
|
|
||||||
BundleSelector(patchBundles) {
|
|
||||||
if (it == null) {
|
|
||||||
viewModel.clearSelectionAction()
|
|
||||||
} else {
|
|
||||||
viewModel.selectBundle(it)
|
|
||||||
launcher.launch(action.activityArg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (viewModel.showCredentialsDialog) {
|
|
||||||
KeystoreCredentialsDialog(
|
|
||||||
onDismissRequest = viewModel::cancelKeystoreImport,
|
|
||||||
onSubmit = { cn, pass ->
|
|
||||||
viewModel.viewModelScope.launch {
|
|
||||||
uiSafe(context, R.string.failed_to_import_keystore, "Failed to import keystore") {
|
|
||||||
val result = viewModel.tryKeystoreImport(cn, pass)
|
|
||||||
if (!result) context.toast(context.getString(R.string.restore_keystore_wrong_credentials))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
|
||||||
|
|
||||||
Scaffold(
|
|
||||||
topBar = {
|
|
||||||
AppTopBar(
|
|
||||||
title = stringResource(R.string.backup_restore),
|
|
||||||
scrollBehavior = scrollBehavior,
|
|
||||||
onBackClick = onBackClick
|
|
||||||
)
|
|
||||||
},
|
|
||||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
|
||||||
) { paddingValues ->
|
|
||||||
ColumnWithScrollbar(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(paddingValues)
|
|
||||||
) {
|
|
||||||
var showPackageSelector by rememberSaveable {
|
|
||||||
mutableStateOf(false)
|
|
||||||
}
|
|
||||||
var showBundleSelector by rememberSaveable {
|
|
||||||
mutableStateOf(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showPackageSelector) {
|
|
||||||
PackageSelector(packages = packagesWithOptions) { selected ->
|
|
||||||
selected?.let(viewModel::resetOptionsForPackage)
|
|
||||||
|
|
||||||
showPackageSelector = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showBundleSelector) {
|
|
||||||
BundleSelector(bundles = patchBundles) { bundle ->
|
|
||||||
bundle?.let(viewModel::clearOptionsForBundle)
|
|
||||||
|
|
||||||
showBundleSelector = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
GroupHeader(stringResource(R.string.keystore))
|
|
||||||
GroupItem(
|
|
||||||
onClick = {
|
|
||||||
if (!viewModel.canExport()) {
|
|
||||||
context.toast(context.getString(R.string.backup_keystore_unavailable))
|
|
||||||
return@GroupItem
|
|
||||||
}
|
|
||||||
exportKeystoreLauncher.launch("Manager.keystore")
|
|
||||||
},
|
|
||||||
headline = R.string.backup,
|
|
||||||
description = R.string.backup_keystore_description
|
|
||||||
)
|
|
||||||
GroupItem(
|
|
||||||
onClick = {
|
|
||||||
importKeystoreLauncher.launch("*/*")
|
|
||||||
},
|
|
||||||
headline = R.string.restore,
|
|
||||||
description = R.string.restore_keystore_description
|
|
||||||
)
|
|
||||||
GroupItem(
|
|
||||||
onClick = viewModel::regenerateKeystore,
|
|
||||||
headline = {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.regenerate_keystore),
|
|
||||||
color = MaterialTheme.colorScheme.error
|
|
||||||
)
|
|
||||||
},
|
|
||||||
description = R.string.regenerate_keystore_description
|
|
||||||
)
|
|
||||||
|
|
||||||
GroupHeader(stringResource(R.string.patch_selection))
|
|
||||||
GroupItem(
|
|
||||||
onClick = viewModel::exportSelection,
|
|
||||||
headline = R.string.backup,
|
|
||||||
description = R.string.restore_patch_selection_description
|
|
||||||
)
|
|
||||||
GroupItem(
|
|
||||||
onClick = viewModel::importSelection,
|
|
||||||
headline = R.string.restore,
|
|
||||||
description = R.string.backup_patch_selection_description
|
|
||||||
)
|
|
||||||
GroupItem(
|
|
||||||
onClick = viewModel::resetSelection, // TODO: allow resetting selection for specific bundle or package name.
|
|
||||||
headline = {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.reset),
|
|
||||||
color = MaterialTheme.colorScheme.error
|
|
||||||
)
|
|
||||||
},
|
|
||||||
description = R.string.reset_patch_selection_description
|
|
||||||
)
|
|
||||||
|
|
||||||
GroupHeader(stringResource(R.string.patch_options))
|
|
||||||
// TODO: patch options import/export.
|
|
||||||
GroupItem(
|
|
||||||
onClick = viewModel::resetOptions,
|
|
||||||
headline = {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.reset),
|
|
||||||
color = MaterialTheme.colorScheme.error
|
|
||||||
)
|
|
||||||
},
|
|
||||||
description = R.string.patch_options_reset_all_description,
|
|
||||||
)
|
|
||||||
GroupItem(
|
|
||||||
onClick = { showPackageSelector = true },
|
|
||||||
headline = {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.patch_options_reset_package),
|
|
||||||
color = MaterialTheme.colorScheme.error
|
|
||||||
)
|
|
||||||
},
|
|
||||||
description = R.string.patch_options_reset_package_description
|
|
||||||
)
|
|
||||||
if (patchBundles.size > 1) {
|
|
||||||
GroupItem(
|
|
||||||
onClick = { showBundleSelector = true },
|
|
||||||
headline = {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.patch_options_reset_bundle),
|
|
||||||
color = MaterialTheme.colorScheme.error
|
|
||||||
)
|
|
||||||
},
|
|
||||||
description = R.string.patch_options_reset_bundle_description,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
private fun PackageSelector(packages: Set<String>, onFinish: (String?) -> Unit) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
val noPackages = packages.isEmpty()
|
|
||||||
|
|
||||||
LaunchedEffect(noPackages) {
|
|
||||||
if (noPackages) {
|
|
||||||
context.toast("No packages available.")
|
|
||||||
onFinish(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (noPackages) return
|
|
||||||
|
|
||||||
ModalBottomSheet(
|
|
||||||
onDismissRequest = { onFinish(null) }
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.Center,
|
|
||||||
modifier = Modifier
|
|
||||||
.height(48.dp)
|
|
||||||
.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "Select package",
|
|
||||||
style = MaterialTheme.typography.titleLarge,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
|
||||||
}
|
|
||||||
packages.forEach {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.Center,
|
|
||||||
modifier = Modifier
|
|
||||||
.height(48.dp)
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable {
|
|
||||||
onFinish(it)
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = it,
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun GroupItem(
|
|
||||||
onClick: () -> Unit,
|
|
||||||
@StringRes headline: Int,
|
|
||||||
@StringRes description: Int? = null
|
|
||||||
) {
|
|
||||||
SettingsListItem(
|
|
||||||
modifier = Modifier.clickable { onClick() },
|
|
||||||
headlineContent = stringResource(headline),
|
|
||||||
supportingContent = description?.let { stringResource(it) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun GroupItem(
|
|
||||||
onClick: () -> Unit,
|
|
||||||
headline: @Composable () -> Unit,
|
|
||||||
@StringRes description: Int? = null
|
|
||||||
) {
|
|
||||||
SettingsListItem(
|
|
||||||
modifier = Modifier.clickable { onClick() },
|
|
||||||
headlineContent = headline,
|
|
||||||
supportingContent = description?.let { stringResource(it) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun KeystoreCredentialsDialog(
|
|
||||||
onDismissRequest: () -> Unit,
|
|
||||||
onSubmit: (String, String) -> Unit
|
|
||||||
) {
|
|
||||||
var cn by rememberSaveable { mutableStateOf("") }
|
|
||||||
var pass by rememberSaveable { mutableStateOf("") }
|
|
||||||
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = onDismissRequest,
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
onSubmit(cn, pass)
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.restore_keystore_dialog_button))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(onClick = onDismissRequest) {
|
|
||||||
Text(stringResource(R.string.cancel))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon = {
|
|
||||||
Icon(Icons.Outlined.Key, null)
|
|
||||||
},
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.restore_keystore_dialog_title),
|
|
||||||
style = MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center),
|
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
text = {
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.restore_keystore_dialog_description),
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
OutlinedTextField(
|
|
||||||
value = cn,
|
|
||||||
onValueChange = { cn = it },
|
|
||||||
label = { Text(stringResource(R.string.restore_keystore_dialog_alias_field)) }
|
|
||||||
)
|
|
||||||
PasswordField(
|
|
||||||
value = pass,
|
|
||||||
onValueChange = { pass = it },
|
|
||||||
label = { Text(stringResource(R.string.restore_keystore_dialog_password_field)) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -51,7 +51,7 @@ import org.koin.androidx.compose.koinViewModel
|
|||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ContributorScreen(
|
fun ContributorSettingsScreen(
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
viewModel: ContributorViewModel = koinViewModel()
|
viewModel: ContributorViewModel = koinViewModel()
|
||||||
) {
|
) {
|
||||||
@@ -97,7 +97,14 @@ fun ContributorScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} ?: item { LoadingIndicator() }
|
} ?: item {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
LoadingIndicator()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,19 +12,23 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
|
import app.revanced.manager.domain.manager.PreferencesManager
|
||||||
import app.revanced.manager.ui.component.AppTopBar
|
import app.revanced.manager.ui.component.AppTopBar
|
||||||
import app.revanced.manager.ui.component.GroupHeader
|
import app.revanced.manager.ui.component.GroupHeader
|
||||||
|
import app.revanced.manager.ui.component.settings.BooleanItem
|
||||||
import app.revanced.manager.ui.component.settings.SettingsListItem
|
import app.revanced.manager.ui.component.settings.SettingsListItem
|
||||||
import app.revanced.manager.ui.viewmodel.DeveloperOptionsViewModel
|
import app.revanced.manager.ui.viewmodel.DeveloperOptionsViewModel
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
import org.koin.compose.koinInject
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun DeveloperOptionsScreen(
|
fun DeveloperSettingsScreen(
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
vm: DeveloperOptionsViewModel = koinViewModel()
|
vm: DeveloperOptionsViewModel = koinViewModel()
|
||||||
) {
|
) {
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
|
val prefs: PreferencesManager = koinInject()
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
@@ -37,20 +41,21 @@ fun DeveloperOptionsScreen(
|
|||||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
Column(modifier = Modifier.padding(paddingValues)) {
|
Column(modifier = Modifier.padding(paddingValues)) {
|
||||||
GroupHeader(stringResource(R.string.patch_bundles_section))
|
GroupHeader(stringResource(R.string.manager))
|
||||||
|
BooleanItem(
|
||||||
|
preference = prefs.showDeveloperSettings,
|
||||||
|
headline = R.string.developer_options,
|
||||||
|
description = R.string.developer_options_description,
|
||||||
|
)
|
||||||
|
|
||||||
|
GroupHeader(stringResource(R.string.patches))
|
||||||
SettingsListItem(
|
SettingsListItem(
|
||||||
headlineContent = stringResource(R.string.patch_bundles_force_download),
|
headlineContent = stringResource(R.string.patches_force_download),
|
||||||
modifier = Modifier.clickable(onClick = vm::redownloadBundles)
|
modifier = Modifier.clickable(onClick = vm::redownloadBundles)
|
||||||
)
|
)
|
||||||
SettingsListItem(
|
SettingsListItem(
|
||||||
headlineContent = stringResource(R.string.patch_bundles_reset),
|
headlineContent = stringResource(R.string.patches_reset),
|
||||||
modifier = Modifier.clickable(onClick = vm::resetBundles)
|
modifier = Modifier.clickable(onClick = vm::redownloadBundles)
|
||||||
)
|
|
||||||
|
|
||||||
GroupHeader(stringResource(R.string.testing))
|
|
||||||
SettingsListItem(
|
|
||||||
headlineContent = stringResource(R.string.disable_safeguard),
|
|
||||||
modifier = Modifier.clickable(onClick = vm::disableSafeguard)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,13 +2,13 @@ package app.revanced.manager.ui.screen.settings
|
|||||||
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.material.icons.outlined.Delete
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
@@ -18,9 +18,7 @@ import androidx.compose.material3.Scaffold
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
import androidx.compose.material3.pulltorefresh.pullToRefresh
|
|
||||||
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
|
||||||
import androidx.compose.material3.rememberTopAppBarState
|
import androidx.compose.material3.rememberTopAppBarState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -28,13 +26,11 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.zIndex
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.network.downloader.DownloaderPluginState
|
import app.revanced.manager.network.downloader.DownloaderPluginState
|
||||||
@@ -43,6 +39,7 @@ import app.revanced.manager.ui.component.AppTopBar
|
|||||||
import app.revanced.manager.ui.component.ExceptionViewerDialog
|
import app.revanced.manager.ui.component.ExceptionViewerDialog
|
||||||
import app.revanced.manager.ui.component.GroupHeader
|
import app.revanced.manager.ui.component.GroupHeader
|
||||||
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
|
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
|
||||||
|
import app.revanced.manager.ui.component.ConfirmDialog
|
||||||
import app.revanced.manager.ui.component.haptics.HapticCheckbox
|
import app.revanced.manager.ui.component.haptics.HapticCheckbox
|
||||||
import app.revanced.manager.ui.component.settings.SettingsListItem
|
import app.revanced.manager.ui.component.settings.SettingsListItem
|
||||||
import app.revanced.manager.ui.viewmodel.DownloadsViewModel
|
import app.revanced.manager.ui.viewmodel.DownloadsViewModel
|
||||||
@@ -55,20 +52,30 @@ fun DownloadsSettingsScreen(
|
|||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
viewModel: DownloadsViewModel = koinViewModel()
|
viewModel: DownloadsViewModel = koinViewModel()
|
||||||
) {
|
) {
|
||||||
val pullRefreshState = rememberPullToRefreshState()
|
|
||||||
val downloadedApps by viewModel.downloadedApps.collectAsStateWithLifecycle(emptyList())
|
val downloadedApps by viewModel.downloadedApps.collectAsStateWithLifecycle(emptyList())
|
||||||
val pluginStates by viewModel.downloaderPluginStates.collectAsStateWithLifecycle()
|
val pluginStates by viewModel.downloaderPluginStates.collectAsStateWithLifecycle()
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
|
var showDeleteConfirmationDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
|
if (showDeleteConfirmationDialog) {
|
||||||
|
ConfirmDialog(
|
||||||
|
onDismiss = { showDeleteConfirmationDialog = false },
|
||||||
|
onConfirm = { viewModel.deleteApps() },
|
||||||
|
title = stringResource(R.string.downloader_plugin_delete_apps_title),
|
||||||
|
description = stringResource(R.string.downloader_plugin_delete_apps_description),
|
||||||
|
icon = Icons.Outlined.Delete
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopBar(
|
AppTopBar(
|
||||||
title = stringResource(R.string.extensions),
|
title = stringResource(R.string.downloads),
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
onBackClick = onBackClick,
|
onBackClick = onBackClick,
|
||||||
actions = {
|
actions = {
|
||||||
if (viewModel.appSelection.isNotEmpty()) {
|
if (viewModel.appSelection.isNotEmpty()) {
|
||||||
IconButton(onClick = { viewModel.deleteApps() }) {
|
IconButton(onClick = { showDeleteConfirmationDialog = true }) {
|
||||||
Icon(Icons.Default.Delete, stringResource(R.string.delete))
|
Icon(Icons.Default.Delete, stringResource(R.string.delete))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,152 +84,138 @@ fun DownloadsSettingsScreen(
|
|||||||
},
|
},
|
||||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
Box(
|
PullToRefreshBox(
|
||||||
contentAlignment = Alignment.TopCenter,
|
onRefresh = viewModel::refreshPlugins,
|
||||||
modifier = Modifier
|
isRefreshing = viewModel.isRefreshingPlugins,
|
||||||
.padding(paddingValues)
|
modifier = Modifier.padding(paddingValues)
|
||||||
.fillMaxWidth()
|
|
||||||
.zIndex(1f)
|
|
||||||
) {
|
) {
|
||||||
PullToRefreshDefaults.Indicator(
|
LazyColumnWithScrollbar(
|
||||||
state = pullRefreshState,
|
modifier = Modifier.fillMaxSize()
|
||||||
isRefreshing = viewModel.isRefreshingPlugins
|
) {
|
||||||
)
|
item {
|
||||||
}
|
GroupHeader(stringResource(R.string.downloader_plugins))
|
||||||
|
}
|
||||||
LazyColumnWithScrollbar(
|
pluginStates.forEach { (packageName, state) ->
|
||||||
modifier = Modifier
|
item(key = packageName) {
|
||||||
.fillMaxSize()
|
var showDialog by rememberSaveable {
|
||||||
.padding(paddingValues)
|
mutableStateOf(false)
|
||||||
.pullToRefresh(
|
|
||||||
isRefreshing = viewModel.isRefreshingPlugins,
|
|
||||||
state = pullRefreshState,
|
|
||||||
onRefresh = viewModel::refreshPlugins
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
item {
|
|
||||||
GroupHeader(stringResource(R.string.downloader_plugins))
|
|
||||||
}
|
|
||||||
pluginStates.forEach { (packageName, state) ->
|
|
||||||
item(key = packageName) {
|
|
||||||
var showDialog by rememberSaveable {
|
|
||||||
mutableStateOf(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun dismiss() {
|
|
||||||
showDialog = false
|
|
||||||
}
|
|
||||||
|
|
||||||
val packageInfo =
|
|
||||||
remember(packageName) {
|
|
||||||
viewModel.pm.getPackageInfo(
|
|
||||||
packageName
|
|
||||||
)
|
|
||||||
} ?: return@item
|
|
||||||
|
|
||||||
if (showDialog) {
|
|
||||||
val signature =
|
|
||||||
remember(packageName) {
|
|
||||||
val androidSignature =
|
|
||||||
viewModel.pm.getSignature(packageName)
|
|
||||||
val hash = MessageDigest.getInstance("SHA-256")
|
|
||||||
.digest(androidSignature.toByteArray())
|
|
||||||
hash.toHexString(format = HexFormat.UpperCase)
|
|
||||||
}
|
|
||||||
|
|
||||||
when (state) {
|
|
||||||
is DownloaderPluginState.Loaded -> TrustDialog(
|
|
||||||
title = R.string.downloader_plugin_revoke_trust_dialog_title,
|
|
||||||
body = stringResource(
|
|
||||||
R.string.downloader_plugin_trust_dialog_body,
|
|
||||||
packageName,
|
|
||||||
signature
|
|
||||||
),
|
|
||||||
onDismiss = ::dismiss,
|
|
||||||
onConfirm = {
|
|
||||||
viewModel.revokePluginTrust(packageName)
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
is DownloaderPluginState.Failed -> ExceptionViewerDialog(
|
|
||||||
text = remember(state.throwable) {
|
|
||||||
state.throwable.stackTraceToString()
|
|
||||||
},
|
|
||||||
onDismiss = ::dismiss
|
|
||||||
)
|
|
||||||
|
|
||||||
is DownloaderPluginState.Untrusted -> TrustDialog(
|
|
||||||
title = R.string.downloader_plugin_trust_dialog_title,
|
|
||||||
body = stringResource(
|
|
||||||
R.string.downloader_plugin_trust_dialog_body,
|
|
||||||
packageName,
|
|
||||||
signature
|
|
||||||
),
|
|
||||||
onDismiss = ::dismiss,
|
|
||||||
onConfirm = {
|
|
||||||
viewModel.trustPlugin(packageName)
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun dismiss() {
|
||||||
|
showDialog = false
|
||||||
|
}
|
||||||
|
|
||||||
|
val packageInfo =
|
||||||
|
remember(packageName) {
|
||||||
|
viewModel.pm.getPackageInfo(
|
||||||
|
packageName
|
||||||
|
)
|
||||||
|
} ?: return@item
|
||||||
|
|
||||||
|
if (showDialog) {
|
||||||
|
val signature =
|
||||||
|
remember(packageName) {
|
||||||
|
val androidSignature =
|
||||||
|
viewModel.pm.getSignature(packageName)
|
||||||
|
val hash = MessageDigest.getInstance("SHA-256")
|
||||||
|
.digest(androidSignature.toByteArray())
|
||||||
|
hash.toHexString(format = HexFormat.UpperCase)
|
||||||
|
}
|
||||||
|
|
||||||
|
when (state) {
|
||||||
|
is DownloaderPluginState.Loaded -> TrustDialog(
|
||||||
|
title = R.string.downloader_plugin_revoke_trust_dialog_title,
|
||||||
|
body = stringResource(
|
||||||
|
R.string.downloader_plugin_trust_dialog_body,
|
||||||
|
packageName,
|
||||||
|
signature
|
||||||
|
),
|
||||||
|
onDismiss = ::dismiss,
|
||||||
|
onConfirm = {
|
||||||
|
viewModel.revokePluginTrust(packageName)
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
is DownloaderPluginState.Failed -> ExceptionViewerDialog(
|
||||||
|
text = remember(state.throwable) {
|
||||||
|
state.throwable.stackTraceToString()
|
||||||
|
},
|
||||||
|
onDismiss = ::dismiss
|
||||||
|
)
|
||||||
|
|
||||||
|
is DownloaderPluginState.Untrusted -> TrustDialog(
|
||||||
|
title = R.string.downloader_plugin_trust_dialog_title,
|
||||||
|
body = stringResource(
|
||||||
|
R.string.downloader_plugin_trust_dialog_body,
|
||||||
|
packageName,
|
||||||
|
signature
|
||||||
|
),
|
||||||
|
onDismiss = ::dismiss,
|
||||||
|
onConfirm = {
|
||||||
|
viewModel.trustPlugin(packageName)
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsListItem(
|
||||||
|
modifier = Modifier.clickable { showDialog = true },
|
||||||
|
headlineContent = {
|
||||||
|
AppLabel(
|
||||||
|
packageInfo = packageInfo,
|
||||||
|
style = MaterialTheme.typography.titleLarge
|
||||||
|
)
|
||||||
|
},
|
||||||
|
supportingContent = stringResource(
|
||||||
|
when (state) {
|
||||||
|
is DownloaderPluginState.Loaded -> R.string.downloader_plugin_state_trusted
|
||||||
|
is DownloaderPluginState.Failed -> R.string.downloader_plugin_state_failed
|
||||||
|
is DownloaderPluginState.Untrusted -> R.string.downloader_plugin_state_untrusted
|
||||||
|
}
|
||||||
|
),
|
||||||
|
trailingContent = { Text(packageInfo.versionName!!) }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (pluginStates.isEmpty()) {
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.downloader_no_plugins_installed),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
GroupHeader(stringResource(R.string.downloaded_apps))
|
||||||
|
}
|
||||||
|
items(downloadedApps, key = { it.packageName to it.version }) { app ->
|
||||||
|
val selected = app in viewModel.appSelection
|
||||||
|
|
||||||
SettingsListItem(
|
SettingsListItem(
|
||||||
modifier = Modifier.clickable { showDialog = true },
|
modifier = Modifier.clickable { viewModel.toggleApp(app) },
|
||||||
headlineContent = {
|
headlineContent = app.packageName,
|
||||||
AppLabel(
|
leadingContent = (@Composable {
|
||||||
packageInfo = packageInfo,
|
HapticCheckbox(
|
||||||
style = MaterialTheme.typography.titleLarge
|
checked = selected,
|
||||||
|
onCheckedChange = { viewModel.toggleApp(app) }
|
||||||
)
|
)
|
||||||
},
|
}).takeIf { viewModel.appSelection.isNotEmpty() },
|
||||||
supportingContent = stringResource(
|
supportingContent = app.version,
|
||||||
when (state) {
|
tonalElevation = if (selected) 8.dp else 0.dp
|
||||||
is DownloaderPluginState.Loaded -> R.string.downloader_plugin_state_trusted
|
|
||||||
is DownloaderPluginState.Failed -> R.string.downloader_plugin_state_failed
|
|
||||||
is DownloaderPluginState.Untrusted -> R.string.downloader_plugin_state_untrusted
|
|
||||||
}
|
|
||||||
),
|
|
||||||
trailingContent = { Text(packageInfo.versionName!!) }
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
if (downloadedApps.isEmpty()) {
|
||||||
if (pluginStates.isEmpty()) {
|
item {
|
||||||
item {
|
Text(
|
||||||
Text(
|
stringResource(R.string.downloader_settings_no_apps),
|
||||||
stringResource(R.string.downloader_no_plugins_installed),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
modifier = Modifier.fillMaxWidth(),
|
textAlign = TextAlign.Center
|
||||||
textAlign = TextAlign.Center
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
GroupHeader(stringResource(R.string.downloaded_apps))
|
|
||||||
}
|
|
||||||
items(downloadedApps, key = { it.packageName to it.version }) { app ->
|
|
||||||
val selected = app in viewModel.appSelection
|
|
||||||
|
|
||||||
SettingsListItem(
|
|
||||||
modifier = Modifier.clickable { viewModel.toggleApp(app) },
|
|
||||||
headlineContent = app.packageName,
|
|
||||||
leadingContent = (@Composable {
|
|
||||||
HapticCheckbox(
|
|
||||||
checked = selected,
|
|
||||||
onCheckedChange = { viewModel.toggleApp(app) }
|
|
||||||
)
|
)
|
||||||
}).takeIf { viewModel.appSelection.isNotEmpty() },
|
}
|
||||||
supportingContent = app.version,
|
|
||||||
tonalElevation = if (selected) 8.dp else 0.dp
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (downloadedApps.isEmpty()) {
|
|
||||||
item {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.downloader_settings_no_apps),
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
textAlign = TextAlign.Center
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FilledTonalButton
|
import androidx.compose.material3.FilledTonalButton
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
@@ -43,7 +42,6 @@ import org.koin.compose.koinInject
|
|||||||
@Composable
|
@Composable
|
||||||
fun GeneralSettingsScreen(
|
fun GeneralSettingsScreen(
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
onUpdateClick: () -> Unit,
|
|
||||||
viewModel: GeneralSettingsViewModel = koinViewModel()
|
viewModel: GeneralSettingsViewModel = koinViewModel()
|
||||||
) {
|
) {
|
||||||
val prefs = viewModel.prefs
|
val prefs = viewModel.prefs
|
||||||
@@ -78,10 +76,10 @@ fun GeneralSettingsScreen(
|
|||||||
val theme by prefs.theme.getAsState()
|
val theme by prefs.theme.getAsState()
|
||||||
SettingsListItem(
|
SettingsListItem(
|
||||||
modifier = Modifier.clickable { showThemePicker = true },
|
modifier = Modifier.clickable { showThemePicker = true },
|
||||||
headlineContent = stringResource(R.string.theme_mode),
|
headlineContent = stringResource(R.string.theme),
|
||||||
supportingContent = stringResource(R.string.theme_mode_description),
|
supportingContent = stringResource(R.string.theme_description),
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
Button(
|
FilledTonalButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
showThemePicker = true
|
showThemePicker = true
|
||||||
}
|
}
|
||||||
@@ -94,24 +92,11 @@ fun GeneralSettingsScreen(
|
|||||||
BooleanItem(
|
BooleanItem(
|
||||||
preference = prefs.dynamicColor,
|
preference = prefs.dynamicColor,
|
||||||
coroutineScope = coroutineScope,
|
coroutineScope = coroutineScope,
|
||||||
headline = R.string.personalized_color,
|
headline = R.string.dynamic_color,
|
||||||
description = R.string.personalized_color_description
|
description = R.string.dynamic_color_description
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
GroupHeader(stringResource(R.string.update))
|
|
||||||
BooleanItem(
|
|
||||||
preference = prefs.managerAutoUpdates,
|
|
||||||
headline = R.string.check_for_update,
|
|
||||||
description = R.string.check_for_update_auto_description
|
|
||||||
)
|
|
||||||
FilledTonalButton (
|
|
||||||
modifier = Modifier.padding(top = paddingValues.calculateTopPadding()),
|
|
||||||
onClick = onUpdateClick
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.check_for_update))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +110,7 @@ private fun ThemePicker(
|
|||||||
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
title = { Text(stringResource(R.string.theme_mode)) },
|
title = { Text(stringResource(R.string.theme)) },
|
||||||
text = {
|
text = {
|
||||||
Column {
|
Column {
|
||||||
Theme.entries.forEach {
|
Theme.entries.forEach {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user