Compare commits
623 Commits
v0.0.32
...
v1.19.0-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
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 | ||
|
|
d9953b1473 | ||
|
|
c6ac898390 | ||
|
|
43f98cec43 | ||
|
|
63175cdec6 | ||
|
|
6436a1ec61 | ||
|
|
c400619338 | ||
|
|
dcd5ba41cf | ||
|
|
7525e52fab | ||
|
|
72fd24e624 | ||
|
|
11d8f9fd30 | ||
|
|
4b0c8cecc8 | ||
|
|
372ce174c9 | ||
|
|
381daff980 | ||
|
|
94acebbebd | ||
|
|
f90f6e81ee | ||
|
|
97e37f304b | ||
|
|
f5e45ead26 | ||
|
|
14f765f4b4 | ||
|
|
b77d46b2c2 | ||
|
|
1442916b20 | ||
|
|
c6a5f42d23 | ||
|
|
580d50eb8d | ||
|
|
9a66b6e50d | ||
|
|
acec064cb7 | ||
|
|
ea05d13a1f | ||
|
|
fd741f2ccf | ||
|
|
055c52178f | ||
|
|
722a5859a5 | ||
|
|
1714c3fa86 | ||
|
|
131df28110 | ||
|
|
1be284f8d8 | ||
|
|
218e53ae75 | ||
|
|
264d8d90c4 | ||
|
|
0b529c2629 | ||
|
|
5abcc7191f | ||
|
|
5346f6e1bf | ||
|
|
2f471b3de4 | ||
|
|
3cf06efddf | ||
|
|
1f4f0a3bb7 | ||
|
|
c07f8eae9b | ||
|
|
374eb3d06d | ||
|
|
fe75b75ddc | ||
|
|
d3790bf64b | ||
|
|
f905a52988 | ||
|
|
e55f427b05 | ||
|
|
0f000fc4d1 | ||
|
|
a0339e3c19 | ||
|
|
9cfa274d81 | ||
|
|
8c79f5e371 | ||
|
|
0d716513d7 | ||
|
|
08f6724060 | ||
|
|
29daf51e64 | ||
|
|
b3b2b01c0f | ||
|
|
c3212d0308 | ||
|
|
aaa114ba13 | ||
|
|
8ca6418630 | ||
|
|
95632b7f55 | ||
|
|
64744b2abf | ||
|
|
096b315701 | ||
|
|
c27ca08d3a | ||
|
|
0011222371 | ||
|
|
29db947e25 | ||
|
|
fd43ac7581 | ||
|
|
e49c19b3cd | ||
|
|
06f0e59967 | ||
|
|
c5fc5ee93b | ||
|
|
159c85bd1f | ||
|
|
2460acf0f4 | ||
|
|
6495687841 | ||
|
|
d229ccb36c | ||
|
|
4d6a57ddcf | ||
|
|
d161d55aaf | ||
|
|
768ad0c9bc | ||
|
|
29323d4e20 | ||
|
|
630b22e193 | ||
|
|
79116f9e67 | ||
|
|
eb58475259 | ||
|
|
a879ac30fb | ||
|
|
c5b0621323 | ||
|
|
0462815014 | ||
|
|
e64318c947 | ||
|
|
b784482788 | ||
|
|
2834e8b348 | ||
|
|
b23dfd4289 | ||
|
|
217d525cb2 | ||
|
|
85b166cbda | ||
|
|
9a57f8b858 | ||
|
|
3bfdc932c2 | ||
|
|
48c878af21 | ||
|
|
490a7a58fc | ||
|
|
567b1a3ace | ||
|
|
6a45db8a38 | ||
|
|
b272988929 | ||
|
|
9828857570 | ||
|
|
ecb54d8e44 | ||
|
|
344717b021 | ||
|
|
da6cf585c0 | ||
|
|
9e93177afd | ||
|
|
16318efb01 | ||
|
|
e83e13b6d4 | ||
|
|
7c2c695d88 | ||
|
|
c7b227529d | ||
|
|
14f49e9d30 | ||
|
|
1875c4ee73 | ||
|
|
76c68baa1f | ||
|
|
68876a4414 | ||
|
|
26d7e5b60f | ||
|
|
e78af6ae99 | ||
|
|
6fe05cd86e | ||
|
|
4100d7a391 | ||
|
|
c1cc812ea4 | ||
|
|
9663e3f0f4 | ||
|
|
941f618153 | ||
|
|
716a30bf7b | ||
|
|
d051ae576b | ||
|
|
35e99cb014 | ||
|
|
af054fba49 | ||
|
|
58d837d641 | ||
|
|
6cc1bd21cd | ||
|
|
cebfa7c8ae | ||
|
|
e2d7ab8f8f | ||
|
|
ec77987fcd | ||
|
|
b161608d02 | ||
|
|
5e7458ff1c | ||
|
|
ed06aaa1f5 | ||
|
|
13b7179941 | ||
|
|
da0d88d86f | ||
|
|
b7347c312a | ||
|
|
c876f2f7e3 | ||
|
|
adad5fd8ff | ||
|
|
2aaa7ae8c9 | ||
|
|
1f64ea37bd | ||
|
|
cac1525da0 | ||
|
|
1ad906fedc | ||
|
|
20ffef39a3 | ||
|
|
185460c054 | ||
|
|
0079e74d77 | ||
|
|
a8e019482f | ||
|
|
f01b8e47aa | ||
|
|
e43dfb7599 | ||
|
|
6b8cfe2b49 | ||
|
|
7f7b14bae3 | ||
|
|
810b02d9fd | ||
|
|
62813145b2 | ||
|
|
4877058253 | ||
|
|
2a9fd3abb8 | ||
|
|
a244f7b598 | ||
|
|
5396457ad5 | ||
|
|
0186b6ea61 | ||
|
|
f6e99f7e88 | ||
|
|
c24a3828be | ||
|
|
2e38a4567a | ||
|
|
67c5d67a61 | ||
|
|
9592dde534 | ||
|
|
d030b0af70 | ||
|
|
4ccb9ac94d | ||
|
|
8af62b917c | ||
|
|
311f114132 | ||
|
|
d015bd03f7 | ||
|
|
a61b9de0fa | ||
|
|
ef1b283917 | ||
|
|
c677f00105 | ||
|
|
8d2f778dfe | ||
|
|
c549d102f6 | ||
|
|
39a9ee4e9d | ||
|
|
a27dc6ad1c | ||
|
|
8ccb75fc8d | ||
|
|
8fc86dbe02 | ||
|
|
359f052608 | ||
|
|
4150e2265c | ||
|
|
b803ce7435 | ||
|
|
289c6cd7a9 | ||
|
|
31fc7b74c2 | ||
|
|
3e565f25be | ||
|
|
e509be4e21 | ||
|
|
170fc537ac | ||
|
|
3fe5882145 | ||
|
|
a290791410 | ||
|
|
2ebd38ff68 | ||
|
|
cd987a5b19 | ||
|
|
7f1dab7ee1 | ||
|
|
bed3945aa5 | ||
|
|
15a32a18b7 | ||
|
|
d2e8e7dd5d | ||
|
|
ce12ec89c4 | ||
|
|
b286444ad9 | ||
|
|
941263102d | ||
|
|
e2ed296dc7 | ||
|
|
cfc866bef2 | ||
|
|
affba669ce | ||
|
|
7230152ab8 | ||
|
|
21fee7171f | ||
|
|
ad17995f28 | ||
|
|
ac830cbe7f | ||
|
|
65da6af3f9 | ||
|
|
3d90bf7588 | ||
|
|
62ef1c88fe | ||
|
|
d6918920b6 | ||
|
|
cdfb09fbfa | ||
|
|
bb681e31c9 | ||
|
|
c7483936ec | ||
|
|
0a1f2da33d | ||
|
|
f5aafdb7d6 | ||
|
|
c9adf1c492 | ||
|
|
4c9cb560e3 | ||
|
|
f0b028279c | ||
|
|
197770b68b | ||
|
|
37b583f560 | ||
|
|
dca2d4fe12 | ||
|
|
3b677f8ae3 | ||
|
|
0b952578d1 | ||
|
|
054afbbedd | ||
|
|
866a6e4a44 | ||
|
|
8ea7dd478b | ||
|
|
7839252934 | ||
|
|
fa4063220f | ||
|
|
d214a02abd | ||
|
|
d1c12edd1b | ||
|
|
ded1a44c37 | ||
|
|
790a6cd1e3 | ||
|
|
a79f883a0f | ||
|
|
d9c5a540a3 | ||
|
|
276f33b9ec | ||
|
|
ded59d2da0 | ||
|
|
62f7a820d8 | ||
|
|
7063ffa013 | ||
|
|
bf4dc3c095 | ||
|
|
c10e5848bf | ||
|
|
92a3b0d6e0 | ||
|
|
b475bd25c8 | ||
|
|
d318224a6f | ||
|
|
0074fee865 | ||
|
|
5617535a63 | ||
|
|
68ccefc59f | ||
|
|
6d60541626 | ||
|
|
a635e5b8d0 | ||
|
|
48a10440fe | ||
|
|
8e3ba88318 | ||
|
|
ab8fccc544 | ||
|
|
8319dc9164 | ||
|
|
6829d3cdea | ||
|
|
3ae4d69110 | ||
|
|
dc665f227e | ||
|
|
a83496568f | ||
|
|
12d25570af | ||
|
|
378c947654 | ||
|
|
bd39a3140e | ||
|
|
7d3ca3dec1 | ||
|
|
1cb556c8f8 | ||
|
|
8c8f96de1c | ||
|
|
318cd87a9a | ||
|
|
5d63d5c2d3 | ||
|
|
7d347fccc6 | ||
|
|
a54ca799b9 | ||
|
|
f5bc1a996f | ||
|
|
8591bc4d01 | ||
|
|
40888c07f3 | ||
|
|
1c965c3788 | ||
|
|
4df690c2a2 | ||
|
|
d6abb61e2b | ||
|
|
3434c862e9 | ||
|
|
ea8af926fa | ||
|
|
c3df48174c | ||
|
|
f1e60f96c4 | ||
|
|
cdd852678b | ||
|
|
bf518b5467 | ||
|
|
ffd53fab26 | ||
|
|
5aad7dad35 | ||
|
|
b1c1a9f4e1 | ||
|
|
d847a8e0b2 | ||
|
|
9668730b5d | ||
|
|
dc049cf26a | ||
|
|
82c2b2f128 | ||
|
|
5f81d65911 | ||
|
|
19f990c564 | ||
|
|
62467007b2 | ||
|
|
d7624e5e1f | ||
|
|
4505f10e50 | ||
|
|
3ce3df5e19 | ||
|
|
8d4e4ba6c9 | ||
|
|
d78868b462 | ||
|
|
01a681ad00 | ||
|
|
adfeb61eab | ||
|
|
8c3faac343 | ||
|
|
c81acce31c | ||
|
|
fe629ce77c | ||
|
|
5c27add2b2 | ||
|
|
ff90dae695 | ||
|
|
4f8aec6a05 | ||
|
|
ba8df57580 | ||
|
|
3bab1940c1 | ||
|
|
e300c92215 | ||
|
|
99764e25ed | ||
|
|
b5dcae11a4 | ||
|
|
f69f475ab9 | ||
|
|
061e929705 | ||
|
|
9e8193a6ac | ||
|
|
1601796c6c | ||
|
|
a0af0dde0a | ||
|
|
c5ba6a238a | ||
|
|
3c3ebe7d9d | ||
|
|
b7227bfad7 | ||
|
|
447c16cff1 | ||
|
|
47529ae3f5 | ||
|
|
149af96d51 | ||
|
|
14e9dd1c04 | ||
|
|
ec9ef98981 | ||
|
|
922f474b59 | ||
|
|
795a1c78df | ||
|
|
6ea055d486 | ||
|
|
b0c4567cb8 | ||
|
|
6e05120aa5 | ||
|
|
c5958f1257 | ||
|
|
0faf86c9e9 | ||
|
|
ee689922a3 | ||
|
|
be77a181ec | ||
|
|
30376c960f | ||
|
|
405147b1c5 | ||
|
|
d545dfe49b | ||
|
|
c571cf2c53 | ||
|
|
fd5d71e24d | ||
|
|
2c3809d2bc | ||
|
|
0fc8e7cbc8 | ||
|
|
787e47f634 | ||
|
|
dc47da75f2 | ||
|
|
6b999b0a0c | ||
|
|
b00d2d16d4 | ||
|
|
97d4da568b | ||
|
|
e563049f6a | ||
|
|
cc00d0dc08 | ||
|
|
2a220c3984 | ||
|
|
1d440d25be | ||
|
|
ba5234e850 | ||
|
|
293f7150f1 | ||
|
|
41b1cec8d3 | ||
|
|
c129c1eeae | ||
|
|
caa9694543 | ||
|
|
ac79765372 | ||
|
|
39500f054d | ||
|
|
f2d5cc91db | ||
|
|
84a788fd9e | ||
|
|
3778bfe1b5 | ||
|
|
63b2d8e0bd | ||
|
|
e7490b8d75 | ||
|
|
2e050d06e8 | ||
|
|
5fd1154039 | ||
|
|
39401a78ec | ||
|
|
273aa42b17 | ||
|
|
603917d21e | ||
|
|
e55cd6a938 | ||
|
|
2aaed14a3a | ||
|
|
511c25163d | ||
|
|
c24e50f3a0 | ||
|
|
2d732288a7 | ||
|
|
56e715cd3c | ||
|
|
074d8005bc | ||
|
|
5b38c9442a | ||
|
|
3b8dc66da6 | ||
|
|
f5ebfc92fc | ||
|
|
9de063aced | ||
|
|
331691cc9d | ||
|
|
1a97cdf91d | ||
|
|
fbd4359d61 | ||
|
|
f31a60d9bb | ||
|
|
79aca0e579 | ||
|
|
6d35c47b6b | ||
|
|
f1261398e9 |
95
.github/ISSUE_TEMPLATE/bug-issue.yml
vendored
@@ -1,5 +1,5 @@
|
|||||||
name: 🐞 Bug report
|
name: 🐞 Bug report
|
||||||
description: Report a very clearly broken issue.
|
description: Create a new bug report.
|
||||||
title: 'bug: <title>'
|
title: 'bug: <title>'
|
||||||
labels: [bug]
|
labels: [bug]
|
||||||
body:
|
body:
|
||||||
@@ -8,53 +8,20 @@ body:
|
|||||||
value: |
|
value: |
|
||||||
# ReVanced Manager bug report
|
# ReVanced Manager bug report
|
||||||
|
|
||||||
Important to note that your issue may have already been reported before. Please check for existing issues [here](https://github.com/revanced/revanced-manager/labels/bug).
|
Please check for existing issues [here](https://github.com/revanced/revanced-manager/labels/bug) before creating a new one.
|
||||||
|
|
||||||
- type: dropdown
|
|
||||||
attributes:
|
|
||||||
label: Type
|
|
||||||
options:
|
|
||||||
- Error while running the manager
|
|
||||||
- Error at runtime
|
|
||||||
- Cosmetic
|
|
||||||
- Other
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Bug description
|
label: Bug description
|
||||||
description: How did you find the bug? Any additional details that might help?
|
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:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Steps to reproduce
|
label: Version of ReVanced Manager and version & name of application you tried to patch
|
||||||
description: Add the steps to reproduce this bug including your environment.
|
|
||||||
placeholder: Step 1. Download some files. Step 2. ...
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Android version
|
|
||||||
description: Android version used.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Manager version
|
|
||||||
description: Manager version used.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Target package name
|
|
||||||
description: App you tried to patch.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Target package version.
|
|
||||||
description: Version of the app you tried to patch.
|
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
@@ -64,57 +31,31 @@ body:
|
|||||||
- Non-root
|
- Non-root
|
||||||
- Root
|
- Root
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: false
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Patches selected.
|
label: Device logs
|
||||||
description: Patches you selected for the app.
|
description: Export logs in ReVanced Manager settings.
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Device logs (exported using Manager settings).
|
|
||||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
|
||||||
render: shell
|
render: shell
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Installer logs (exported using Installer menu option) [unneeded if issue is not during patching].
|
label: Patcher logs
|
||||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
description: Export logs in "Patcher" screen.
|
||||||
render: shell
|
render: shell
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Screenshots or videos
|
|
||||||
description: Add screenshots or videos that show the bug here.
|
|
||||||
placeholder: Drag and drop the screenshots/videos into this box.
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Solution
|
|
||||||
description: If applicable, add a possible solution.
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Additional context
|
|
||||||
description: Add additional context here.
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: acknowledgements
|
|
||||||
attributes:
|
attributes:
|
||||||
label: Acknowledgements
|
label: Acknowledgements
|
||||||
description: Your issue will be closed if you haven't done these steps.
|
description: Your issue will be closed if you don't follow the checklist below!
|
||||||
options:
|
options:
|
||||||
- label: I have searched the existing issues and this is a new and no duplicate or related to another open issue.
|
- label: This request is not a duplicate of an existing issue.
|
||||||
required: true
|
required: true
|
||||||
- label: I have written a short but informative title.
|
- label: I have chosen an appropriate title.
|
||||||
required: true
|
required: true
|
||||||
- label: I filled out all of the requested information in this issue properly.
|
- label: All requested information has been provided properly.
|
||||||
required: true
|
required: true
|
||||||
- label: The issue is related solely to the ReVanced Manager
|
- label: The issue is solely related to the ReVanced Manager
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
42
.github/ISSUE_TEMPLATE/feature-issue.yml
vendored
@@ -1,52 +1,42 @@
|
|||||||
name: ⭐ Feature request
|
name: ⭐ Feature request
|
||||||
description: Create a detailed feature request.
|
description: Create a new feature request.
|
||||||
title: 'feat: <title>'
|
title: 'feat: <title>'
|
||||||
labels: [feature-request]
|
labels: 'feature request'
|
||||||
body:
|
body:
|
||||||
- type: dropdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
label: Type
|
value: |
|
||||||
options:
|
# ReVanced Manager feature request
|
||||||
- Functionality
|
|
||||||
- Cosmetic
|
Please check for existing feature requests [here](https://github.com/revanced/revanced-manager/labels/bug) before creating a new one.
|
||||||
- Other
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Issue
|
label: Feature description
|
||||||
description: What is the current problem. Why does it require a feature request?
|
description: Describe your feature in detail.
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Feature
|
|
||||||
description: Describe your feature in detail. How does it solve the issue?
|
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Motivation
|
label: Motivation
|
||||||
description: Why should your feature should be considered?
|
description: Explain why the lack of it is a problem.
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Additional context
|
label: Additional context
|
||||||
description: Add additional context here.
|
description: In case there is something else you want to add.
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: acknowledgements
|
|
||||||
attributes:
|
attributes:
|
||||||
label: Acknowledgements
|
label: Acknowledgements
|
||||||
description: Your issue will be closed if you haven't done these steps.
|
description: Your issue will be closed if you don't follow the checklist below!
|
||||||
options:
|
options:
|
||||||
- label: I have searched the existing issues and this is a new and no duplicate or related to another open issue.
|
- label: This request is not a duplicate of an existing issue.
|
||||||
required: true
|
required: true
|
||||||
- label: I have written a short but informative title.
|
- label: I have chosen an appropriate title.
|
||||||
required: true
|
required: true
|
||||||
- label: I filled out all of the requested information in this issue properly.
|
- label: All requested information has been provided properly.
|
||||||
required: true
|
required: true
|
||||||
- label: The issue is related solely to the ReVanced Manager
|
- label: The issue is solely related to the ReVanced Manager
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
2
.github/config.yaml
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
firstPRMergeComment: >
|
||||||
|
Thank you for contributing to ReVanced. Join us on [Discord](https://revanced.app/discord) if you want to receive a contributor role.
|
||||||
39
.github/workflows/debug-build.yml
vendored
@@ -1,39 +0,0 @@
|
|||||||
name: "Debug Build"
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- "flutter-disabled"
|
|
||||||
tags-ignore:
|
|
||||||
- "*"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v1
|
|
||||||
- name: Set up JDK 12
|
|
||||||
uses: actions/setup-java@v1
|
|
||||||
with:
|
|
||||||
java-version: '12.x'
|
|
||||||
- uses: subosito/flutter-action@v1
|
|
||||||
with:
|
|
||||||
channel: 'stable'
|
|
||||||
- name: Set up Flutter
|
|
||||||
run: flutter pub get
|
|
||||||
- name: Generate files with Builder
|
|
||||||
run: flutter packages pub run build_runner build --delete-conflicting-outputs
|
|
||||||
- name: Build with Flutter
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: flutter build apk --debug
|
|
||||||
- name: Add version to APK
|
|
||||||
run: mv build/app/outputs/flutter-apk/app-debug.apk revanced-manager-latest.apk
|
|
||||||
- name: Publish debug APK
|
|
||||||
uses: "marvinpinto/action-automatic-releases@latest"
|
|
||||||
with:
|
|
||||||
repo_token: "${{ secrets.GITHUB_TOKEN }}"
|
|
||||||
automatic_release_tag: "latest"
|
|
||||||
prerelease: true
|
|
||||||
title: "Development Build"
|
|
||||||
files: revanced-manager-latest.apk
|
|
||||||
121
.github/workflows/pr-build.yml
vendored
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
name: PR Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
# Flutter
|
||||||
|
flutter-branch:
|
||||||
|
description: Flutter branch
|
||||||
|
type: choice
|
||||||
|
default: 'stable'
|
||||||
|
options:
|
||||||
|
- stable
|
||||||
|
- beta
|
||||||
|
- dev
|
||||||
|
- master
|
||||||
|
flutter-cache:
|
||||||
|
description: Cache
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
# Application configuration
|
||||||
|
app-flavour:
|
||||||
|
description: App flavour
|
||||||
|
default: 'release'
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- release
|
||||||
|
- debug
|
||||||
|
- profile
|
||||||
|
# Pull Request
|
||||||
|
pr-number:
|
||||||
|
description: PR number (No hashtag)
|
||||||
|
required: true
|
||||||
|
|
||||||
|
run-name: "Build PR ${{ inputs.pr-number }}"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build the application
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- name: Setup
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
gh repo clone "${{ github.repository }}"
|
||||||
|
cd revanced-manager
|
||||||
|
gh repo set-default "${{ github.repository }}"
|
||||||
|
gh pr checkout "${{ inputs.pr-number }}"
|
||||||
|
|
||||||
|
echo "DATETIME=$( TZ='UTC+0' date --rfc-email )" >> $GITHUB_ENV
|
||||||
|
echo "COMMIT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup JDK
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
java-version: '17'
|
||||||
|
distribution: 'zulu'
|
||||||
|
cache: gradle
|
||||||
|
|
||||||
|
- name: Setup Flutter
|
||||||
|
uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
channel: ${{ inputs.flutter-branch }}
|
||||||
|
cache: ${{ inputs.flutter-cache }}
|
||||||
|
|
||||||
|
- name: Install Flutter dependencies
|
||||||
|
run: flutter pub get
|
||||||
|
|
||||||
|
- name: Generate files with Builder
|
||||||
|
run: dart run build_runner build --delete-conflicting-outputs
|
||||||
|
|
||||||
|
- name: Build with Flutter
|
||||||
|
continue-on-error: true
|
||||||
|
id: flutter-build
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
flutter build apk --"${{ inputs.app-flavour }}";
|
||||||
|
|
||||||
|
- name: Prepare to comment
|
||||||
|
run: |
|
||||||
|
if [[ "${{ steps.flutter-build.outcome }}" == "success" ]]; then
|
||||||
|
echo "MESSAGE=✅ ReVanced Manager ${{ env.COMMIT_HASH }} build succeeded." >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "MESSAGE=🚫 ReVanced Manager ${{ env.COMMIT_HASH }} build failed." >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: "Comment to Pull Request #${{ inputs.pr-number }}"
|
||||||
|
uses: thollander/actions-comment-pull-request@v2
|
||||||
|
with:
|
||||||
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
|
pr_number: ${{ inputs.pr-number }}
|
||||||
|
mode: recreate
|
||||||
|
message: |
|
||||||
|
## ⚒️ ReVanced PR Build workflow
|
||||||
|
|
||||||
|
${{ env.MESSAGE }}
|
||||||
|
|
||||||
|
You can see more details in run [${{ github.run_id }}](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})!
|
||||||
|
|
||||||
|
### ⚙️ Overview
|
||||||
|
- App flavor: ${{ inputs.app-flavour }}
|
||||||
|
- Branch: ${{ inputs.flutter-branch }}
|
||||||
|
- Start time: ${{ env.DATETIME }}
|
||||||
|
|
||||||
|
- name: Upload build
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
if-no-files-found: error
|
||||||
|
name: revanced-manager-(${{ env.COMMIT_HASH }}-${{ inputs.pr-number }}-${{ inputs.app-flavour }})-${{ inputs.flutter-branch }}
|
||||||
|
path: |
|
||||||
|
build/app/outputs/flutter-apk/app-${{ inputs.app-flavour }}.apk
|
||||||
|
build/app/outputs/flutter-apk/app-${{ inputs.app-flavour }}.apk.sha1
|
||||||
86
.github/workflows/release-build.yml
vendored
@@ -1,49 +1,75 @@
|
|||||||
name: "Release Build"
|
name: "Release Build"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
tags:
|
branches:
|
||||||
- "v*"
|
- main
|
||||||
|
- dev
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v4
|
||||||
- name: Set env
|
|
||||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
- name: Set up JDK
|
||||||
- name: Set up JDK 12
|
uses: actions/setup-java@v4
|
||||||
uses: actions/setup-java@v1
|
|
||||||
with:
|
with:
|
||||||
java-version: '12.x'
|
java-version: "17"
|
||||||
- uses: subosito/flutter-action@v1
|
distribution: "zulu"
|
||||||
|
|
||||||
|
- uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
channel: 'stable'
|
channel: "stable"
|
||||||
- name: Set up Flutter
|
|
||||||
run: flutter pub get
|
- name: Cache Node modules
|
||||||
- name: Generate files with Builder
|
uses: actions/cache@v3
|
||||||
run: flutter packages pub run build_runner build --delete-conflicting-outputs
|
with:
|
||||||
- name: Build with Flutter
|
path: |
|
||||||
|
node_modules
|
||||||
|
key: npm-${{ hashFiles('package-lock.json') }}
|
||||||
|
|
||||||
|
- name: Setup semantic-release
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
- name: Get version code
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
|
run: |
|
||||||
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
|
npx semantic-release --extends ./.releaserc.getversion.json --no-ci --dry-run
|
||||||
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }}
|
echo "RELEASE_VERSION=v$(cat .VERSION)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Bump pubspec version
|
||||||
|
run: |
|
||||||
|
VERSION=$(echo "${RELEASE_VERSION}" | sed 's/v//')
|
||||||
|
IFS='.' read -r -a nums <<< "${VERSION/-dev/}.0"
|
||||||
|
VERSIONCODE=$((nums[0] * 100000000 + nums[1] * 100000 + nums[2] * 100 + nums[3]))
|
||||||
|
sed -i "/^version/c\\version: $VERSION+$VERSIONCODE" pubspec.yaml
|
||||||
|
|
||||||
|
- name: Set up Flutter
|
||||||
|
run: flutter pub get
|
||||||
|
|
||||||
|
- name: Generate files with Builder
|
||||||
|
run: dart run build_runner build --delete-conflicting-outputs
|
||||||
|
|
||||||
|
- name: Build with Flutter
|
||||||
run: flutter build apk
|
run: flutter build apk
|
||||||
|
|
||||||
- name: Sign APK
|
- name: Sign APK
|
||||||
id: sign_apk
|
id: sign_apk
|
||||||
uses: r0adkll/sign-android-release@v1
|
uses: ilharp/sign-android-release@v1
|
||||||
with:
|
with:
|
||||||
releaseDirectory: build/app/outputs/apk/release
|
releaseDir: build/app/outputs/apk/release
|
||||||
signingKeyBase64: ${{ secrets.SIGNING_KEYSTORE }}
|
signingKey: ${{ secrets.SIGNING_KEYSTORE }}
|
||||||
keyStorePassword: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }}
|
keyStorePassword: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }}
|
||||||
alias: ${{ secrets.SIGNING_KEY_ALIAS }}
|
keyAlias: ${{ secrets.SIGNING_KEY_ALIAS }}
|
||||||
keyPassword: ${{ secrets.SIGNING_KEY_PASSWORD }}
|
keyPassword: ${{ secrets.SIGNING_KEY_PASSWORD }}
|
||||||
- name: Add version to APK
|
|
||||||
run: mv ${{steps.sign_apk.outputs.signedReleaseFile}} revanced-manager-${{ env.RELEASE_VERSION }}.apk
|
- name: Rename APK
|
||||||
|
run: mv ${{steps.sign_apk.outputs.signedFile}} revanced-manager-${{ env.RELEASE_VERSION }}.apk
|
||||||
|
|
||||||
- name: Publish release APK
|
- name: Publish release APK
|
||||||
uses: "marvinpinto/action-automatic-releases@latest"
|
env:
|
||||||
with:
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
repo_token: "${{ secrets.GITHUB_TOKEN }}"
|
run: npx semantic-release --extends ./.releaserc.release.json
|
||||||
prerelease: false
|
|
||||||
files: revanced-manager-${{ env.RELEASE_VERSION }}.apk
|
|
||||||
|
|||||||
19
.github/workflows/update-documentation.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
name: Update documentation
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- docs/**
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
trigger:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Dispatch event to documentation repository
|
||||||
|
if: github.ref == 'refs/heads/main'
|
||||||
|
steps:
|
||||||
|
- uses: peter-evans/repository-dispatch@v2
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.DOCUMENTATION_REPO_ACCESS_TOKEN }}
|
||||||
|
repository: revanced/revanced-documentation
|
||||||
|
event-type: update-documentation
|
||||||
|
client-payload: '{"repo": "${{ github.event.repository.name }}", "ref": "${{ github.ref }}"}'
|
||||||
10
.gitignore
vendored
@@ -9,6 +9,7 @@
|
|||||||
.buildlog/
|
.buildlog/
|
||||||
.history
|
.history
|
||||||
.svn/
|
.svn/
|
||||||
|
local.properties
|
||||||
|
|
||||||
# IntelliJ related
|
# IntelliJ related
|
||||||
*.iml
|
*.iml
|
||||||
@@ -58,6 +59,7 @@ unlinked.ds
|
|||||||
unlinked_spec.ds
|
unlinked_spec.ds
|
||||||
|
|
||||||
# Android related
|
# Android related
|
||||||
|
.gradle/
|
||||||
**/android/**/gradle-wrapper.jar
|
**/android/**/gradle-wrapper.jar
|
||||||
**/android/.gradle
|
**/android/.gradle
|
||||||
**/android/captures/
|
**/android/captures/
|
||||||
@@ -134,5 +136,11 @@ app.*.map.json
|
|||||||
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
|
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
|
||||||
!/dev/ci/**/Gemfile.lock
|
!/dev/ci/**/Gemfile.lock
|
||||||
|
|
||||||
Firebase related
|
# Firebase related
|
||||||
.firebase
|
.firebase
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# FVM
|
||||||
|
.fvm
|
||||||
24
.releaserc.getversion.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"branches": [
|
||||||
|
"main",
|
||||||
|
{
|
||||||
|
"name": "dev",
|
||||||
|
"prerelease": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
[
|
||||||
|
"@semantic-release/commit-analyzer", {
|
||||||
|
"releaseRules": [
|
||||||
|
{ "type": "build", "scope": "Needs bump", "release": "patch" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@semantic-release/exec",
|
||||||
|
{
|
||||||
|
"verifyReleaseCmd": "echo ${nextRelease.version} > .VERSION"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
46
.releaserc.release.json
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"branches": [
|
||||||
|
"main",
|
||||||
|
{
|
||||||
|
"name": "dev",
|
||||||
|
"prerelease": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
[
|
||||||
|
"@semantic-release/commit-analyzer", {
|
||||||
|
"releaseRules": [
|
||||||
|
{ "type": "build", "scope": "Needs bump", "release": "patch" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@semantic-release/changelog",
|
||||||
|
"@semantic-release/release-notes-generator",
|
||||||
|
[
|
||||||
|
"@semantic-release/git",
|
||||||
|
{
|
||||||
|
"assets": [
|
||||||
|
"pubspec.yaml"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@semantic-release/github",
|
||||||
|
{
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"path": "revanced-manager-v*.apk"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"successComment": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@saithodev/semantic-release-backmerge",
|
||||||
|
{
|
||||||
|
"backmergeBranches": [{"from": "main", "to": "dev"}],
|
||||||
|
"clearWorkspace": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<component name="ProjectRunConfigurationManager">
|
|
||||||
<configuration default="false" name="main.dart" type="FlutterRunConfigurationType" factoryName="Flutter">
|
|
||||||
<option name="filePath" value="$PROJECT_DIR$/lib/main.dart" />
|
|
||||||
<method v="2" />
|
|
||||||
</configuration>
|
|
||||||
</component>
|
|
||||||
103
CONTRIBUTING.md
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
<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-round.svg" />
|
||||||
|
<img height="24px" src="assets/revanced-logo/revanced-logo-round.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>
|
||||||
|
|
||||||
|
# 👋 Contribution guidelines
|
||||||
|
|
||||||
|
This document describes how to contribute to ReVanced Manager.
|
||||||
|
|
||||||
|
## 📖 Resources to help you get started
|
||||||
|
|
||||||
|
* 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
|
||||||
|
|
||||||
|
## 🙏 Submitting a feature request
|
||||||
|
|
||||||
|
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-issue.yml&title=feat%3A+%3Ctitle%3E).
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> We may reject your request at the discretion of ReVanced Manager's maintainers,
|
||||||
|
> please provide good motivation for a request to be accepted.
|
||||||
|
|
||||||
|
## 🐞 Submitting a bug report
|
||||||
|
|
||||||
|
If you encounter a bug while using the ReVanced Manager app, open an issue using the
|
||||||
|
[bug report issue template](https://github.com/ReVanced/revanced-manager/issues/new?assignees=&labels=bug&projects=&template=bug-issue.yml&title=bug%3A+%3Ctitle%3E).
|
||||||
|
|
||||||
|
## 📝 How to contribute
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> We recommend that you discuss your changes with
|
||||||
|
> the maintainers of ReVanced Manager before contributing.
|
||||||
|
> This will help you determine whether your change is acceptable.
|
||||||
|
|
||||||
|
1. Fork the repository and create a new branch based off the `dev` branch
|
||||||
|
2. Commit your changes
|
||||||
|
3. Open a pull request to the `dev` branch and reference issues that your pull request closes
|
||||||
|
4. The maintainers of ReVanced Manager will review and provide suggestions.
|
||||||
|
Once your pull request is approved and merged, it 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.
|
||||||
107
README.md
@@ -1,30 +1,97 @@
|
|||||||
|
<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-round.svg" />
|
||||||
|
<img height="24px" src="assets/revanced-logo/revanced-logo-round.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
|
# 💊 ReVanced Manager
|
||||||
|
|
||||||
The official ReVanced Manager based on Flutter.
|
[](https://github.com/ReVanced/revanced-manager/commits "")
|
||||||
|
[](https://github.com/ReVanced/revanced-manager/commits "")
|
||||||
|
|
||||||
|
ReVanced Manager is an Android application that uses ReVanced Patcher to add, remove, and modify existing functionalities in Android applications.
|
||||||
|
|
||||||
|
## 💪 Features
|
||||||
|
|
||||||
|
Some of the features we provide are:
|
||||||
|
|
||||||
|
* 📱 **Portable**: ReVanced Patcher that fit in your pocket;
|
||||||
|
* 🤗 **Intuitive UI**: Help you manage your patched applications with easy-to-use interface;
|
||||||
|
* 🛠️ **Customization**: Patch with third-party sources;
|
||||||
|
* ✨ And a **lot more!**
|
||||||
|
|
||||||
## 🔽 Download
|
## 🔽 Download
|
||||||
To download the Alpha version of Manager, go [here](https://github.com/revanced/revanced-manager/releases/latest) and install the provided APK file.
|
|
||||||
|
|
||||||
## 📝 Prerequisites
|
You can get ReVanced Manager by downloading from [ReVanced site](https://revanced.app/download) or [GitHub releases](https://github.com/ReVanced/revanced-manager/releases).
|
||||||
1. Android 8 or higher
|
|
||||||
2. Does not work on some armv7 devices
|
|
||||||
3. [Vanced MicroG](https://github.com/TeamVanced/VancedMicroG/releases) required for YouTube and YouTube Music (Only for non-root)
|
|
||||||
|
|
||||||
## ⚠️ Disclaimer
|
## 📚 Everything else
|
||||||
*Please note that even though we're releasing the Manager, it is an ALPHA version. There's a big chance that the Manager might not work at all for you.*
|
|
||||||
|
|
||||||
## 🔴 Issues
|
### 📄 Documentation
|
||||||
For suggestions and bug reports, open an issue [here](https://github.com/revanced/revanced-manager/issues/new/choose).
|
|
||||||
|
|
||||||
## 💭 Discussion
|
Documentation on how to use the application is available [here](/docs/README.md).
|
||||||
If you wish to discuss the Manager, a thread has been made under the [#development](https://discord.com/channels/952946952348270622/1002922226443632761) channel in the Discord server, please note that this thread may be temporary and may be removed in the future.
|
|
||||||
|
|
||||||
|
### 👋 Contributing
|
||||||
|
|
||||||
## 🛠️ Building Manager from source
|
Thank you for considering contributing to ReVanced Manager, you can find the contribution guidelines [here](/CONTRIBUTING.md).
|
||||||
1. Setup flutter environment for your [platform](https://docs.flutter.dev/get-started/install)
|
|
||||||
2. Clone the repository locally
|
### 🔴 Issues
|
||||||
3. Add your github token in gradle.properties like [this](https://github.com/revanced/revanced-documentation/wiki/Building-from-source)
|
|
||||||
4. Open the project in terminal
|
For suggestions and bug reports, open an issue [here](https://github.com/ReVanced/revanced-manager/issues/choose).
|
||||||
5. Run `flutter pub get` in terminal
|
|
||||||
6. Then `flutter packages pub run build_runner build --delete-conflicting-outputs` (Must be done on each git pull)
|
## ⚖️ License
|
||||||
7. To build release apk run `flutter build apk`
|
|
||||||
|
ReVanced Manager adopts the [GNU General Public License 3.0](/LICENSE), [tl;dr](https://www.tldrlegal.com/license/gnu-general-public-license-v3-gpl-3): You may copy, distribute and modify the software as long as you track changes/dates in source files. Any modifications to or software including (via compiler) GPL-licensed code must also be made available under the GPL along with build & install instructions.
|
||||||
|
|||||||
78
SECURITY.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<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-round.svg" />
|
||||||
|
<img height="24px" src="assets/revanced-logo/revanced-logo-round.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>
|
||||||
|
|
||||||
|
# 🔒 Security Policy
|
||||||
|
|
||||||
|
This document describes how to report security vulnerabilities for ReVanced Manager.
|
||||||
|
|
||||||
|
## 🚨 Reporting a Vulnerability
|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
|
If a vulnerability is confirmed and accepted, you can join our [Discord](https://discord.gg/revanced) server to receive a special contributor role.
|
||||||
|
|
||||||
|
### ⏳ Supported Versions
|
||||||
|
|
||||||
|
| Version | Branch | Supported |
|
||||||
|
| ------- | ------------|------------------- |
|
||||||
|
| v1.19.0 | main | :white_check_mark: |
|
||||||
|
| latest | dev | :white_check_mark: |
|
||||||
|
| latest | compose-dev | :white_check_mark: |
|
||||||
|
|
||||||
@@ -9,21 +9,153 @@
|
|||||||
# packages, and plugins designed to encourage good coding practices.
|
# packages, and plugins designed to encourage good coding practices.
|
||||||
include: package:flutter_lints/flutter.yaml
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
linter:
|
analyzer:
|
||||||
# The lint rules applied to this project can be customized in the
|
exclude:
|
||||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
- lib/app/app.locator.dart
|
||||||
# included above or to enable additional rules. A list of all available lints
|
- lib/app/app.router.dart
|
||||||
# and their documentation is published at
|
- lib/models/patch.g.dart
|
||||||
# https://dart-lang.github.io/linter/lints/index.html.
|
- lib/models/patched_application.g.dart
|
||||||
#
|
|
||||||
# Instead of disabling a lint rule for the entire project in the
|
|
||||||
# section below, it can also be suppressed for a single line of code
|
|
||||||
# or a specific dart file by using the `// ignore: name_of_lint` and
|
|
||||||
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
|
||||||
# producing the lint.
|
|
||||||
rules:
|
|
||||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
|
||||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
|
||||||
|
|
||||||
# Additional information about this file can be found at
|
linter:
|
||||||
# https://dart.dev/guides/language/analysis-options
|
rules:
|
||||||
|
- always_declare_return_types
|
||||||
|
- require_trailing_commas
|
||||||
|
- always_put_control_body_on_new_line
|
||||||
|
- always_use_package_imports # we do this commonly
|
||||||
|
- annotate_overrides
|
||||||
|
- avoid_bool_literals_in_conditional_expressions
|
||||||
|
- avoid_double_and_int_checks
|
||||||
|
- avoid_empty_else
|
||||||
|
- avoid_equals_and_hash_code_on_mutable_classes
|
||||||
|
- avoid_escaping_inner_quotes
|
||||||
|
- avoid_field_initializers_in_const_classes
|
||||||
|
- avoid_function_literals_in_foreach_calls
|
||||||
|
- avoid_implementing_value_types
|
||||||
|
- avoid_init_to_null
|
||||||
|
- avoid_js_rounded_ints
|
||||||
|
- avoid_null_checks_in_equality_operators
|
||||||
|
- avoid_print
|
||||||
|
- avoid_redundant_argument_values
|
||||||
|
- avoid_relative_lib_imports
|
||||||
|
- avoid_renaming_method_parameters
|
||||||
|
- avoid_return_types_on_setters
|
||||||
|
- avoid_returning_null_for_void
|
||||||
|
- avoid_setters_without_getters
|
||||||
|
- avoid_shadowing_type_parameters
|
||||||
|
- avoid_single_cascade_in_expression_statements
|
||||||
|
- avoid_type_to_string
|
||||||
|
- avoid_types_as_parameter_names
|
||||||
|
- avoid_unnecessary_containers
|
||||||
|
- avoid_void_async
|
||||||
|
- avoid_web_libraries_in_flutter # we use web libraries in web-specific code, and our tests prevent us from using them elsewhere
|
||||||
|
- await_only_futures
|
||||||
|
- camel_case_extensions
|
||||||
|
- camel_case_types
|
||||||
|
- cancel_subscriptions
|
||||||
|
- cast_nullable_to_non_nullable
|
||||||
|
- close_sinks # not reliable enough
|
||||||
|
- control_flow_in_finally
|
||||||
|
- curly_braces_in_flow_control_structures
|
||||||
|
- depend_on_referenced_packages
|
||||||
|
- deprecated_consistency
|
||||||
|
- directives_ordering
|
||||||
|
- empty_catches
|
||||||
|
- empty_constructor_bodies
|
||||||
|
- empty_statements
|
||||||
|
- eol_at_end_of_file
|
||||||
|
- exhaustive_cases
|
||||||
|
- file_names
|
||||||
|
- flutter_style_todos
|
||||||
|
- hash_and_equals
|
||||||
|
- implementation_imports
|
||||||
|
- collection_methods_unrelated_type
|
||||||
|
- leading_newlines_in_multiline_strings
|
||||||
|
- library_names
|
||||||
|
- library_prefixes
|
||||||
|
- library_private_types_in_public_api
|
||||||
|
- missing_whitespace_between_adjacent_strings
|
||||||
|
- no_adjacent_strings_in_list
|
||||||
|
- no_duplicate_case_values
|
||||||
|
- no_logic_in_create_state
|
||||||
|
- non_constant_identifier_names
|
||||||
|
- noop_primitive_operations
|
||||||
|
- null_check_on_nullable_type_parameter
|
||||||
|
- null_closures
|
||||||
|
- overridden_fields
|
||||||
|
- package_api_docs
|
||||||
|
- package_names
|
||||||
|
- package_prefixed_library_names
|
||||||
|
- prefer_adjacent_string_concatenation
|
||||||
|
- prefer_asserts_in_initializer_lists
|
||||||
|
- prefer_collection_literals
|
||||||
|
- prefer_conditional_assignment
|
||||||
|
- prefer_const_constructors
|
||||||
|
- prefer_const_constructors_in_immutables
|
||||||
|
- prefer_const_declarations
|
||||||
|
- prefer_const_literals_to_create_immutables
|
||||||
|
- prefer_contains
|
||||||
|
- prefer_final_fields
|
||||||
|
- prefer_final_in_for_each
|
||||||
|
- prefer_final_locals
|
||||||
|
- prefer_for_elements_to_map_fromIterable
|
||||||
|
- prefer_foreach
|
||||||
|
- prefer_function_declarations_over_variables
|
||||||
|
- prefer_generic_function_type_aliases
|
||||||
|
- prefer_if_elements_to_conditional_expressions
|
||||||
|
- prefer_if_null_operators
|
||||||
|
- prefer_initializing_formals
|
||||||
|
- prefer_inlined_adds
|
||||||
|
- prefer_interpolation_to_compose_strings
|
||||||
|
- prefer_is_empty
|
||||||
|
- prefer_is_not_empty
|
||||||
|
- prefer_is_not_operator
|
||||||
|
- prefer_iterable_whereType
|
||||||
|
- prefer_mixin # Has false positives, see https://github.com/dart-lang/linter/issues/3018
|
||||||
|
- prefer_null_aware_method_calls # "call()" is confusing to people new to the language since it's not documented anywhere
|
||||||
|
- prefer_null_aware_operators
|
||||||
|
- prefer_single_quotes
|
||||||
|
- prefer_spread_collections
|
||||||
|
- prefer_typing_uninitialized_variables
|
||||||
|
- prefer_void_to_null
|
||||||
|
- provide_deprecation_message
|
||||||
|
- recursive_getters
|
||||||
|
- sized_box_for_whitespace
|
||||||
|
- slash_for_doc_comments
|
||||||
|
- sort_child_properties_last
|
||||||
|
- sort_constructors_first
|
||||||
|
- sort_pub_dependencies
|
||||||
|
- sort_unnamed_constructors_first
|
||||||
|
- test_types_in_equals
|
||||||
|
- throw_in_finally
|
||||||
|
- tighten_type_of_initializing_formals
|
||||||
|
- type_init_formals
|
||||||
|
- unnecessary_brace_in_string_interps
|
||||||
|
- unnecessary_const
|
||||||
|
- unnecessary_getters_setters
|
||||||
|
- unnecessary_new
|
||||||
|
- unnecessary_null_aware_assignments
|
||||||
|
- unnecessary_null_checks
|
||||||
|
- unnecessary_null_in_if_null_operators
|
||||||
|
- unnecessary_nullable_for_final_variable_declarations
|
||||||
|
- unnecessary_overrides
|
||||||
|
- unnecessary_parenthesis
|
||||||
|
- unnecessary_statements
|
||||||
|
- unnecessary_string_escapes
|
||||||
|
- unnecessary_string_interpolations
|
||||||
|
- unnecessary_this
|
||||||
|
- unrelated_type_equality_checks
|
||||||
|
- unsafe_html
|
||||||
|
- use_build_context_synchronously
|
||||||
|
- use_full_hex_values_for_flutter_colors
|
||||||
|
- use_function_type_syntax_for_parameters
|
||||||
|
- use_if_null_to_convert_nulls_to_bools
|
||||||
|
- use_is_even_rather_than_modulo
|
||||||
|
- use_key_in_widget_constructors
|
||||||
|
- use_late_for_private_fields_and_variables
|
||||||
|
- use_named_constants
|
||||||
|
- use_raw_strings
|
||||||
|
- use_rethrow_when_possible
|
||||||
|
- use_setters_to_change_properties
|
||||||
|
- use_test_throws_matchers
|
||||||
|
- valid_regexps
|
||||||
|
- void_checks
|
||||||
|
|||||||
3
android/Gemfile
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
source "https://rubygems.org"
|
||||||
|
|
||||||
|
gem "fastlane"
|
||||||
@@ -1,3 +1,9 @@
|
|||||||
|
plugins {
|
||||||
|
id "com.android.application"
|
||||||
|
id "kotlin-android"
|
||||||
|
id "dev.flutter.flutter-gradle-plugin"
|
||||||
|
}
|
||||||
|
|
||||||
def localProperties = new Properties()
|
def localProperties = new Properties()
|
||||||
def localPropertiesFile = rootProject.file('local.properties')
|
def localPropertiesFile = rootProject.file('local.properties')
|
||||||
if (localPropertiesFile.exists()) {
|
if (localPropertiesFile.exists()) {
|
||||||
@@ -6,11 +12,6 @@ if (localPropertiesFile.exists()) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def flutterRoot = localProperties.getProperty('flutter.sdk')
|
|
||||||
if (flutterRoot == null) {
|
|
||||||
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
|
|
||||||
}
|
|
||||||
|
|
||||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||||
if (flutterVersionCode == null) {
|
if (flutterVersionCode == null) {
|
||||||
flutterVersionCode = '1'
|
flutterVersionCode = '1'
|
||||||
@@ -21,46 +22,73 @@ if (flutterVersionName == null) {
|
|||||||
flutterVersionName = '1.0'
|
flutterVersionName = '1.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
apply plugin: 'com.android.application'
|
|
||||||
apply plugin: 'kotlin-android'
|
|
||||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 33
|
compileSdk 34
|
||||||
ndkVersion flutter.ndkVersion
|
ndkVersion flutter.ndkVersion
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_11
|
sourceCompatibility JavaVersion.VERSION_17
|
||||||
targetCompatibility JavaVersion.VERSION_11
|
targetCompatibility JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = '11'
|
jvmTarget = '17'
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
main.java.srcDirs += 'src/main/kotlin'
|
main.java.srcDirs += 'src/main/kotlin'
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "app.revanced.manager.flutter"
|
applicationId "app.revanced.manager.flutter"
|
||||||
minSdkVersion 26
|
minSdk 26
|
||||||
targetSdkVersion 33
|
targetSdk 34
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
shrinkResources false
|
shrinkResources false
|
||||||
minifyEnabled false
|
minifyEnabled false
|
||||||
|
resValue "string", "app_name", "ReVanced Manager"
|
||||||
signingConfig signingConfigs.debug
|
signingConfig signingConfigs.debug
|
||||||
|
ndk {
|
||||||
|
abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86_64'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debug {
|
||||||
|
shrinkResources false
|
||||||
|
minifyEnabled false
|
||||||
|
resValue "string", "app_name", "ReVanced Manager Debug"
|
||||||
|
applicationIdSuffix ".debug"
|
||||||
|
signingConfig signingConfigs.debug
|
||||||
|
ndk {
|
||||||
|
abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86_64'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
profile {
|
||||||
|
shrinkResources false
|
||||||
|
minifyEnabled false
|
||||||
|
resValue "string", "app_name", "ReVanced Manager Profile"
|
||||||
|
applicationIdSuffix ".profile"
|
||||||
|
signingConfig signingConfigs.debug
|
||||||
|
ndk {
|
||||||
|
abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86_64'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
packagingOptions {
|
||||||
|
jniLibs {
|
||||||
|
useLegacyPackaging true
|
||||||
|
excludes += ['/prebuilt/**']
|
||||||
|
}
|
||||||
|
resources {
|
||||||
|
excludes += ['/prebuilt/**']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
packagingOptions {
|
namespace 'app.revanced.manager.flutter'
|
||||||
exclude '/prebuilt/**'
|
}
|
||||||
}
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(17)
|
||||||
}
|
}
|
||||||
|
|
||||||
flutter {
|
flutter {
|
||||||
@@ -71,14 +99,9 @@ dependencies {
|
|||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||||
|
|
||||||
// ReVanced
|
// ReVanced
|
||||||
implementation "app.revanced:revanced-patcher:6.0.0"
|
implementation "app.revanced:revanced-patcher:19.1.0"
|
||||||
|
|
||||||
// Signing & aligning
|
// Signing & aligning
|
||||||
implementation("org.bouncycastle:bcpkix-jdk15on:1.70")
|
implementation("org.bouncycastle:bcpkix-jdk15on:1.70")
|
||||||
implementation("com.android.tools.build:apksig:7.2.2")
|
implementation("com.android.tools.build:apksig:7.2.2")
|
||||||
|
|
||||||
// MicroG cronet
|
|
||||||
implementation("org.microg:cronet-common:$cronetVersion")
|
|
||||||
implementation("org.microg:cronet-native:$cronetVersion")
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
package="app.revanced.manager.flutter">
|
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -1,23 +1,29 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
package="app.revanced.manager.flutter">
|
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
|
||||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="32" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="32" />
|
||||||
|
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||||
<application
|
<application
|
||||||
android:label="ReVanced Manager"
|
android:label="@string/app_name"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
android:extractNativeLibs="true">
|
android:requestLegacyExternalStorage="true">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@@ -27,14 +33,17 @@
|
|||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="io.flutter.embedding.android.NormalTheme"
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
android:resource="@style/NormalTheme"
|
android:resource="@style/NormalTheme" />
|
||||||
/>
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".ExportSettingsActivity"
|
||||||
|
android:exported="true">
|
||||||
|
</activity>
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2" />
|
||||||
@@ -47,5 +56,22 @@
|
|||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
android:resource="@xml/file_paths" />
|
android:resource="@xml/file_paths" />
|
||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".utils.packageInstaller.InstallerReceiver"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="APP_INSTALL_ACTION" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
<receiver
|
||||||
|
android:name=".utils.packageInstaller.UninstallerReceiver"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="APP_UNINSTALL_ACTION" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package app.revanced.manager.flutter
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageInfo
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Base64
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.File
|
||||||
|
import java.security.cert.CertificateFactory
|
||||||
|
import java.security.cert.X509Certificate
|
||||||
|
import java.security.MessageDigest
|
||||||
|
|
||||||
|
class ExportSettingsActivity : Activity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
val callingPackageName = getCallingPackage()!!
|
||||||
|
|
||||||
|
if (getFingerprint(callingPackageName) == getFingerprint(getPackageName())) {
|
||||||
|
// Create JSON Object
|
||||||
|
val json = JSONObject()
|
||||||
|
|
||||||
|
// Default Data
|
||||||
|
json.put("keystorePassword", "s3cur3p@ssw0rd")
|
||||||
|
|
||||||
|
// Load Shared Preferences
|
||||||
|
val sharedPreferences = getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
|
||||||
|
val allEntries: Map<String, *> = sharedPreferences.getAll()
|
||||||
|
for ((key, value) in allEntries.entries) {
|
||||||
|
json.put(key.replace("flutter.", ""), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load keystore
|
||||||
|
val keystoreFile = File(getExternalFilesDir(null), "/revanced-manager.keystore")
|
||||||
|
if (keystoreFile.exists()) {
|
||||||
|
val keystoreBytes = keystoreFile.readBytes()
|
||||||
|
val keystoreBase64 = Base64.encodeToString(keystoreBytes, Base64.DEFAULT)
|
||||||
|
json.put("keystore", keystoreBase64)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load saved patches
|
||||||
|
val storedPatchesFile = File(filesDir.parentFile.absolutePath, "/app_flutter/selected-patches.json")
|
||||||
|
if (storedPatchesFile.exists()) {
|
||||||
|
val patchesBytes = storedPatchesFile.readBytes()
|
||||||
|
val patches = String(patchesBytes, Charsets.UTF_8)
|
||||||
|
json.put("patches", JSONObject(patches))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send data back
|
||||||
|
val resultIntent = Intent()
|
||||||
|
resultIntent.putExtra("data", json.toString())
|
||||||
|
setResult(Activity.RESULT_OK, resultIntent)
|
||||||
|
finish()
|
||||||
|
} else {
|
||||||
|
val resultIntent = Intent()
|
||||||
|
setResult(Activity.RESULT_CANCELED)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getFingerprint(packageName: String): String {
|
||||||
|
// Get the signature of the app that matches the package name
|
||||||
|
val packageInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES)
|
||||||
|
val signature = packageInfo.signatures[0]
|
||||||
|
|
||||||
|
// Get the raw certificate data
|
||||||
|
val rawCert = signature.toByteArray()
|
||||||
|
|
||||||
|
// Generate an X509Certificate from the data
|
||||||
|
val certFactory = CertificateFactory.getInstance("X509")
|
||||||
|
val x509Cert = certFactory.generateCertificate(ByteArrayInputStream(rawCert)) as X509Certificate
|
||||||
|
|
||||||
|
// Get the SHA256 fingerprint
|
||||||
|
val fingerprint = MessageDigest.getInstance("SHA256").digest(x509Cert.encoded).joinToString("") {
|
||||||
|
"%02x".format(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fingerprint
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,94 +1,233 @@
|
|||||||
package app.revanced.manager.flutter
|
package app.revanced.manager.flutter
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.app.SearchManager
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageInstaller
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import androidx.annotation.NonNull
|
|
||||||
import app.revanced.manager.flutter.utils.Aapt
|
import app.revanced.manager.flutter.utils.Aapt
|
||||||
import app.revanced.manager.flutter.utils.aligning.ZipAligner
|
import app.revanced.manager.flutter.utils.aligning.ZipAligner
|
||||||
|
import app.revanced.manager.flutter.utils.packageInstaller.InstallerReceiver
|
||||||
|
import app.revanced.manager.flutter.utils.packageInstaller.UninstallerReceiver
|
||||||
import app.revanced.manager.flutter.utils.signing.Signer
|
import app.revanced.manager.flutter.utils.signing.Signer
|
||||||
import app.revanced.manager.flutter.utils.zip.ZipFile
|
import app.revanced.manager.flutter.utils.zip.ZipFile
|
||||||
import app.revanced.manager.flutter.utils.zip.structures.ZipEntry
|
import app.revanced.manager.flutter.utils.zip.structures.ZipEntry
|
||||||
|
import app.revanced.patcher.PatchBundleLoader
|
||||||
|
import app.revanced.patcher.PatchSet
|
||||||
import app.revanced.patcher.Patcher
|
import app.revanced.patcher.Patcher
|
||||||
import app.revanced.patcher.PatcherOptions
|
import app.revanced.patcher.PatcherOptions
|
||||||
import app.revanced.patcher.extensions.PatchExtensions.patchName
|
import app.revanced.patcher.patch.PatchResult
|
||||||
import app.revanced.patcher.logging.Logger
|
|
||||||
import app.revanced.patcher.util.patch.PatchBundle
|
|
||||||
import dalvik.system.DexClassLoader
|
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
import kotlinx.coroutines.InternalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.flow.FlowCollector
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.PrintWriter
|
||||||
|
import java.io.StringWriter
|
||||||
|
import java.util.logging.LogRecord
|
||||||
|
import java.util.logging.Logger
|
||||||
|
|
||||||
private const val PATCHER_CHANNEL = "app.revanced.manager.flutter/patcher"
|
|
||||||
private const val INSTALLER_CHANNEL = "app.revanced.manager.flutter/installer"
|
|
||||||
|
|
||||||
class MainActivity : FlutterActivity() {
|
class MainActivity : FlutterActivity() {
|
||||||
private val handler = Handler(Looper.getMainLooper())
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
private lateinit var installerChannel: MethodChannel
|
private lateinit var installerChannel: MethodChannel
|
||||||
|
private var cancel: Boolean = false
|
||||||
|
private var stopResult: MethodChannel.Result? = null
|
||||||
|
|
||||||
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
|
private lateinit var patches: PatchSet
|
||||||
|
|
||||||
|
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||||
super.configureFlutterEngine(flutterEngine)
|
super.configureFlutterEngine(flutterEngine)
|
||||||
val mainChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, PATCHER_CHANNEL)
|
|
||||||
installerChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, INSTALLER_CHANNEL)
|
val patcherChannel = "app.revanced.manager.flutter/patcher"
|
||||||
|
val installerChannel = "app.revanced.manager.flutter/installer"
|
||||||
|
val openBrowserChannel = "app.revanced.manager.flutter/browser"
|
||||||
|
|
||||||
|
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, openBrowserChannel).setMethodCallHandler { call, result ->
|
||||||
|
if (call.method == "openBrowser") {
|
||||||
|
val searchQuery = call.argument<String>("query")
|
||||||
|
openBrowser(searchQuery)
|
||||||
|
result.success(null)
|
||||||
|
} else {
|
||||||
|
result.notImplemented()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val mainChannel =
|
||||||
|
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, patcherChannel)
|
||||||
|
|
||||||
|
this.installerChannel =
|
||||||
|
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, installerChannel)
|
||||||
|
|
||||||
mainChannel.setMethodCallHandler { call, result ->
|
mainChannel.setMethodCallHandler { call, result ->
|
||||||
when (call.method) {
|
when (call.method) {
|
||||||
"runPatcher" -> {
|
"runPatcher" -> {
|
||||||
val patchBundleFilePath = call.argument<String>("patchBundleFilePath")
|
|
||||||
val originalFilePath = call.argument<String>("originalFilePath")
|
val originalFilePath = call.argument<String>("originalFilePath")
|
||||||
val inputFilePath = call.argument<String>("inputFilePath")
|
val inputFilePath = call.argument<String>("inputFilePath")
|
||||||
val patchedFilePath = call.argument<String>("patchedFilePath")
|
val patchedFilePath = call.argument<String>("patchedFilePath")
|
||||||
val outFilePath = call.argument<String>("outFilePath")
|
val outFilePath = call.argument<String>("outFilePath")
|
||||||
val integrationsPath = call.argument<String>("integrationsPath")
|
val integrationsPath = call.argument<String>("integrationsPath")
|
||||||
val selectedPatches = call.argument<List<String>>("selectedPatches")
|
val selectedPatches = call.argument<List<String>>("selectedPatches")
|
||||||
|
val options = call.argument<Map<String, Map<String, Any>>>("options")
|
||||||
val cacheDirPath = call.argument<String>("cacheDirPath")
|
val cacheDirPath = call.argument<String>("cacheDirPath")
|
||||||
val mergeIntegrations = call.argument<Boolean>("mergeIntegrations")
|
|
||||||
val keyStoreFilePath = call.argument<String>("keyStoreFilePath")
|
val keyStoreFilePath = call.argument<String>("keyStoreFilePath")
|
||||||
if (patchBundleFilePath != null &&
|
val keystorePassword = call.argument<String>("keystorePassword")
|
||||||
|
|
||||||
|
if (
|
||||||
originalFilePath != null &&
|
originalFilePath != null &&
|
||||||
inputFilePath != null &&
|
inputFilePath != null &&
|
||||||
patchedFilePath != null &&
|
patchedFilePath != null &&
|
||||||
outFilePath != null &&
|
outFilePath != null &&
|
||||||
integrationsPath != null &&
|
integrationsPath != null &&
|
||||||
selectedPatches != null &&
|
selectedPatches != null &&
|
||||||
|
options != null &&
|
||||||
cacheDirPath != null &&
|
cacheDirPath != null &&
|
||||||
mergeIntegrations != null &&
|
keyStoreFilePath != null &&
|
||||||
keyStoreFilePath != null
|
keystorePassword != null
|
||||||
) {
|
) {
|
||||||
|
cancel = false
|
||||||
runPatcher(
|
runPatcher(
|
||||||
result,
|
result,
|
||||||
patchBundleFilePath,
|
|
||||||
originalFilePath,
|
originalFilePath,
|
||||||
inputFilePath,
|
inputFilePath,
|
||||||
patchedFilePath,
|
patchedFilePath,
|
||||||
outFilePath,
|
outFilePath,
|
||||||
integrationsPath,
|
integrationsPath,
|
||||||
selectedPatches,
|
selectedPatches,
|
||||||
|
options,
|
||||||
cacheDirPath,
|
cacheDirPath,
|
||||||
mergeIntegrations,
|
keyStoreFilePath,
|
||||||
keyStoreFilePath
|
keystorePassword
|
||||||
)
|
)
|
||||||
} else {
|
} else result.notImplemented()
|
||||||
result.notImplemented()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"stopPatcher" -> {
|
||||||
|
cancel = true
|
||||||
|
stopResult = result
|
||||||
|
}
|
||||||
|
|
||||||
|
"getPatches" -> {
|
||||||
|
val patchBundleFilePath = call.argument<String>("patchBundleFilePath")!!
|
||||||
|
val cacheDirPath = call.argument<String>("cacheDirPath")!!
|
||||||
|
|
||||||
|
try {
|
||||||
|
val patchBundleFile = File(patchBundleFilePath)
|
||||||
|
patchBundleFile.setWritable(false)
|
||||||
|
patches = PatchBundleLoader.Dex(
|
||||||
|
patchBundleFile,
|
||||||
|
optimizedDexDirectory = File(cacheDirPath)
|
||||||
|
)
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
return@setMethodCallHandler result.notImplemented()
|
||||||
|
} catch (err: Error) {
|
||||||
|
return@setMethodCallHandler result.notImplemented()
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONArray().apply {
|
||||||
|
patches.forEach {
|
||||||
|
JSONObject().apply {
|
||||||
|
put("name", it.name)
|
||||||
|
put("description", it.description)
|
||||||
|
put("excluded", !it.use)
|
||||||
|
put("compatiblePackages", JSONArray().apply {
|
||||||
|
it.compatiblePackages?.forEach { compatiblePackage ->
|
||||||
|
val compatiblePackageJson = JSONObject().apply {
|
||||||
|
put("name", compatiblePackage.name)
|
||||||
|
put(
|
||||||
|
"versions",
|
||||||
|
JSONArray().apply {
|
||||||
|
compatiblePackage.versions?.forEach { version ->
|
||||||
|
put(version)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
put(compatiblePackageJson)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
put("options", JSONArray().apply {
|
||||||
|
it.options.values.forEach { option ->
|
||||||
|
JSONObject().apply {
|
||||||
|
put("key", option.key)
|
||||||
|
put("title", option.title)
|
||||||
|
put("description", option.description)
|
||||||
|
put("required", option.required)
|
||||||
|
|
||||||
|
fun JSONObject.putValue(
|
||||||
|
value: Any?,
|
||||||
|
key: String = "value"
|
||||||
|
) = if (value is Array<*>) put(
|
||||||
|
key,
|
||||||
|
JSONArray().apply {
|
||||||
|
value.forEach { put(it) }
|
||||||
|
})
|
||||||
|
else put(key, value)
|
||||||
|
|
||||||
|
putValue(option.default)
|
||||||
|
|
||||||
|
option.values?.let { values ->
|
||||||
|
put("values",
|
||||||
|
JSONObject().apply {
|
||||||
|
values.forEach { (key, value) ->
|
||||||
|
putValue(value, key)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} ?: put("values", null)
|
||||||
|
put("valueType", option.valueType)
|
||||||
|
}.let(::put)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}.let(::put)
|
||||||
|
}
|
||||||
|
}.toString().let(result::success)
|
||||||
|
}
|
||||||
|
|
||||||
|
"installApk" -> {
|
||||||
|
val apkPath = call.argument<String>("apkPath")!!
|
||||||
|
PackageInstallerManager.result = result
|
||||||
|
installApk(apkPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
"uninstallApp" -> {
|
||||||
|
val packageName = call.argument<String>("packageName")!!
|
||||||
|
uninstallApp(packageName)
|
||||||
|
PackageInstallerManager.result = result
|
||||||
|
}
|
||||||
|
|
||||||
else -> result.notImplemented()
|
else -> result.notImplemented()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun openBrowser(query: String?) {
|
||||||
|
val intent = Intent(Intent.ACTION_WEB_SEARCH).apply {
|
||||||
|
putExtra(SearchManager.QUERY, query)
|
||||||
|
}
|
||||||
|
if (intent.resolveActivity(packageManager) != null) {
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(InternalCoroutinesApi::class)
|
||||||
private fun runPatcher(
|
private fun runPatcher(
|
||||||
result: MethodChannel.Result,
|
result: MethodChannel.Result,
|
||||||
patchBundleFilePath: String,
|
|
||||||
originalFilePath: String,
|
originalFilePath: String,
|
||||||
inputFilePath: String,
|
inputFilePath: String,
|
||||||
patchedFilePath: String,
|
patchedFilePath: String,
|
||||||
outFilePath: String,
|
outFilePath: String,
|
||||||
integrationsPath: String,
|
integrationsPath: String,
|
||||||
selectedPatches: List<String>,
|
selectedPatches: List<String>,
|
||||||
|
options: Map<String, Map<String, Any>>,
|
||||||
cacheDirPath: String,
|
cacheDirPath: String,
|
||||||
mergeIntegrations: Boolean,
|
keyStoreFilePath: String,
|
||||||
keyStoreFilePath: String
|
keystorePassword: String
|
||||||
) {
|
) {
|
||||||
val originalFile = File(originalFilePath)
|
val originalFile = File(originalFilePath)
|
||||||
val inputFile = File(inputFilePath)
|
val inputFile = File(inputFilePath)
|
||||||
@@ -96,125 +235,148 @@ class MainActivity : FlutterActivity() {
|
|||||||
val outFile = File(outFilePath)
|
val outFile = File(outFilePath)
|
||||||
val integrations = File(integrationsPath)
|
val integrations = File(integrationsPath)
|
||||||
val keyStoreFile = File(keyStoreFilePath)
|
val keyStoreFile = File(keyStoreFilePath)
|
||||||
|
val cacheDir = File(cacheDirPath)
|
||||||
|
|
||||||
Thread {
|
Thread {
|
||||||
try {
|
fun updateProgress(progress: Double, header: String, log: String) {
|
||||||
val patches = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CUPCAKE) {
|
|
||||||
PatchBundle.Dex(
|
|
||||||
patchBundleFilePath,
|
|
||||||
DexClassLoader(
|
|
||||||
patchBundleFilePath,
|
|
||||||
cacheDirPath,
|
|
||||||
null,
|
|
||||||
javaClass.classLoader
|
|
||||||
)
|
|
||||||
).loadPatches().filter { patch -> selectedPatches.any { it == patch.patchName } }
|
|
||||||
} else {
|
|
||||||
TODO("VERSION.SDK_INT < CUPCAKE")
|
|
||||||
}
|
|
||||||
|
|
||||||
handler.post {
|
handler.post {
|
||||||
installerChannel.invokeMethod(
|
installerChannel.invokeMethod(
|
||||||
"update",
|
"update",
|
||||||
mapOf(
|
mapOf(
|
||||||
"progress" to 0.1,
|
"progress" to progress,
|
||||||
"header" to "",
|
"header" to header,
|
||||||
"log" to "Copying original apk"
|
"log" to log
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun postStop() = handler.post { stopResult!!.success(null) }
|
||||||
|
|
||||||
|
// Setup logger
|
||||||
|
Logger.getLogger("").apply {
|
||||||
|
handlers.forEach {
|
||||||
|
it.close()
|
||||||
|
removeHandler(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
object : java.util.logging.Handler() {
|
||||||
|
override fun publish(record: LogRecord) {
|
||||||
|
if (record.loggerName?.startsWith("app.revanced") != true || cancel) return
|
||||||
|
|
||||||
|
updateProgress(-1.0, "", record.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun flush() = Unit
|
||||||
|
override fun close() = flush()
|
||||||
|
}.let(::addHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
updateProgress(0.0, "", "Copying APK")
|
||||||
|
|
||||||
|
if (cancel) {
|
||||||
|
postStop()
|
||||||
|
return@Thread
|
||||||
|
}
|
||||||
|
|
||||||
originalFile.copyTo(inputFile, true)
|
originalFile.copyTo(inputFile, true)
|
||||||
|
|
||||||
handler.post {
|
if (cancel) {
|
||||||
installerChannel.invokeMethod(
|
postStop()
|
||||||
"update",
|
return@Thread
|
||||||
mapOf(
|
|
||||||
"progress" to 0.2,
|
|
||||||
"header" to "Unpacking apk...",
|
|
||||||
"log" to "Unpacking input apk"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val patcher =
|
|
||||||
Patcher(
|
|
||||||
PatcherOptions(
|
|
||||||
inputFile,
|
|
||||||
cacheDirPath,
|
|
||||||
Aapt.binary(applicationContext).absolutePath,
|
|
||||||
cacheDirPath,
|
|
||||||
logger = ManagerLogger()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
handler.post {
|
|
||||||
installerChannel.invokeMethod(
|
|
||||||
"update",
|
|
||||||
mapOf("progress" to 0.3, "header" to "", "log" to "")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (mergeIntegrations) {
|
|
||||||
handler.post {
|
|
||||||
installerChannel.invokeMethod(
|
|
||||||
"update",
|
|
||||||
mapOf(
|
|
||||||
"progress" to 0.4,
|
|
||||||
"header" to "Merging integrations...",
|
|
||||||
"log" to "Merging integrations"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
patcher.addFiles(listOf(integrations)) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handler.post {
|
updateProgress(0.05, "Reading APK...", "Reading APK")
|
||||||
installerChannel.invokeMethod(
|
|
||||||
"update",
|
val patcher = Patcher(
|
||||||
mapOf(
|
PatcherOptions(
|
||||||
"progress" to 0.5,
|
inputFile,
|
||||||
"header" to "Applying patches...",
|
cacheDir,
|
||||||
"log" to ""
|
Aapt.binary(applicationContext).absolutePath,
|
||||||
)
|
cacheDir.path,
|
||||||
|
true // TODO: Add option to disable this
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (cancel) {
|
||||||
|
postStop()
|
||||||
|
return@Thread
|
||||||
}
|
}
|
||||||
|
|
||||||
patcher.addPatches(patches)
|
updateProgress(0.1, "Loading patches...", "Loading patches")
|
||||||
patcher.executePatches().forEach { (patch, res) ->
|
|
||||||
if (res.isSuccess) {
|
val patches = patches.filter { patch ->
|
||||||
val msg = "[success] $patch"
|
val isCompatible = patch.compatiblePackages?.any {
|
||||||
handler.post {
|
it.name == patcher.context.packageMetadata.packageName
|
||||||
installerChannel.invokeMethod(
|
} ?: false
|
||||||
"update",
|
|
||||||
mapOf(
|
val compatibleOrUniversal =
|
||||||
"progress" to 0.5,
|
isCompatible || patch.compatiblePackages.isNullOrEmpty()
|
||||||
"header" to "",
|
|
||||||
"log" to msg
|
compatibleOrUniversal && selectedPatches.any { it == patch.name }
|
||||||
)
|
}.onEach { patch ->
|
||||||
)
|
options[patch.name]?.forEach { (key, value) ->
|
||||||
}
|
patch.options[key] = value
|
||||||
return@forEach
|
|
||||||
}
|
|
||||||
val msg = "[error] $patch:" + res.exceptionOrNull()!!.printStackTrace()
|
|
||||||
handler.post {
|
|
||||||
installerChannel.invokeMethod(
|
|
||||||
"update",
|
|
||||||
mapOf("progress" to 0.5, "header" to "", "log" to msg)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handler.post {
|
if (cancel) {
|
||||||
installerChannel.invokeMethod(
|
postStop()
|
||||||
"update",
|
return@Thread
|
||||||
mapOf(
|
|
||||||
"progress" to 0.7,
|
|
||||||
"header" to "Repacking apk...",
|
|
||||||
"log" to "Repacking patched apk"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
val res = patcher.save()
|
|
||||||
|
updateProgress(0.15, "Executing...", "")
|
||||||
|
|
||||||
|
// Update the progress bar every time a patch is executed from 0.15 to 0.7
|
||||||
|
val totalPatchesCount = patches.size
|
||||||
|
val progressStep = 0.55 / totalPatchesCount
|
||||||
|
var progress = 0.15
|
||||||
|
|
||||||
|
patcher.apply {
|
||||||
|
acceptIntegrations(listOf(integrations))
|
||||||
|
acceptPatches(patches)
|
||||||
|
|
||||||
|
runBlocking {
|
||||||
|
apply(false).collect(FlowCollector { patchResult: PatchResult ->
|
||||||
|
if (cancel) {
|
||||||
|
handler.post { stopResult!!.success(null) }
|
||||||
|
this.cancel()
|
||||||
|
this@apply.close()
|
||||||
|
return@FlowCollector
|
||||||
|
}
|
||||||
|
|
||||||
|
val msg = patchResult.exception?.let {
|
||||||
|
val writer = StringWriter()
|
||||||
|
it.printStackTrace(PrintWriter(writer))
|
||||||
|
"${patchResult.patch.name} failed: $writer"
|
||||||
|
} ?: run {
|
||||||
|
"${patchResult.patch.name} succeeded"
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProgress(progress, "", msg)
|
||||||
|
progress += progressStep
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cancel) {
|
||||||
|
postStop()
|
||||||
|
patcher.close()
|
||||||
|
return@Thread
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProgress(0.75, "Building...", "")
|
||||||
|
|
||||||
|
val res = patcher.get()
|
||||||
|
patcher.close()
|
||||||
|
|
||||||
ZipFile(patchedFile).use { file ->
|
ZipFile(patchedFile).use { file ->
|
||||||
res.dexFiles.forEach {
|
res.dexFiles.forEach {
|
||||||
|
if (cancel) {
|
||||||
|
postStop()
|
||||||
|
return@Thread
|
||||||
|
}
|
||||||
file.addEntryCompressData(
|
file.addEntryCompressData(
|
||||||
ZipEntry.createWithName(it.name),
|
ZipEntry.createWithName(it.name),
|
||||||
it.stream.readBytes()
|
it.stream.readBytes()
|
||||||
@@ -231,81 +393,75 @@ class MainActivity : FlutterActivity() {
|
|||||||
ZipAligner::getEntryAlignment
|
ZipAligner::getEntryAlignment
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
handler.post {
|
|
||||||
installerChannel.invokeMethod(
|
|
||||||
"update",
|
|
||||||
mapOf(
|
|
||||||
"progress" to 0.9,
|
|
||||||
"header" to "Signing apk...",
|
|
||||||
"log" to ""
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Signer("ReVanced", "s3cur3p@ssw0rd").signApk(patchedFile, outFile, keyStoreFile)
|
|
||||||
|
|
||||||
handler.post {
|
if (cancel) {
|
||||||
installerChannel.invokeMethod(
|
postStop()
|
||||||
"update",
|
return@Thread
|
||||||
mapOf(
|
|
||||||
"progress" to 1.0,
|
|
||||||
"header" to "Finished!",
|
|
||||||
"log" to "Finished!"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateProgress(0.8, "Signing...", "Signing APK")
|
||||||
|
|
||||||
|
try {
|
||||||
|
Signer("ReVanced", keystorePassword)
|
||||||
|
.signApk(patchedFile, outFile, keyStoreFile)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
print("Error signing APK: ${e.message}")
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProgress(.85, "Patched", "Patched APK")
|
||||||
} catch (ex: Throwable) {
|
} catch (ex: Throwable) {
|
||||||
val stack = ex.stackTraceToString()
|
if (!cancel) {
|
||||||
handler.post {
|
val stack = ex.stackTraceToString()
|
||||||
installerChannel.invokeMethod(
|
updateProgress(
|
||||||
"update",
|
-100.0,
|
||||||
mapOf(
|
"Failed",
|
||||||
"progress" to -100.0,
|
"An error occurred:\n$stack"
|
||||||
"header" to "Aborting...",
|
|
||||||
"log" to "An error occurred! Aborting\nError:\n$stack"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handler.post { result.success(null) }
|
handler.post { result.success(null) }
|
||||||
}.start()
|
}.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class ManagerLogger : Logger {
|
private fun installApk(apkPath: String) {
|
||||||
override fun error(msg: String) {
|
val packageInstaller: PackageInstaller = applicationContext.packageManager.packageInstaller
|
||||||
handler.post {
|
val sessionParams = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
|
||||||
installerChannel
|
val sessionId: Int = packageInstaller.createSession(sessionParams)
|
||||||
.invokeMethod(
|
val session: PackageInstaller.Session = packageInstaller.openSession(sessionId)
|
||||||
"update",
|
session.use { activeSession ->
|
||||||
mapOf("progress" to -1.0, "header" to "", "log" to msg)
|
val sessionOutputStream = activeSession.openWrite(applicationContext.packageName, 0, -1)
|
||||||
)
|
sessionOutputStream.use { outputStream ->
|
||||||
|
val apkFile = File(apkPath)
|
||||||
|
apkFile.inputStream().use { inputStream ->
|
||||||
|
inputStream.copyTo(outputStream)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val receiverIntent = Intent(applicationContext, InstallerReceiver::class.java).apply {
|
||||||
override fun warn(msg: String) {
|
action = "APP_INSTALL_ACTION"
|
||||||
handler.post {
|
|
||||||
installerChannel.invokeMethod(
|
|
||||||
"update",
|
|
||||||
mapOf("progress" to -1.0, "header" to "", "log" to msg)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
val receiverPendingIntent = PendingIntent.getBroadcast(context, sessionId, receiverIntent, PackageInstallerManager.flags)
|
||||||
|
session.commit(receiverPendingIntent.intentSender)
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
|
|
||||||
override fun info(msg: String) {
|
private fun uninstallApp(packageName: String) {
|
||||||
handler.post {
|
val packageInstaller: PackageInstaller = applicationContext.packageManager.packageInstaller
|
||||||
installerChannel.invokeMethod(
|
val receiverIntent = Intent(applicationContext, UninstallerReceiver::class.java).apply {
|
||||||
"update",
|
action = "APP_UNINSTALL_ACTION"
|
||||||
mapOf("progress" to -1.0, "header" to "", "log" to msg)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
val receiverPendingIntent = PendingIntent.getBroadcast(context, 0, receiverIntent, PackageInstallerManager.flags)
|
||||||
|
packageInstaller.uninstall(packageName, receiverPendingIntent.intentSender)
|
||||||
|
}
|
||||||
|
|
||||||
override fun trace(msg: String) {
|
object PackageInstallerManager {
|
||||||
handler.post {
|
var result: MethodChannel.Result? = null
|
||||||
installerChannel.invokeMethod(
|
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
"update",
|
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
mapOf("progress" to -1.0, "header" to "", "log" to msg)
|
} else {
|
||||||
)
|
PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package app.revanced.manager.flutter.utils.packageInstaller
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageInstaller
|
||||||
|
import app.revanced.manager.flutter.MainActivity
|
||||||
|
|
||||||
|
class InstallerReceiver : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
when (val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)) {
|
||||||
|
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
||||||
|
val confirmationIntent = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
|
||||||
|
if (confirmationIntent != null) {
|
||||||
|
context.startActivity(confirmationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
val packageName = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)
|
||||||
|
val message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
|
||||||
|
val otherPackageName = intent.getStringExtra(PackageInstaller.EXTRA_OTHER_PACKAGE_NAME)
|
||||||
|
MainActivity.PackageInstallerManager.result!!.success(mapOf(
|
||||||
|
"status" to status,
|
||||||
|
"packageName" to packageName,
|
||||||
|
"message" to message,
|
||||||
|
"otherPackageName" to otherPackageName
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package app.revanced.manager.flutter.utils.packageInstaller
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageInstaller
|
||||||
|
import app.revanced.manager.flutter.MainActivity
|
||||||
|
|
||||||
|
class UninstallerReceiver : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
when (val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)) {
|
||||||
|
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
||||||
|
val confirmationIntent = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
|
||||||
|
if (confirmationIntent != null) {
|
||||||
|
context.startActivity(confirmationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
MainActivity.PackageInstallerManager.result!!.success(status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 814 B |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |
30
android/app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="256"
|
||||||
|
android:viewportHeight="256">
|
||||||
|
<group android:scaleX="0.23"
|
||||||
|
android:scaleY="0.23"
|
||||||
|
android:translateX="98.56"
|
||||||
|
android:translateY="98.56">
|
||||||
|
<path
|
||||||
|
android:pathData="M253.85,4.9C254.32,3.82 254.22,2.57 253.58,1.58C252.93,0.6 251.83,0 250.64,0C243.29,0 230.47,0 225.95,0C224.96,0 224.06,0.59 223.66,1.5C216.03,18.88 144.1,182.7 130.29,214.16C129.89,215.07 128.99,215.66 128,215.66C127.01,215.66 126.11,215.07 125.71,214.16C111.9,182.7 39.97,18.88 32.34,1.5C31.94,0.59 31.04,0 30.05,0C25.53,0 12.71,0 5.36,0C4.17,0 3.07,0.6 2.42,1.58C1.78,2.57 1.68,3.82 2.15,4.9C16.78,38.3 101.47,231.61 111.24,253.9C111.8,255.18 113.06,256 114.45,256C120.29,256 135.71,256 141.55,256C142.94,256 144.2,255.18 144.76,253.9C154.52,231.61 239.22,38.3 253.85,4.9Z"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M130.59,131.75C130.06,132.68 129.07,133.25 128,133.25C126.93,133.25 125.94,132.68 125.4,131.75C113.45,111.06 63.88,25.19 51.93,4.5C51.4,3.57 51.4,2.43 51.93,1.5C52.47,0.57 53.46,-0 54.53,-0L201.47,-0C202.54,-0 203.53,0.57 204.06,1.5C204.6,2.43 204.6,3.57 204.06,4.5C192.12,25.19 142.54,111.06 130.59,131.75Z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startX="128"
|
||||||
|
android:startY="-0"
|
||||||
|
android:endX="128"
|
||||||
|
android:endY="254.6"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0" android:color="#FFF04E98"/>
|
||||||
|
<item android:offset="0.5" android:color="#FF5F65D4"/>
|
||||||
|
<item android:offset="1" android:color="#FF4E98F0"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 5.3 KiB |
@@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
</adaptive-icon>
|
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 9.4 KiB |
@@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<color name="ic_launcher_background">#1B1B1B</color>
|
<color name="ic_launcher_background">#1B1B1B</color>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
package="app.revanced.manager.flutter">
|
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
buildscript {
|
buildscript {
|
||||||
ext.cronetVersion = '102.5005.125'
|
ext.kotlin_version = '1.9.10'
|
||||||
ext.kotlin_version = '1.7.10'
|
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:7.1.3'
|
classpath 'com.android.tools.build:gradle:8.1.2'
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,21 +16,20 @@ allprojects {
|
|||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
maven {
|
maven {
|
||||||
url = uri("https://maven.pkg.github.com/revanced/revanced-patcher")
|
url 'https://jitpack.io'
|
||||||
credentials {
|
|
||||||
username = (project.findProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR")) as String
|
|
||||||
password = (project.findProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN")) as String
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
mavenLocal()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rootProject.buildDir = '../build'
|
rootProject.buildDir = '../build'
|
||||||
subprojects {
|
subprojects {
|
||||||
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||||
|
}
|
||||||
|
subprojects {
|
||||||
project.evaluationDependsOn(':app')
|
project.evaluationDependsOn(':app')
|
||||||
}
|
}
|
||||||
|
|
||||||
task clean(type: Delete) {
|
tasks.register("clean", Delete) {
|
||||||
delete rootProject.buildDir
|
delete rootProject.buildDir
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536M
|
org.gradle.jvmargs=-Xmx1536M -XX:+UseParallelGC
|
||||||
|
org.gradle.parallel=true
|
||||||
|
org.gradle.daemon=true
|
||||||
|
org.gradle.caching=true
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
|
android.defaults.buildfeatures.buildconfig=true
|
||||||
|
android.nonTransitiveRClass=false
|
||||||
|
android.nonFinalResIds=false
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
#Mon May 09 12:07:41 MSK 2022
|
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-rc-1-bin.zip
|
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStorePath=wrapper/dists
|
distributionSha256Sum=9d926787066a081739e8200858338b4a69e837c3a821a33aca9db09dd4a41026
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
include ':app'
|
pluginManagement {
|
||||||
|
def flutterSdkPath = {
|
||||||
|
def properties = new Properties()
|
||||||
|
file("local.properties").withInputStream { properties.load(it) }
|
||||||
|
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||||
|
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||||
|
return flutterSdkPath
|
||||||
|
}
|
||||||
|
settings.ext.flutterSdkPath = flutterSdkPath()
|
||||||
|
|
||||||
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
|
includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
|
||||||
def properties = new Properties()
|
|
||||||
|
|
||||||
assert localPropertiesFile.exists()
|
plugins {
|
||||||
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
|
id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
include ":app"
|
||||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
|
||||||
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
|
apply from: "${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle/app_plugin_loader.gradle"
|
||||||
|
|||||||
@@ -1,37 +1,65 @@
|
|||||||
{
|
{
|
||||||
"okButton": "OK",
|
"okButton": "OK",
|
||||||
"cancelButton": "Cancel",
|
"cancelButton": "Cancel",
|
||||||
|
"dismissButton": "Dismiss",
|
||||||
|
"quitButton": "Quit",
|
||||||
|
"updateButton": "Update",
|
||||||
"enabledLabel": "Enabled",
|
"enabledLabel": "Enabled",
|
||||||
"disabledLabel": "Disabled",
|
"disabledLabel": "Disabled",
|
||||||
|
"installed":"Installed: {version}",
|
||||||
|
"suggested":"Suggested: {version}",
|
||||||
"yesButton": "Yes",
|
"yesButton": "Yes",
|
||||||
"noButton": "No",
|
"noButton": "No",
|
||||||
|
"warning": "Warning",
|
||||||
|
"options": "Options",
|
||||||
|
"notice": "Notice",
|
||||||
|
"noShowAgain": "Don't show this again",
|
||||||
|
"add": "Add",
|
||||||
|
"remove": "Remove",
|
||||||
"navigationView": {
|
"navigationView": {
|
||||||
"dashboardTab": "Dashboard",
|
"dashboardTab": "Dashboard",
|
||||||
"patcherTab": "Patcher",
|
"patcherTab": "Patcher",
|
||||||
"settingsTab": "Settings"
|
"settingsTab": "Settings"
|
||||||
},
|
},
|
||||||
"homeView": {
|
"homeView": {
|
||||||
|
"refreshSuccess": "Refreshed successfully",
|
||||||
"widgetTitle": "Dashboard",
|
"widgetTitle": "Dashboard",
|
||||||
|
|
||||||
"updatesSubtitle": "Updates",
|
"updatesSubtitle": "Updates",
|
||||||
"patchedSubtitle": "Patched Applications",
|
"patchedSubtitle": "Patched apps",
|
||||||
"updatesAvailable": "Updates available",
|
|
||||||
"noUpdates": "No updates available",
|
"noUpdates": "No updates available",
|
||||||
"noInstallations": "No patched applications installed",
|
|
||||||
"installed": "Installed",
|
"WIP": "Work in progress...",
|
||||||
|
|
||||||
|
"noInstallations": "No patched apps installed",
|
||||||
|
"installUpdate": "Continue to install the update?",
|
||||||
|
|
||||||
"updateDialogTitle": "Update Manager",
|
"updateDialogTitle": "Update Manager",
|
||||||
"updateDialogText": "Are you sure you want to download and update ReVanced Manager?",
|
"updatePatchesDialogTitle": "Update ReVanced Patches",
|
||||||
"notificationTitle": "ReVanced Manager was updated",
|
"updateChangelogTitle": "Changelog",
|
||||||
"notificationText": "Tap to open the app",
|
|
||||||
|
"patchesConsentDialogText": "ReVanced Patches needs to be downloaded.",
|
||||||
|
"patchesConsentDialogText2": "This will connect you to {url}.",
|
||||||
|
"patchesConsentDialogText3": "Auto update?",
|
||||||
|
"patchesConsentDialogText3Sub": "You can change this in settings at a later time.",
|
||||||
|
|
||||||
|
"notificationTitle": "Update downloaded",
|
||||||
|
"notificationText": "Tap to install the update",
|
||||||
|
|
||||||
"downloadingMessage": "Downloading update...",
|
"downloadingMessage": "Downloading update...",
|
||||||
|
"downloadedMessage": "Update downloaded!",
|
||||||
|
|
||||||
"installingMessage": "Installing update...",
|
"installingMessage": "Installing update...",
|
||||||
|
|
||||||
"errorDownloadMessage": "Unable to download update",
|
"errorDownloadMessage": "Unable to download update",
|
||||||
"errorInstallMessage": "Unable to install update",
|
"errorInstallMessage": "Unable to install update",
|
||||||
"noConnection": "No internet connection"
|
|
||||||
|
"noConnection": "No internet connection",
|
||||||
|
"updatesDisabled": "Updating a patched app is currently disabled. Repatch the app again."
|
||||||
},
|
},
|
||||||
"applicationItem": {
|
"applicationItem": {
|
||||||
"patchButton": "Patch",
|
"infoButton": "Info"
|
||||||
"infoButton": "Info",
|
|
||||||
"changelogLabel": "Changelog"
|
|
||||||
},
|
},
|
||||||
"latestCommitCard": {
|
"latestCommitCard": {
|
||||||
"loadingLabel": "Loading...",
|
"loadingLabel": "Loading...",
|
||||||
@@ -43,21 +71,27 @@
|
|||||||
"patcherView": {
|
"patcherView": {
|
||||||
"widgetTitle": "Patcher",
|
"widgetTitle": "Patcher",
|
||||||
"patchButton": "Patch",
|
"patchButton": "Patch",
|
||||||
"patchDialogTitle": "Warning",
|
|
||||||
"patchDialogText": "You have selected a resource patch and a split APK installation was detected so patching errors may occur.\nAre you sure you want to proceed with patching a split base APK?"
|
"armv7WarningDialogText": "Patching on ARMv7 devices is not yet supported and might fail. Continue anyways?",
|
||||||
|
|
||||||
|
"removedPatchesWarningDialogText": "The following patches have been removed since the last time you used them.\n\n{patches}\n\nContinue anyways?",
|
||||||
|
"requiredOptionDialogText" : "Some patch options have to be set."
|
||||||
},
|
},
|
||||||
"appSelectorCard": {
|
"appSelectorCard": {
|
||||||
"widgetTitle": "Select application",
|
"widgetTitle": "Select an app",
|
||||||
"widgetTitleSelected": "Selected application",
|
"widgetTitleSelected": "Selected app",
|
||||||
"widgetSubtitle": "No application selected",
|
"widgetSubtitle": "No app selected",
|
||||||
"noAppsLabel": "No applications found.",
|
|
||||||
|
"noAppsLabel": "No applications found",
|
||||||
|
|
||||||
"currentVersion": "Current",
|
"currentVersion": "Current",
|
||||||
"recommendedVersion": "Recommended",
|
"suggestedVersion": "Suggested",
|
||||||
"anyVersion": "any"
|
"anyVersion": "Any version"
|
||||||
},
|
},
|
||||||
"patchSelectorCard": {
|
"patchSelectorCard": {
|
||||||
"widgetTitle": "Select patches",
|
"widgetTitle": "Select patches",
|
||||||
"widgetTitleSelected": "Selected patches",
|
"widgetTitleSelected": "Selected patches",
|
||||||
|
|
||||||
"widgetSubtitle": "Select an application first",
|
"widgetSubtitle": "Select an application first",
|
||||||
"widgetEmptySubtitle": "No patches selected"
|
"widgetEmptySubtitle": "No patches selected"
|
||||||
},
|
},
|
||||||
@@ -66,98 +100,264 @@
|
|||||||
"widgetSubtitle": "We are online!"
|
"widgetSubtitle": "We are online!"
|
||||||
},
|
},
|
||||||
"appSelectorView": {
|
"appSelectorView": {
|
||||||
"viewTitle": "Select application",
|
"viewTitle": "Select an app",
|
||||||
"searchBarHint": "Search applications",
|
"searchBarHint": "Search app",
|
||||||
|
|
||||||
"storageButton": "Storage",
|
"storageButton": "Storage",
|
||||||
"errorMessage": "Unable to use selected application."
|
"selectFromStorageButton": "Select from storage",
|
||||||
|
|
||||||
|
"errorMessage": "Unable to use selected application",
|
||||||
|
|
||||||
|
"downloadToast": "Download function is not available yet",
|
||||||
|
|
||||||
|
"requireSuggestedAppVersionDialogText": "The version of the app you have selected does not match the suggested version which can lead to unexpected issues. Please use the suggested version.\n\nSelected version: {selected}\nSuggested version: {suggested}\n\nTo continue anyway, disable \"Require suggested app version\" in the settings.",
|
||||||
|
|
||||||
|
"featureNotAvailable": "Feature not implemented",
|
||||||
|
"featureNotAvailableText": "This app is a split APK and can only be patched and installed reliably by mounting with root permissions. However, you can patch and install a full APK by selecting it from storage."
|
||||||
},
|
},
|
||||||
"patchesSelectorView": {
|
"patchesSelectorView": {
|
||||||
"viewTitle": "Select patches",
|
"viewTitle": "Select patches",
|
||||||
"searchBarHint": "Search patches",
|
"searchBarHint": "Search patches",
|
||||||
|
"universalPatches": "Universal patches",
|
||||||
|
"newPatches": "New patches",
|
||||||
|
"patches": "Patches",
|
||||||
|
|
||||||
"doneButton": "Done",
|
"doneButton": "Done",
|
||||||
|
|
||||||
|
"default": "Default",
|
||||||
|
"defaultTooltip": "Select all default patches",
|
||||||
|
|
||||||
|
"none": "None",
|
||||||
|
"noneTooltip": "Deselect all patches",
|
||||||
|
|
||||||
|
"loadPatchesSelection": "Load patch selection",
|
||||||
|
"noSavedPatches": "No saved patch selection for the selected app.\nPress Done to save the current selection.",
|
||||||
"noPatchesFound": "No patches found for the selected app",
|
"noPatchesFound": "No patches found for the selected app",
|
||||||
"selectAllPatchesWarningTitle": "Warning",
|
"setRequiredOption": "Some patches require options to be set:\n\n{patches}\n\nPlease set them before continuing."
|
||||||
"selectAllPatchesWarningContent": "You are about to select all patches, that includes unrecommended patches and can cause unwanted behavior."
|
},
|
||||||
|
"patchOptionsView": {
|
||||||
|
"customValue": "Custom value",
|
||||||
|
"resetOptionsTooltip": "Reset patch options",
|
||||||
|
"viewTitle": "Patch options",
|
||||||
|
"saveOptions": "Save",
|
||||||
|
|
||||||
|
"addOptions": "Add options",
|
||||||
|
"deselectPatch": "Deselect patch",
|
||||||
|
"tooltip": "More input options",
|
||||||
|
"selectFilePath": "Select file path",
|
||||||
|
"selectFolder": "Select folder",
|
||||||
|
"selectOption": "Select option",
|
||||||
|
|
||||||
|
"requiredOption": "This option is required",
|
||||||
|
"unsupportedOption": "This option is not supported",
|
||||||
|
"requiredOptionNull": "The following options have to be set:\n\n{options}"
|
||||||
},
|
},
|
||||||
"patchItem": {
|
"patchItem": {
|
||||||
"unsupportedWarningButton": "Warning",
|
"unsupportedDialogText": "Selecting this patch may result in patching errors.\n\nApp version: {packageVersion}\nSupported versions:\n{supportedVersions}",
|
||||||
"unsupportedDialogTitle": "Warning",
|
"unsupportedPatchVersion": "Patch is not supported for this app version.",
|
||||||
"unsupportedDialogText": "Selecting this patch may result in patching errors.\n\nApp version: {packageVersion}\nCurrent supported versions:\n{supportedVersions}"
|
"unsupportedRequiredOption": "This patch contains a required option that is not supported by this app",
|
||||||
|
|
||||||
|
"patchesChangeWarningDialogText": "It is recommended to use the default patch selection and options. Changing them may result in unexpected issues.\n\nYou'll need to turn on \"Allow changing patch selection\" in settings before changing any patch selection.",
|
||||||
|
"patchesChangeWarningDialogButton": "Use default selection"
|
||||||
},
|
},
|
||||||
"installerView": {
|
"installerView": {
|
||||||
"widgetTitle": "Installer",
|
"widgetTitle": "Installer",
|
||||||
|
"installType": "Select install type",
|
||||||
|
"installTypeDescription": "Select the installation type to continue with.",
|
||||||
|
|
||||||
"installButton": "Install",
|
"installButton": "Install",
|
||||||
"installRootButton": "Install as Root",
|
"installRootType": "Mount",
|
||||||
|
"installNonRootType": "Regular",
|
||||||
|
|
||||||
|
"warning": "Disable auto updates for the patched app to avoid unexpected issues.",
|
||||||
|
|
||||||
|
"pressBackAgain": "Press back again to cancel",
|
||||||
"openButton": "Open",
|
"openButton": "Open",
|
||||||
"shareButton": "Share file",
|
"shareButton": "Share file",
|
||||||
|
|
||||||
"notificationTitle": "ReVanced Manager is patching",
|
"notificationTitle": "ReVanced Manager is patching",
|
||||||
"notificationText": "Tap to return to the installer",
|
"notificationText": "Tap to return to the installer",
|
||||||
"shareApkMenuOption": "Share APK",
|
|
||||||
"shareLogMenuOption": "Share log",
|
"exportApkButtonTooltip": "Export patched APK",
|
||||||
"installErrorDialogTitle": "Error",
|
"exportLogButtonTooltip": "Export log",
|
||||||
"installErrorDialogText1": "Root install is not possible with the current patches selection.\nRepatch your app or choose non-root install.",
|
|
||||||
"installErrorDialogText2": "Non-root install is not possible with the current patches selection.\nRepatch your app or choose root install if you have your device rooted.",
|
"screenshotDetected": "A screenshot has been detected. If you are trying to share the log, please share a text copy instead.\n\nCopy log to clipboard?",
|
||||||
"installErrorDialogText3": "Root install is not possible as the original APK was selected from storage.\nSelect an installed app or choose non-root install.",
|
"copiedToClipboard": "Copied log to clipboard",
|
||||||
"noExit": "Installer is still running..."
|
|
||||||
|
"noExit": "Installer is still running, cannot exit..."
|
||||||
},
|
},
|
||||||
"settingsView": {
|
"settingsView": {
|
||||||
"widgetTitle": "Settings",
|
"widgetTitle": "Settings",
|
||||||
|
|
||||||
"appearanceSectionTitle": "Appearance",
|
"appearanceSectionTitle": "Appearance",
|
||||||
"teamSectionTitle": "Team",
|
"teamSectionTitle": "Team",
|
||||||
"infoSectionTitle": "Info",
|
"debugSectionTitle": "Debugging",
|
||||||
"advancedSectionTitle": "Advanced",
|
"advancedSectionTitle": "Advanced",
|
||||||
"darkThemeLabel": "Dark Mode",
|
"exportSectionTitle": "Import & export",
|
||||||
"darkThemeHint": "Welcome to the Dark Side",
|
|
||||||
|
"themeModeLabel": "App theme",
|
||||||
|
"systemThemeLabel": "System",
|
||||||
|
"lightThemeLabel": "Light",
|
||||||
|
"darkThemeLabel": "Dark",
|
||||||
|
|
||||||
"dynamicThemeLabel": "Material You",
|
"dynamicThemeLabel": "Material You",
|
||||||
"dynamicThemeHint": "Enjoy an experience closer to your device",
|
"dynamicThemeHint": "Enjoy an experience closer to your device",
|
||||||
|
|
||||||
"languageLabel": "Language",
|
"languageLabel": "Language",
|
||||||
"englishOption": "English",
|
"englishOption": "English",
|
||||||
"frenchOption": "French",
|
|
||||||
"sourcesLabel": "Sources",
|
"sourcesLabel": "Sources",
|
||||||
"sourcesLabelHint": "Configure your custom sources",
|
"sourcesLabelHint": "Configure the source of patches and integrations",
|
||||||
"orgPatchesLabel": "Patches Organization",
|
"sourcesIntegrationsLabel": "Integrations source",
|
||||||
"sourcesPatchesLabel": "Patches Source",
|
|
||||||
"orgIntegrationsLabel": "Integrations Organization",
|
|
||||||
"sourcesIntegrationsLabel": "Integrations Source",
|
|
||||||
"sourcesResetDialogTitle": "Reset",
|
"sourcesResetDialogTitle": "Reset",
|
||||||
"sourcesResetDialogText": "Are you sure you want to reset custom sources to their default values?",
|
"sourcesResetDialogText": "Are you sure you want to reset your sources to their default values?",
|
||||||
"apiURLResetDialogText": "Are you sure you want to reset API URL to its default value?",
|
"apiURLResetDialogText": "Are you sure you want to reset your API URL to its default value?",
|
||||||
|
"sourcesUpdateNote": "Note: Patches will be updated to the latest version automatically.\n\nThis will reveal your IP address to the server.",
|
||||||
|
|
||||||
|
"apiURLLabel": "API URL",
|
||||||
|
"apiURLHint": "Configure the URL of the API to use",
|
||||||
|
"selectApiURL": "API URL",
|
||||||
|
"hostRepositoryLabel": "Repository API",
|
||||||
|
"orgPatchesLabel": "Patches organization",
|
||||||
|
"sourcesPatchesLabel": "Patches source",
|
||||||
|
"orgIntegrationsLabel": "Integrations organization",
|
||||||
|
|
||||||
"contributorsLabel": "Contributors",
|
"contributorsLabel": "Contributors",
|
||||||
"contributorsHint": "A list of contributors of ReVanced",
|
"contributorsHint": "A list of contributors of ReVanced",
|
||||||
"logsLabel": "Logs",
|
|
||||||
"logsHint": "Share device debug logs",
|
"logsLabel": "Share logs",
|
||||||
"apiURLLabel": "API URL",
|
"logsHint": "Share ReVanced Manager logs",
|
||||||
"apiURLHint": "Configure your custom API URL",
|
|
||||||
"selectApiURL": "Select URL",
|
"enablePatchesSelectionLabel": "Allow changing patch selection",
|
||||||
|
"enablePatchesSelectionHint": "Do not prevent selecting or deselecting patches",
|
||||||
|
"enablePatchesSelectionWarningText": "Changing the selection of patches may cause unexpected issues.\n\nEnable anyways?",
|
||||||
|
"disablePatchesSelectionWarningText": "You are about to disable changing the selection of patches.\nThe default selection of patches will be restored.\n\nDisable anyways?",
|
||||||
|
|
||||||
|
"autoUpdatePatchesLabel": "Auto update patches",
|
||||||
|
"autoUpdatePatchesHint": "Automatically update patches to the latest version",
|
||||||
|
"universalPatchesLabel": "Show universal patches",
|
||||||
|
"universalPatchesHint": "Display all apps and universal patches (may slow down the app list)",
|
||||||
|
|
||||||
|
"versionCompatibilityCheckLabel": "Version compatibility check",
|
||||||
|
"versionCompatibilityCheckHint": "Prevent selecting patches that are not compatible with the selected app version",
|
||||||
|
|
||||||
|
"requireSuggestedAppVersionLabel": "Require suggested app version",
|
||||||
|
"requireSuggestedAppVersionHint": "Prevent selecting an app with a version that is not the suggested",
|
||||||
|
"requireSuggestedAppVersionDialogText": "Selecting an app that is not the suggested version may cause unexpected issues.\n\nDo you want to proceed anyways?",
|
||||||
|
|
||||||
"aboutLabel": "About",
|
"aboutLabel": "About",
|
||||||
"snackbarMessage": "Copied to clipboard"
|
"snackbarMessage": "Copied to clipboard",
|
||||||
|
"restartAppForChanges": "Restart the app to apply changes",
|
||||||
|
|
||||||
|
"deleteTempDirLabel": "Delete temporary files",
|
||||||
|
"deleteTempDirHint": "Delete unused temporary files",
|
||||||
|
"deletedTempDir": "Temporary files deleted",
|
||||||
|
|
||||||
|
"exportPatchesLabel": "Export patch selection",
|
||||||
|
"exportPatchesHint": "Export patch selection to a JSON file",
|
||||||
|
"exportedPatches": "Patch selection exported",
|
||||||
|
"noExportFileFound": "No patch selection to export",
|
||||||
|
|
||||||
|
"importPatchesLabel": "Import patch selection",
|
||||||
|
"importPatchesHint": "Import patch selection from a JSON file",
|
||||||
|
"importedPatches": "Patch selection imported",
|
||||||
|
|
||||||
|
"resetStoredPatchesLabel": "Reset patch selection",
|
||||||
|
"resetStoredPatchesHint": "Reset the stored patch selection",
|
||||||
|
"resetStoredPatchesDialogTitle": "Reset patch selection?",
|
||||||
|
"resetStoredPatchesDialogText": "The default selection of patches will be restored.",
|
||||||
|
"resetStoredPatches": "Patch selection has been reset",
|
||||||
|
|
||||||
|
"resetStoredOptionsLabel": "Reset patch options",
|
||||||
|
"resetStoredOptionsHint": "Reset all patch options",
|
||||||
|
"resetStoredOptionsDialogTitle": "Reset patch options?",
|
||||||
|
"resetStoredOptionsDialogText": "Resetting patch options will remove all saved options.",
|
||||||
|
"resetStoredOptions": "Options have been reset",
|
||||||
|
|
||||||
|
"deleteLogsLabel": "Clear logs",
|
||||||
|
"deleteLogsHint": "Delete collected ReVanced Manager logs",
|
||||||
|
"deletedLogs": "Logs deleted",
|
||||||
|
|
||||||
|
"regenerateKeystoreLabel": "Regenerate keystore",
|
||||||
|
"regenerateKeystoreHint": "Regenerate the keystore used to sign apps",
|
||||||
|
|
||||||
|
"regenerateKeystoreDialogTitle": "Regenerate keystore?",
|
||||||
|
"regenerateKeystoreDialogText": "Patched apps signed with the old keystore will no longer be able to be updated.",
|
||||||
|
"regeneratedKeystore": "Keystore regenerated",
|
||||||
|
|
||||||
|
"exportKeystoreLabel": "Export keystore",
|
||||||
|
"exportKeystoreHint": "Export the keystore used to sign apps",
|
||||||
|
"exportedKeystore": "Keystore exported",
|
||||||
|
"noKeystoreExportFileFound": "No keystore to export",
|
||||||
|
|
||||||
|
"importKeystoreLabel": "Import keystore",
|
||||||
|
"importKeystoreHint": "Import a keystore used to sign apps",
|
||||||
|
"importedKeystore": "Keystore imported",
|
||||||
|
|
||||||
|
"selectKeystorePassword": "Keystore password",
|
||||||
|
"selectKeystorePasswordHint": "Select keystore password used to sign apps",
|
||||||
|
|
||||||
|
"jsonSelectorErrorMessage": "Unable to use selected JSON file",
|
||||||
|
"keystoreSelectorErrorMessage": "Unable to use selected keystore file"
|
||||||
},
|
},
|
||||||
"appInfoView": {
|
"appInfoView": {
|
||||||
"widgetTitle": "App Info",
|
"widgetTitle": "App info",
|
||||||
"openButton": "Open",
|
"openButton": "Open",
|
||||||
"uninstallButton": "Uninstall",
|
"uninstallButton": "Uninstall",
|
||||||
"patchButton": "Patch",
|
"unmountButton": "Unmount",
|
||||||
"unpatchButton": "Unpatch",
|
|
||||||
"unpatchDialogText": "Are you sure you want to unpatch this app?",
|
|
||||||
"rootDialogTitle": "Error",
|
"rootDialogTitle": "Error",
|
||||||
"rootDialogText": "App was installed with root mode enabled but currently root mode is disabled.\nPlease enable root mode first.",
|
|
||||||
"packageNameLabel": "Package Name",
|
"unmountDialogText": "Are you sure you want to unmount this app?",
|
||||||
"originalPackageNameLabel": "Original Package Name",
|
"uninstallDialogText": "Are you sure you want to uninstall this app?",
|
||||||
"installTypeLabel": "Installation Type",
|
"rootDialogText": "App was installed with superuser permissions, but currently ReVanced Manager has no permissions.\nPlease grant superuser permissions first.",
|
||||||
"rootTypeLabel": "Root",
|
|
||||||
"nonRootTypeLabel": "Non-root",
|
"packageNameLabel": "Package name",
|
||||||
"patchedDateLabel": "Patched Date",
|
"installTypeLabel": "Installation type",
|
||||||
|
"mountTypeLabel": "Mount",
|
||||||
|
"regularTypeLabel": "Regular",
|
||||||
|
"patchedDateLabel": "Patched date",
|
||||||
|
"appliedPatchesLabel": "Applied patches",
|
||||||
|
|
||||||
"patchedDateHint": "{date} at {time}",
|
"patchedDateHint": "{date} at {time}",
|
||||||
"appliedPatchesLabel": "Applied Patches",
|
|
||||||
"appliedPatchesHint": "{quantity} applied patches",
|
"appliedPatchesHint": "{quantity} applied patches",
|
||||||
"updateNotImplemented": "Update functionality not implemented yet"
|
|
||||||
|
"updateNotImplemented": "This feature has not been implemented yet"
|
||||||
},
|
},
|
||||||
"contributorsView": {
|
"contributorsView": {
|
||||||
"widgetTitle": "Contributors",
|
"widgetTitle": "Contributors",
|
||||||
"patcherContributors": "Patcher Contributors",
|
"patcherContributors": "Patcher contributors",
|
||||||
"patchesContributors": "Patches Contributors",
|
"patchesContributors": "Patches contributors",
|
||||||
"integrationsContributors": "Integrations Contributors",
|
"integrationsContributors": "Integrations contributors",
|
||||||
"cliContributors": "CLI Contributors",
|
"cliContributors": "CLI contributors",
|
||||||
"managerContributors": "Manager Contributors"
|
"managerContributors": "Manager contributors"
|
||||||
|
},
|
||||||
|
"installErrorDialog": {
|
||||||
|
"mount_version_mismatch": "Version mismatch",
|
||||||
|
"mount_no_root": "No root access",
|
||||||
|
"mount_missing_installation": "Installation not found",
|
||||||
|
|
||||||
|
"status_failure_blocked": "Installation blocked",
|
||||||
|
"install_failed_verification_failure": "Verification failed",
|
||||||
|
"status_failure_invalid": "Installation invalid",
|
||||||
|
"install_failed_version_downgrade": "Can't downgrade",
|
||||||
|
"status_failure_conflict": "Installation conflict",
|
||||||
|
"status_failure_storage": "Installation storage issue",
|
||||||
|
"status_failure_incompatible": "Installation incompatible",
|
||||||
|
"status_failure_timeout": "Installation timeout",
|
||||||
|
"status_unknown": "Installation failed",
|
||||||
|
|
||||||
|
"mount_version_mismatch_description": "The installation failed due to the installed app being a different version than the patched app.\n\nInstall the version of the app you are mounting and try again.",
|
||||||
|
"mount_no_root_description": "The installation failed due to root access not being granted.\n\nGrant root access to ReVanced Manager and try again.",
|
||||||
|
"mount_missing_installation_description": "The installation failed due to the unpatched app not being installed on this device in order to mount over it.\n\nInstall the unpatched app before mounting and try again.",
|
||||||
|
|
||||||
|
"status_failure_timeout_description": "The installation took too long to finish.\n\nWould you like to try again?",
|
||||||
|
"status_failure_storage_description": "The installation failed due to insufficient storage.\n\nFree up some space and try again.",
|
||||||
|
"status_failure_invalid_description": "The installation failed due to the patched app being invalid.\n\nUninstall the app and try again?",
|
||||||
|
"status_failure_incompatible_description": "The app is incompatible with this device.\n\nContact the developer of the app and ask for support.",
|
||||||
|
"status_failure_conflict_description": "The installation was prevented by an existing installation of the app.\n\nUninstall the installed app and try again?",
|
||||||
|
"status_failure_blocked_description": "The installation was blocked by {packageName}.\n\nAdjust your security settings and try again.",
|
||||||
|
"install_failed_verification_failure_description": "The installation failed due to a verification issue.\n\nAdjust your security settings and try again.",
|
||||||
|
"install_failed_version_downgrade_description": "The installation failed due to the patched app being a lower version than the installed app.\n\nUninstall the app and try again?",
|
||||||
|
"status_unknown_description": "The installation failed due to an unknown reason. Please try again."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
1
assets/revanced-logo/revanced-logo-round.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 800 800" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><g id="Logo"><g id="Ring"><circle id="Ring-Background" serif:id="Ring Background" cx="400" cy="400" r="400" style="fill:#1b1b1b;"/><path id="Ring1" serif:id="Ring" d="M400,0c220.766,0 400,179.234 400,400c-0,220.766 -179.234,400 -400,400c-220.766,-0 -400,-179.234 -400,-400c0,-220.766 179.234,-400 400,-400Zm-0,36c200.897,-0 364,163.103 364,364c0,200.897 -163.103,364 -364,364c-200.897,0 -364,-163.103 -364,-364c-0,-200.897 163.103,-364 364,-364Z" style="fill:url(#_Linear1);"/></g><g id="Shape"><path id="V-Shape" serif:id="V Shape" d="M538.74,269.872c1.481,-3.382 1.157,-7.283 -0.863,-10.373c-2.021,-3.091 -5.464,-4.954 -9.156,-4.954c-5.148,0 -10.435,0 -14.165,0c-3.1,0 -5.907,1.834 -7.153,4.672c-12.468,28.396 -78.273,178.273 -100.25,228.328c-1.246,2.838 -4.053,4.671 -7.154,4.671c-3.1,0 -5.907,-1.833 -7.153,-4.671c-21.977,-50.055 -87.782,-199.932 -100.25,-228.328c-1.246,-2.838 -4.053,-4.672 -7.153,-4.672c-3.73,0 -9.017,0 -14.164,0c-3.693,0 -7.135,1.863 -9.156,4.954c-2.02,3.09 -2.344,6.991 -0.863,10.373c23.557,53.766 101.872,232.519 117.871,269.034c1.743,3.979 5.674,6.549 10.018,6.549c6.293,-0 15.408,-0 21.701,-0c4.344,-0 8.275,-2.57 10.018,-6.549c15.999,-36.515 94.315,-215.268 117.872,-269.034Z" style="fill:#fff;"/><path id="Diamond" d="M408.119,395.312c-1.675,2.901 -4.77,4.688 -8.119,4.688c-3.349,-0 -6.444,-1.787 -8.119,-4.688c-16.997,-29.44 -56.156,-97.264 -73.153,-126.704c-1.675,-2.901 -1.675,-6.474 0,-9.375c1.675,-2.901 4.77,-4.688 8.119,-4.688c33.995,0 112.311,0 146.306,0c3.349,0 6.444,1.787 8.119,4.688c1.675,2.901 1.675,6.474 -0,9.375c-16.997,29.44 -56.156,97.264 -73.153,126.704Z" style="fill:url(#_Linear2);"/></g></g><defs><linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(4.89859e-14,800,-800,4.89859e-14,400.001,3.31681e-10)"><stop offset="0" style="stop-color:#f04e98;stop-opacity:1"/><stop offset="0.5" style="stop-color:#5f65d4;stop-opacity:1"/><stop offset="1" style="stop-color:#4e98f0;stop-opacity:1"/></linearGradient><linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.77155e-14,289.317,-282.535,1.73003e-14,400,254.545)"><stop offset="0" style="stop-color:#f04e98;stop-opacity:1"/><stop offset="0.5" style="stop-color:#5f65d4;stop-opacity:1"/><stop offset="1" style="stop-color:#4e98f0;stop-opacity:1"/></linearGradient></defs></svg>
|
||||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -1,3 +1,4 @@
|
|||||||
|
preserve_hierarchy: 1
|
||||||
files:
|
files:
|
||||||
- source: /assets/i18n/en.json
|
- source: /assets/i18n/en_US.json
|
||||||
translation: /assets/i18n/%locale_with_underscore%.json
|
translation: /assets/i18n/%locale_with_underscore%.json
|
||||||
|
|||||||
16
docs/0_prerequisites.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# 💼 Prerequisites
|
||||||
|
|
||||||
|
In order to use ReVanced Manager, certain requirements must be met.
|
||||||
|
|
||||||
|
## 🤝 Requirements
|
||||||
|
|
||||||
|
- An Android device running Android 8 or higher
|
||||||
|
- Any device architecture except ARMv7[^1]
|
||||||
|
|
||||||
|
[^1]: This constraint only applies to patches, that require patching APK resources which is why some patches may or may not work on ARMv7 architecture. You can find out, which architectures your device supports here: [⚙️ Configuring ReVanced Manager](2_4_settings.md#%E2%84%B9%EF%B8%8F-about).
|
||||||
|
|
||||||
|
## ⏭️ What's next
|
||||||
|
|
||||||
|
The next page will guide you through patching an app.
|
||||||
|
|
||||||
|
Continue: [⬇️ Installation](1_installation.md)
|
||||||
14
docs/1_installation.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# ⬇️ Installation
|
||||||
|
|
||||||
|
In order to use ReVanced on your Android device, ReVanced Manager must be installed.
|
||||||
|
|
||||||
|
## ✅ Installation steps
|
||||||
|
|
||||||
|
1. Download the latest version of ReVanced Manager from [here](https://github.com/revanced/revanced-manager/releases/latest)
|
||||||
|
2. Install ReVanced Manager
|
||||||
|
|
||||||
|
## ⏭️ What's next
|
||||||
|
|
||||||
|
The next page will guide you through using ReVanced Manager.
|
||||||
|
|
||||||
|
Continue: [🛠️ Usage](2_usage.md)
|
||||||
38
docs/2_1_patching.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# 🧩 Patching apps
|
||||||
|
|
||||||
|
The following pages will guide you through using ReVanced Manager to patch apps.
|
||||||
|
|
||||||
|
## ✅ Steps to patch apps
|
||||||
|
|
||||||
|
1. Navigate to the **Patcher** tab from the bottom navigation bar
|
||||||
|
2. Tap on the **Select an app** card
|
||||||
|
3. Choose an app to patch[^1]
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> The suggested version is visible in each app's card.
|
||||||
|
4. Tap on the **Select patches** card and select the patches you want to apply[^2].
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Some patches have options that can or must be configured by tapping on ⚙️ icon next to the patch name.
|
||||||
|
|
||||||
|
>[!WARNING]
|
||||||
|
> If you see a warning you can click on it for more information.
|
||||||
|
5. Tap on the **Done** then **Patch** button
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> The patching process may take ~5 minutes. Exiting the app may cancel patching or increase the time it takes to patch.
|
||||||
|
6. Tap on the **Install** button
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> If you are rooted, you can mount the patched app on top of the original app.[^3]
|
||||||
|
> Optionally, you may export the patched app to storage using the option in the bottom left corner.
|
||||||
|
|
||||||
|
[^1]: Non-root users may be prompted to select an APK from storage, in which case you must source the APK file yourself. ReVanced does not provide any APK files.
|
||||||
|
[^2]: It is suggested to use the default set of patches by tapping on the **Default** button above the list of patches.
|
||||||
|
[^3]: Mounting the patched app on top of the original app will only work if the installed app version matches the version of the app selected in step 3. above.
|
||||||
|
|
||||||
|
## ⏭️ What's next
|
||||||
|
|
||||||
|
The next page will bring you back to the usage page.
|
||||||
|
|
||||||
|
Continue: [🛠️ Usage](2_usage.md)
|
||||||
15
docs/2_2_managing.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# 🧰 Managing patched apps
|
||||||
|
|
||||||
|
After patching an app, you may want to manage it. This page will guide you through managing patched apps.
|
||||||
|
|
||||||
|
## ✅ Steps to manage patched apps
|
||||||
|
|
||||||
|
1. Tap on the **Dashboard** tab in the bottom navigation bar
|
||||||
|
2. Tap on the **Info** button for the app you want to manage
|
||||||
|
3. Choose one of the options from the menu
|
||||||
|
|
||||||
|
## ⏭️ What's next
|
||||||
|
|
||||||
|
The next page will bring you back to the usage page.
|
||||||
|
|
||||||
|
Continue: [🛠️ Usage](2_usage.md)
|
||||||
14
docs/2_3_updating.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# 🔄 Updating ReVanced Manager
|
||||||
|
|
||||||
|
In order to keep up with the latest features and bug fixes, it is recommended to keep ReVanced Manager up to date.
|
||||||
|
|
||||||
|
## ✅ Updating steps
|
||||||
|
|
||||||
|
1. Navigate to the **Dashboard** tab from the bottom navigation bar
|
||||||
|
2. Tap on the **Update** button in the **Updates** section
|
||||||
|
|
||||||
|
## ⏭️ What's next
|
||||||
|
|
||||||
|
The next page will bring you back to the usage page.
|
||||||
|
|
||||||
|
Continue: [🛠️ Usage](2_usage.md)
|
||||||
52
docs/2_4_settings.md
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# ⚙️ Configuring ReVanced Manager
|
||||||
|
|
||||||
|
ReVanced Manager has settings that can be configured to your liking.
|
||||||
|
|
||||||
|
## 🎛️ Essential settings
|
||||||
|
|
||||||
|
- ### 🪛 Allow changing patch selection
|
||||||
|
|
||||||
|
Allows the user to change the patch selection from the default selection.
|
||||||
|
|
||||||
|
- ### 🔍 Version compatibility check
|
||||||
|
|
||||||
|
Constrains patches to supported app versions. Disable this to patch any version of an app.
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> Disabling this may cause issues if the patches are not compatible with the app version.
|
||||||
|
|
||||||
|
- ### 🧑🔬 Show universal patches
|
||||||
|
|
||||||
|
Reveals patches which can be applied to any app.
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> These patches may not work on all apps.
|
||||||
|
|
||||||
|
- ### 🧬 Sources
|
||||||
|
|
||||||
|
Override the API and download patches from a different source.
|
||||||
|
|
||||||
|
- ### 🔗 API URL
|
||||||
|
|
||||||
|
API to use to fetch updates and patches from.
|
||||||
|
|
||||||
|
- ### 💾 Imports & Exports
|
||||||
|
|
||||||
|
You can import, export or reset the following settings:
|
||||||
|
|
||||||
|
- 🔑 Keystore
|
||||||
|
- 📄 Patch selection
|
||||||
|
- ⚙️ Patch options
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> This is particularly useful if you want to backup or reset your settings.
|
||||||
|
|
||||||
|
- ### ❓ About
|
||||||
|
|
||||||
|
View information about your device and ReVanced Manager. This includes the version of ReVanced Manager and supported architectures of your device.
|
||||||
|
|
||||||
|
## ⏭️ What's next
|
||||||
|
|
||||||
|
The next page will bring you back to the usage page.
|
||||||
|
|
||||||
|
Continue: [🛠️ Usage](2_usage.md)
|
||||||
16
docs/2_usage.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# 🛠️ Usage
|
||||||
|
|
||||||
|
The following pages will guide you through using ReVanced Manager to patch apps, manage patched apps, and update ReVanced Manager.
|
||||||
|
|
||||||
|
## 📖 Table of contents
|
||||||
|
|
||||||
|
1. [🧩 Patching apps](2_1_patching.md)
|
||||||
|
2. [🧰 Managing patched apps](2_2_managing.md)
|
||||||
|
3. [🔄 Updating ReVanced Manager](2_3_updating.md)
|
||||||
|
4. [⚙️ Configuring ReVanced Manager](2_4_settings.md)
|
||||||
|
|
||||||
|
## ⏭️ What's next
|
||||||
|
|
||||||
|
The next page will guide you through troubleshooting ReVanced Manager.
|
||||||
|
|
||||||
|
Continue: [❔ Troubleshooting](3_troubleshooting.md)
|
||||||
31
docs/3_troubleshooting.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# ❔ Troubleshooting
|
||||||
|
|
||||||
|
In case you encounter any issues while using ReVanced Manager, please refer to this page for possible solutions.
|
||||||
|
|
||||||
|
- 💉 Patching fails with an error
|
||||||
|
|
||||||
|
Make sure ReVanced Manager is up to date by following [🔄 Updating ReVanced Manager](2_3_updating.md) and select the **Default** button when choosing patches.
|
||||||
|
|
||||||
|
- 🚫 App not installed as package conflicts with an existing package
|
||||||
|
|
||||||
|
An existing installation of the app you're trying to patch conflicts with the patched app (i.e., signature mismatch or downgrade). Uninstall the existing app before installing the patched app.
|
||||||
|
|
||||||
|
- ❗️ Error code `135`, `139` or `1` when patching the app
|
||||||
|
|
||||||
|
You may be trying to patch a split APK[^1]. This may not work under certain circumstances. In such a case, patch a full APK.
|
||||||
|
|
||||||
|
Your device may otherwise be unsupported. Please look at the [Prerequisites](0_prerequisites.md) page for supported devices.
|
||||||
|
|
||||||
|
Alternatively, you can use [ReVanced CLI](https://github.com/revanced/revanced-cli) to patch the app.
|
||||||
|
|
||||||
|
- 🚨 Patched app crashes on launch
|
||||||
|
|
||||||
|
Select the **Default** button when choosing patches.
|
||||||
|
|
||||||
|
## ⏭️ What's next
|
||||||
|
|
||||||
|
The next page will teach you how to build ReVanced Manager from source.
|
||||||
|
|
||||||
|
Continue: [🔨 Building from source](4_building.md)
|
||||||
|
|
||||||
|
[^1]: https://developer.android.com/guide/app-bundle/app-bundle-format
|
||||||
33
docs/4_building.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# 🛠️ Building from source
|
||||||
|
|
||||||
|
This page will guide you through building ReVanced Manager from source.
|
||||||
|
|
||||||
|
1\. Setup the Flutter environment for your [platform](https://docs.flutter.dev/get-started/install)
|
||||||
|
|
||||||
|
2\. Clone the repository
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone https://github.com/revanced/revanced-manager.git && cd revanced-manager
|
||||||
|
```
|
||||||
|
3\. Get dependencies
|
||||||
|
|
||||||
|
```sh
|
||||||
|
flutter pub get
|
||||||
|
```
|
||||||
|
|
||||||
|
4\. Delete conflicting outputs
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> Must be run every time you sync your local repository with the remote repository.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
dart run build_runner build --delete-conflicting-outputs
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
5\. Build the APK
|
||||||
|
|
||||||
|
```sh
|
||||||
|
flutter build apk
|
||||||
|
```
|
||||||
21
docs/README.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# 💊 ReVanced Manager
|
||||||
|
|
||||||
|
This documentation explains how to use [ReVanced Manager](https://github.com/revanced/revanced-manager).
|
||||||
|
|
||||||
|
## 📖 Table of contents
|
||||||
|
|
||||||
|
0. [💼 Prerequisites](0_prerequisites.md)
|
||||||
|
1. [⬇️ Installation](1_installation.md)
|
||||||
|
2. [🛠️ Usage](2_usage.md)
|
||||||
|
1. [🧩 Patching apps](2_1_patching.md)
|
||||||
|
2. [🧰 Managing patched apps](2_2_managing.md)
|
||||||
|
3. [🔄 Updating ReVanced Manager](2_3_updating.md)
|
||||||
|
4. [⚙️ Configuring ReVanced Manager](2_4_settings.md)
|
||||||
|
3. [❔ Troubleshooting](3_troubleshooting.md)
|
||||||
|
4. [🔨 Building from source](4_building.md)
|
||||||
|
|
||||||
|
## ⏭️ Start here
|
||||||
|
|
||||||
|
The next page will tell you about the prerequisites for using ReVanced Manager.
|
||||||
|
|
||||||
|
Continue: [💼 Prerequisites](0_prerequisites.md)
|
||||||
2
fastlane/Appfile
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
json_key_file("") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one
|
||||||
|
package_name("app.revanced.manager.flutter") # e.g. com.krausefx.app
|
||||||
38
fastlane/Fastfile
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# This file contains the fastlane.tools configuration
|
||||||
|
# You can find the documentation at https://docs.fastlane.tools
|
||||||
|
#
|
||||||
|
# For a list of all available actions, check out
|
||||||
|
#
|
||||||
|
# https://docs.fastlane.tools/actions
|
||||||
|
#
|
||||||
|
# For a list of all available plugins, check out
|
||||||
|
#
|
||||||
|
# https://docs.fastlane.tools/plugins/available-plugins
|
||||||
|
#
|
||||||
|
|
||||||
|
# Uncomment the line if you want fastlane to automatically update itself
|
||||||
|
# update_fastlane
|
||||||
|
|
||||||
|
default_platform(:android)
|
||||||
|
|
||||||
|
platform :android do
|
||||||
|
desc "Runs all the tests"
|
||||||
|
lane :test do
|
||||||
|
gradle(task: "test")
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "Submit a new Beta Build to Crashlytics Beta"
|
||||||
|
lane :beta do
|
||||||
|
gradle(task: "clean assembleRelease")
|
||||||
|
crashlytics
|
||||||
|
|
||||||
|
# sh "your_script.sh"
|
||||||
|
# You can also use other beta testing services here
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "Deploy a new version to the Google Play"
|
||||||
|
lane :deploy do
|
||||||
|
gradle(task: "clean assembleRelease")
|
||||||
|
upload_to_play_store
|
||||||
|
end
|
||||||
|
end
|
||||||
48
fastlane/README.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
fastlane documentation
|
||||||
|
----
|
||||||
|
|
||||||
|
# Installation
|
||||||
|
|
||||||
|
Make sure you have the latest version of the Xcode command line tools installed:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
xcode-select --install
|
||||||
|
```
|
||||||
|
|
||||||
|
For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane)
|
||||||
|
|
||||||
|
# Available Actions
|
||||||
|
|
||||||
|
## Android
|
||||||
|
|
||||||
|
### android test
|
||||||
|
|
||||||
|
```sh
|
||||||
|
[bundle exec] fastlane android test
|
||||||
|
```
|
||||||
|
|
||||||
|
Runs all the tests
|
||||||
|
|
||||||
|
### android beta
|
||||||
|
|
||||||
|
```sh
|
||||||
|
[bundle exec] fastlane android beta
|
||||||
|
```
|
||||||
|
|
||||||
|
Submit a new Beta Build to Crashlytics Beta
|
||||||
|
|
||||||
|
### android deploy
|
||||||
|
|
||||||
|
```sh
|
||||||
|
[bundle exec] fastlane android deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
Deploy a new version to the Google Play
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
|
||||||
|
|
||||||
|
More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools).
|
||||||
|
|
||||||
|
The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools).
|
||||||
1
fastlane/metadata/android/en-US/full_description.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ReVanced Manager is an Android application that uses ReVanced Patcher to add, remove, and modify existing functionalities in Android applications
|
||||||
BIN
fastlane/metadata/android/en-US/images/featureGraphic.png
Normal file
|
After Width: | Height: | Size: 110 KiB |
BIN
fastlane/metadata/android/en-US/images/icon.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/1.jpg
Normal file
|
After Width: | Height: | Size: 374 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/2.jpg
Normal file
|
After Width: | Height: | Size: 694 KiB |
1
fastlane/metadata/android/en-US/short_description.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Patch your favorite apps, right on your device.
|
||||||
1
fastlane/metadata/android/en-US/title.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ReVanced Manager
|
||||||
20
fastlane/report.xml
Normal file
BIN
fonts/custom-icons.ttf
Normal file
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:revanced_manager/services/download_manager.dart';
|
||||||
import 'package:revanced_manager/services/github_api.dart';
|
import 'package:revanced_manager/services/github_api.dart';
|
||||||
import 'package:revanced_manager/services/manager_api.dart';
|
import 'package:revanced_manager/services/manager_api.dart';
|
||||||
import 'package:revanced_manager/services/patcher_api.dart';
|
import 'package:revanced_manager/services/patcher_api.dart';
|
||||||
@@ -9,6 +10,8 @@ import 'package:revanced_manager/ui/views/home/home_viewmodel.dart';
|
|||||||
import 'package:revanced_manager/ui/views/installer/installer_view.dart';
|
import 'package:revanced_manager/ui/views/installer/installer_view.dart';
|
||||||
import 'package:revanced_manager/ui/views/navigation/navigation_view.dart';
|
import 'package:revanced_manager/ui/views/navigation/navigation_view.dart';
|
||||||
import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart';
|
import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart';
|
||||||
|
import 'package:revanced_manager/ui/views/patch_options/patch_options_view.dart';
|
||||||
|
import 'package:revanced_manager/ui/views/patch_options/patch_options_viewmodel.dart';
|
||||||
import 'package:revanced_manager/ui/views/patcher/patcher_view.dart';
|
import 'package:revanced_manager/ui/views/patcher/patcher_view.dart';
|
||||||
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
||||||
import 'package:revanced_manager/ui/views/patches_selector/patches_selector_view.dart';
|
import 'package:revanced_manager/ui/views/patches_selector/patches_selector_view.dart';
|
||||||
@@ -23,6 +26,7 @@ import 'package:stacked_services/stacked_services.dart';
|
|||||||
MaterialRoute(page: PatcherView),
|
MaterialRoute(page: PatcherView),
|
||||||
MaterialRoute(page: AppSelectorView),
|
MaterialRoute(page: AppSelectorView),
|
||||||
MaterialRoute(page: PatchesSelectorView),
|
MaterialRoute(page: PatchesSelectorView),
|
||||||
|
MaterialRoute(page: PatchOptionsView),
|
||||||
MaterialRoute(page: InstallerView),
|
MaterialRoute(page: InstallerView),
|
||||||
MaterialRoute(page: SettingsView),
|
MaterialRoute(page: SettingsView),
|
||||||
MaterialRoute(page: ContributorsView),
|
MaterialRoute(page: ContributorsView),
|
||||||
@@ -32,11 +36,13 @@ import 'package:stacked_services/stacked_services.dart';
|
|||||||
LazySingleton(classType: NavigationViewModel),
|
LazySingleton(classType: NavigationViewModel),
|
||||||
LazySingleton(classType: HomeViewModel),
|
LazySingleton(classType: HomeViewModel),
|
||||||
LazySingleton(classType: PatcherViewModel),
|
LazySingleton(classType: PatcherViewModel),
|
||||||
|
LazySingleton(classType: PatchOptionsViewModel),
|
||||||
LazySingleton(classType: NavigationService),
|
LazySingleton(classType: NavigationService),
|
||||||
LazySingleton(classType: ManagerAPI),
|
LazySingleton(classType: ManagerAPI),
|
||||||
LazySingleton(classType: PatcherAPI),
|
LazySingleton(classType: PatcherAPI),
|
||||||
LazySingleton(classType: RevancedAPI),
|
LazySingleton(classType: RevancedAPI),
|
||||||
LazySingleton(classType: GithubAPI),
|
LazySingleton(classType: GithubAPI),
|
||||||
|
LazySingleton(classType: DownloadManager),
|
||||||
LazySingleton(classType: Toast),
|
LazySingleton(classType: Toast),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,34 +1,53 @@
|
|||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_i18n/flutter_i18n.dart';
|
import 'package:flutter_i18n/flutter_i18n.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:revanced_manager/app/app.locator.dart';
|
import 'package:revanced_manager/app/app.locator.dart';
|
||||||
|
import 'package:revanced_manager/services/download_manager.dart';
|
||||||
import 'package:revanced_manager/services/github_api.dart';
|
import 'package:revanced_manager/services/github_api.dart';
|
||||||
import 'package:revanced_manager/services/manager_api.dart';
|
import 'package:revanced_manager/services/manager_api.dart';
|
||||||
import 'package:revanced_manager/services/patcher_api.dart';
|
|
||||||
import 'package:revanced_manager/services/revanced_api.dart';
|
import 'package:revanced_manager/services/revanced_api.dart';
|
||||||
|
import 'package:revanced_manager/services/root_api.dart';
|
||||||
import 'package:revanced_manager/ui/theme/dynamic_theme_builder.dart';
|
import 'package:revanced_manager/ui/theme/dynamic_theme_builder.dart';
|
||||||
import 'package:revanced_manager/ui/views/navigation/navigation_view.dart';
|
import 'package:revanced_manager/ui/views/navigation/navigation_view.dart';
|
||||||
import 'package:stacked_themes/stacked_themes.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:timezone/data/latest.dart' as tz;
|
import 'package:timezone/data/latest.dart' as tz;
|
||||||
|
|
||||||
|
late SharedPreferences prefs;
|
||||||
Future main() async {
|
Future main() async {
|
||||||
await ThemeManager.initialise();
|
|
||||||
await setupLocator();
|
await setupLocator();
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
await locator<ManagerAPI>().initialize();
|
await locator<ManagerAPI>().initialize();
|
||||||
String apiUrl = locator<ManagerAPI>().getApiUrl();
|
await locator<DownloadManager>().initialize();
|
||||||
|
final String apiUrl = locator<ManagerAPI>().getApiUrl();
|
||||||
await locator<RevancedAPI>().initialize(apiUrl);
|
await locator<RevancedAPI>().initialize(apiUrl);
|
||||||
locator<GithubAPI>().initialize();
|
final String repoUrl = locator<ManagerAPI>().getRepoUrl();
|
||||||
await locator<PatcherAPI>().initialize();
|
locator<GithubAPI>().initialize(repoUrl);
|
||||||
tz.initializeTimeZones();
|
tz.initializeTimeZones();
|
||||||
|
|
||||||
|
// TODO(aAbed): remove in the future, keep it for now during migration.
|
||||||
|
final rootAPI = RootAPI();
|
||||||
|
if (await rootAPI.hasRootPermissions()) {
|
||||||
|
await rootAPI.removeOrphanedFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
prefs = await SharedPreferences.getInstance();
|
||||||
|
|
||||||
runApp(const MyApp());
|
runApp(const MyApp());
|
||||||
}
|
}
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatelessWidget {
|
||||||
const MyApp({Key? key}) : super(key: key);
|
const MyApp({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
// String rawLocale = prefs.getString('language') ?? 'en_US';
|
||||||
|
// String replaceLocale = rawLocale.replaceAll('_', '-');
|
||||||
|
// List<String> localeList = replaceLocale.split('-');
|
||||||
|
// Locale locale = Locale(localeList[0], localeList[1]);
|
||||||
|
const Locale locale = Locale('en', 'US');
|
||||||
|
|
||||||
return DynamicThemeBuilder(
|
return DynamicThemeBuilder(
|
||||||
title: 'ReVanced Manager',
|
title: 'ReVanced Manager',
|
||||||
home: const NavigationView(),
|
home: const NavigationView(),
|
||||||
@@ -36,11 +55,18 @@ class MyApp extends StatelessWidget {
|
|||||||
FlutterI18nDelegate(
|
FlutterI18nDelegate(
|
||||||
translationLoader: FileTranslationLoader(
|
translationLoader: FileTranslationLoader(
|
||||||
fallbackFile: 'en_US',
|
fallbackFile: 'en_US',
|
||||||
|
forcedLocale: locale,
|
||||||
basePath: 'assets/i18n',
|
basePath: 'assets/i18n',
|
||||||
|
useCountryCode: true,
|
||||||
),
|
),
|
||||||
|
missingTranslationHandler: (key, locale) {
|
||||||
|
log(
|
||||||
|
'--> Missing translation: key: $key, languageCode: ${locale?.languageCode}',
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
GlobalMaterialLocalizations.delegate,
|
GlobalMaterialLocalizations.delegate,
|
||||||
GlobalWidgetsLocalizations.delegate
|
GlobalWidgetsLocalizations.delegate,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +1,44 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
import 'package:revanced_manager/utils/string.dart';
|
|
||||||
|
|
||||||
part 'patch.g.dart';
|
part 'patch.g.dart';
|
||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class Patch {
|
class Patch {
|
||||||
final String name;
|
|
||||||
final String description;
|
|
||||||
final String version;
|
|
||||||
final bool excluded;
|
|
||||||
final List<String> dependencies;
|
|
||||||
final List<Package> compatiblePackages;
|
|
||||||
|
|
||||||
Patch({
|
Patch({
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.description,
|
required this.description,
|
||||||
required this.version,
|
|
||||||
required this.excluded,
|
required this.excluded,
|
||||||
required this.dependencies,
|
|
||||||
required this.compatiblePackages,
|
required this.compatiblePackages,
|
||||||
|
required this.options,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory Patch.fromJson(Map<String, dynamic> json) => _$PatchFromJson(json);
|
factory Patch.fromJson(Map<String, dynamic> json) {
|
||||||
|
_migrateV16ToV17(json);
|
||||||
|
|
||||||
|
return _$PatchFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _migrateV16ToV17(Map<String, dynamic> json) {
|
||||||
|
if (json['options'] == null) {
|
||||||
|
json['options'] = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final String name;
|
||||||
|
final String? description;
|
||||||
|
final bool excluded;
|
||||||
|
final List<Package> compatiblePackages;
|
||||||
|
final List<Option> options;
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$PatchToJson(this);
|
Map<String, dynamic> toJson() => _$PatchToJson(this);
|
||||||
|
|
||||||
String getSimpleName() {
|
String getSimpleName() {
|
||||||
return name
|
return name;
|
||||||
.replaceAll('-', ' ')
|
|
||||||
.split('-')
|
|
||||||
.join(' ')
|
|
||||||
.toTitleCase()
|
|
||||||
.replaceFirst('Microg', 'MicroG');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class Package {
|
class Package {
|
||||||
final String name;
|
|
||||||
final List<String> versions;
|
|
||||||
|
|
||||||
Package({
|
Package({
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.versions,
|
required this.versions,
|
||||||
@@ -48,5 +47,49 @@ class Package {
|
|||||||
factory Package.fromJson(Map<String, dynamic> json) =>
|
factory Package.fromJson(Map<String, dynamic> json) =>
|
||||||
_$PackageFromJson(json);
|
_$PackageFromJson(json);
|
||||||
|
|
||||||
|
final String name;
|
||||||
|
final List<String> versions;
|
||||||
|
|
||||||
Map toJson() => _$PackageToJson(this);
|
Map toJson() => _$PackageToJson(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class Option {
|
||||||
|
Option({
|
||||||
|
required this.key,
|
||||||
|
required this.title,
|
||||||
|
required this.description,
|
||||||
|
required this.value,
|
||||||
|
required this.values,
|
||||||
|
required this.required,
|
||||||
|
required this.valueType,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory Option.fromJson(Map<String, dynamic> json) {
|
||||||
|
_migrateV17ToV19(json);
|
||||||
|
|
||||||
|
return _$OptionFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _migrateV17ToV19(Map<String, dynamic> json) {
|
||||||
|
if (json['valueType'] == null) {
|
||||||
|
final type = json['optionClassType'];
|
||||||
|
if (type is String) {
|
||||||
|
json['valueType'] =
|
||||||
|
type.replaceAll('PatchOption', '').replaceAll('List', 'Array');
|
||||||
|
|
||||||
|
json['optionClassType'] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final String key;
|
||||||
|
final String title;
|
||||||
|
final String description;
|
||||||
|
final dynamic value;
|
||||||
|
final Map<String, dynamic>? values;
|
||||||
|
final bool required;
|
||||||
|
final String valueType;
|
||||||
|
|
||||||
|
Map toJson() => _$OptionToJson(this);
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,9 +6,22 @@ part 'patched_application.g.dart';
|
|||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class PatchedApplication {
|
class PatchedApplication {
|
||||||
|
PatchedApplication({
|
||||||
|
required this.name,
|
||||||
|
required this.packageName,
|
||||||
|
required this.version,
|
||||||
|
required this.apkFilePath,
|
||||||
|
required this.icon,
|
||||||
|
required this.patchDate,
|
||||||
|
this.isRooted = false,
|
||||||
|
this.isFromStorage = false,
|
||||||
|
this.appliedPatches = const [],
|
||||||
|
});
|
||||||
|
|
||||||
|
factory PatchedApplication.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$PatchedApplicationFromJson(json);
|
||||||
String name;
|
String name;
|
||||||
String packageName;
|
String packageName;
|
||||||
String originalPackageName;
|
|
||||||
String version;
|
String version;
|
||||||
final String apkFilePath;
|
final String apkFilePath;
|
||||||
@JsonKey(
|
@JsonKey(
|
||||||
@@ -19,27 +32,7 @@ class PatchedApplication {
|
|||||||
DateTime patchDate;
|
DateTime patchDate;
|
||||||
bool isRooted;
|
bool isRooted;
|
||||||
bool isFromStorage;
|
bool isFromStorage;
|
||||||
bool hasUpdates;
|
|
||||||
List<String> appliedPatches;
|
List<String> appliedPatches;
|
||||||
List<String> changelog;
|
|
||||||
|
|
||||||
PatchedApplication({
|
|
||||||
required this.name,
|
|
||||||
required this.packageName,
|
|
||||||
required this.originalPackageName,
|
|
||||||
required this.version,
|
|
||||||
required this.apkFilePath,
|
|
||||||
required this.icon,
|
|
||||||
required this.patchDate,
|
|
||||||
this.isRooted = false,
|
|
||||||
this.isFromStorage = false,
|
|
||||||
this.hasUpdates = false,
|
|
||||||
this.appliedPatches = const [],
|
|
||||||
this.changelog = const [],
|
|
||||||
});
|
|
||||||
|
|
||||||
factory PatchedApplication.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$PatchedApplicationFromJson(json);
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$PatchedApplicationToJson(this);
|
Map<String, dynamic> toJson() => _$PatchedApplicationToJson(this);
|
||||||
|
|
||||||
|
|||||||
75
lib/services/download_manager.dart
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter_cache_manager/file.dart';
|
||||||
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
|
import 'package:injectable/injectable.dart';
|
||||||
|
import 'package:revanced_manager/app/app.locator.dart';
|
||||||
|
import 'package:revanced_manager/services/manager_api.dart';
|
||||||
|
|
||||||
|
@lazySingleton
|
||||||
|
class DownloadManager {
|
||||||
|
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||||
|
late final String _userAgent;
|
||||||
|
|
||||||
|
final _cacheOptions = CacheOptions(
|
||||||
|
store: MemCacheStore(),
|
||||||
|
maxStale: const Duration(days: 1),
|
||||||
|
priority: CachePriority.high,
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<void> initialize() async {
|
||||||
|
_userAgent =
|
||||||
|
'ReVanced-Manager/${await _managerAPI.getCurrentManagerVersion()}';
|
||||||
|
}
|
||||||
|
|
||||||
|
Dio initDio(String url) {
|
||||||
|
var dio = Dio();
|
||||||
|
try {
|
||||||
|
dio = Dio(
|
||||||
|
BaseOptions(
|
||||||
|
baseUrl: url,
|
||||||
|
headers: {
|
||||||
|
'User-Agent': _userAgent,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} on Exception catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dio.interceptors.add(DioCacheInterceptor(options: _cacheOptions));
|
||||||
|
return dio;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> clearAllCache() async {
|
||||||
|
try {
|
||||||
|
await _cacheOptions.store!.clean();
|
||||||
|
} on Exception catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<File> getSingleFile(String url) async {
|
||||||
|
return DefaultCacheManager().getSingleFile(
|
||||||
|
url,
|
||||||
|
headers: {
|
||||||
|
'User-Agent': _userAgent,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<FileResponse> getFileStream(String url) {
|
||||||
|
return DefaultCacheManager().getFileStream(
|
||||||
|
url,
|
||||||
|
withProgress: true,
|
||||||
|
headers: {
|
||||||
|
'User-Agent': _userAgent,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,128 +1,171 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:dio_http_cache_lts/dio_http_cache_lts.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
|
||||||
import 'package:injectable/injectable.dart';
|
import 'package:injectable/injectable.dart';
|
||||||
import 'package:native_dio_client/native_dio_client.dart';
|
import 'package:revanced_manager/app/app.locator.dart';
|
||||||
import 'package:revanced_manager/models/patch.dart';
|
import 'package:revanced_manager/services/download_manager.dart';
|
||||||
import 'package:revanced_manager/utils/check_for_gms.dart';
|
import 'package:revanced_manager/services/manager_api.dart';
|
||||||
|
|
||||||
@lazySingleton
|
@lazySingleton
|
||||||
class GithubAPI {
|
class GithubAPI {
|
||||||
late Dio _dio = Dio();
|
late final Dio _dio;
|
||||||
final DioCacheManager _dioCacheManager = DioCacheManager(CacheConfig());
|
late final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||||
final Options _cacheOptions = buildCacheOptions(
|
late final DownloadManager _downloadManager = locator<DownloadManager>();
|
||||||
const Duration(hours: 6),
|
|
||||||
maxStale: const Duration(days: 1),
|
|
||||||
);
|
|
||||||
final Map<String, String> repoAppPath = {
|
|
||||||
'com.google.android.youtube': 'youtube',
|
|
||||||
'com.google.android.apps.youtube.music': 'music',
|
|
||||||
'com.twitter.android': 'twitter',
|
|
||||||
'com.reddit.frontpage': 'reddit',
|
|
||||||
'com.zhiliaoapp.musically': 'tiktok',
|
|
||||||
'de.dwd.warnapp': 'warnwetter',
|
|
||||||
'com.garzotto.pflotsh.ecmwf_a': 'ecmwf',
|
|
||||||
'com.spotify.music': 'spotify',
|
|
||||||
};
|
|
||||||
|
|
||||||
void initialize() async {
|
Future<void> initialize(String repoUrl) async {
|
||||||
bool isGMSInstalled = await checkForGMS();
|
_dio = _downloadManager.initDio(repoUrl);
|
||||||
|
|
||||||
if (!isGMSInstalled) {
|
|
||||||
_dio = Dio(BaseOptions(
|
|
||||||
baseUrl: 'https://api.github.com',
|
|
||||||
));
|
|
||||||
print('GitHub API: Using default engine + $isGMSInstalled');
|
|
||||||
} else {
|
|
||||||
_dio = Dio(BaseOptions(
|
|
||||||
baseUrl: 'https://api.github.com',
|
|
||||||
))
|
|
||||||
..httpClientAdapter = NativeAdapter();
|
|
||||||
print('ReVanced API: Using CronetEngine + $isGMSInstalled');
|
|
||||||
}
|
|
||||||
_dio.interceptors.add(_dioCacheManager.interceptor);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> clearAllCache() async {
|
Future<void> clearAllCache() async {
|
||||||
await _dioCacheManager.clearAll();
|
await _downloadManager.clearAllCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, dynamic>?> _getLatestRelease(String repoName) async {
|
Future<Map<String, dynamic>?> getLatestRelease(
|
||||||
|
String repoName,
|
||||||
|
) async {
|
||||||
try {
|
try {
|
||||||
var response = await _dio.get(
|
final response = await _dio.get(
|
||||||
'/repos/$repoName/releases/latest',
|
'/repos/$repoName/releases',
|
||||||
options: _cacheOptions,
|
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data[0];
|
||||||
} on Exception {
|
} on Exception catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<String>> getCommits(
|
Future<Map<String, dynamic>?> getPatchesRelease(
|
||||||
String packageName,
|
|
||||||
String repoName,
|
String repoName,
|
||||||
DateTime since,
|
String version,
|
||||||
) async {
|
) async {
|
||||||
String path =
|
|
||||||
'src/main/kotlin/app/revanced/patches/${repoAppPath[packageName]}';
|
|
||||||
try {
|
try {
|
||||||
var response = await _dio.get(
|
final response = await _dio.get(
|
||||||
'/repos/$repoName/commits',
|
'/repos/$repoName/releases/tags/$version',
|
||||||
queryParameters: {
|
|
||||||
'path': path,
|
|
||||||
'since': since.toIso8601String(),
|
|
||||||
},
|
|
||||||
options: _cacheOptions,
|
|
||||||
);
|
);
|
||||||
List<dynamic> commits = response.data;
|
return response.data;
|
||||||
return commits
|
} on Exception catch (e) {
|
||||||
.map(
|
if (kDebugMode) {
|
||||||
(commit) => (commit['commit']['message']).split('\n')[0] +
|
print(e);
|
||||||
' - ' +
|
}
|
||||||
commit['commit']['author']['name'] +
|
return null;
|
||||||
'\n' as String,
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
} catch (e) {
|
|
||||||
return List.empty();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<File?> getLatestReleaseFile(String extension, String repoName) async {
|
Future<Map<String, dynamic>?> getLatestPatchesRelease(
|
||||||
|
String repoName,
|
||||||
|
) async {
|
||||||
try {
|
try {
|
||||||
Map<String, dynamic>? release = await _getLatestRelease(repoName);
|
final response = await _dio.get(
|
||||||
|
'/repos/$repoName/releases/latest',
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
} on Exception catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>?> getLatestManagerRelease(
|
||||||
|
String repoName,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
final response = await _dio.get(
|
||||||
|
'/repos/$repoName/releases',
|
||||||
|
);
|
||||||
|
final Map<String, dynamic> releases = response.data[0];
|
||||||
|
int updates = 0;
|
||||||
|
final String currentVersion =
|
||||||
|
await _managerAPI.getCurrentManagerVersion();
|
||||||
|
while (response.data[updates]['tag_name'] != 'v$currentVersion') {
|
||||||
|
updates++;
|
||||||
|
}
|
||||||
|
for (int i = 1; i < updates; i++) {
|
||||||
|
releases.update(
|
||||||
|
'body',
|
||||||
|
(value) =>
|
||||||
|
value +
|
||||||
|
'\n' +
|
||||||
|
'# ' +
|
||||||
|
response.data[i]['tag_name'] +
|
||||||
|
'\n' +
|
||||||
|
response.data[i]['body'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return releases;
|
||||||
|
} on Exception catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<File?> getLatestReleaseFile(
|
||||||
|
String extension,
|
||||||
|
String repoName,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
final Map<String, dynamic>? release = await getLatestRelease(repoName);
|
||||||
if (release != null) {
|
if (release != null) {
|
||||||
Map<String, dynamic>? asset =
|
final Map<String, dynamic>? asset =
|
||||||
(release['assets'] as List<dynamic>).firstWhereOrNull(
|
(release['assets'] as List<dynamic>).firstWhereOrNull(
|
||||||
(asset) => (asset['name'] as String).endsWith(extension),
|
(asset) => (asset['name'] as String).endsWith(extension),
|
||||||
);
|
);
|
||||||
if (asset != null) {
|
if (asset != null) {
|
||||||
return await DefaultCacheManager().getSingleFile(
|
return await _downloadManager.getSingleFile(
|
||||||
asset['browser_download_url'],
|
asset['browser_download_url'],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} on Exception {
|
} on Exception catch (e) {
|
||||||
return null;
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Patch>> getPatches(String repoName) async {
|
Future<File?> getPatchesReleaseFile(
|
||||||
List<Patch> patches = [];
|
String extension,
|
||||||
|
String repoName,
|
||||||
|
String version,
|
||||||
|
String url,
|
||||||
|
) async {
|
||||||
try {
|
try {
|
||||||
File? f = await getLatestReleaseFile('.json', repoName);
|
if (url.isNotEmpty) {
|
||||||
if (f != null) {
|
return await _downloadManager.getSingleFile(
|
||||||
List<dynamic> list = jsonDecode(f.readAsStringSync());
|
url,
|
||||||
patches = list.map((patch) => Patch.fromJson(patch)).toList();
|
);
|
||||||
|
}
|
||||||
|
final Map<String, dynamic>? release =
|
||||||
|
await getPatchesRelease(repoName, version);
|
||||||
|
if (release != null) {
|
||||||
|
final Map<String, dynamic>? asset =
|
||||||
|
(release['assets'] as List<dynamic>).firstWhereOrNull(
|
||||||
|
(asset) => (asset['name'] as String).endsWith(extension),
|
||||||
|
);
|
||||||
|
if (asset != null) {
|
||||||
|
final String downloadUrl = asset['browser_download_url'];
|
||||||
|
if (extension == '.apk') {
|
||||||
|
_managerAPI.setIntegrationsDownloadURL(downloadUrl);
|
||||||
|
} else {
|
||||||
|
_managerAPI.setPatchesDownloadURL(downloadUrl);
|
||||||
|
}
|
||||||
|
return await _downloadManager.getSingleFile(
|
||||||
|
downloadUrl,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} on Exception catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
}
|
}
|
||||||
} on Exception {
|
|
||||||
return List.empty();
|
|
||||||
}
|
}
|
||||||
return patches;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,88 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:device_apps/device_apps.dart';
|
import 'package:device_apps/device_apps.dart';
|
||||||
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_i18n/widgets/I18nText.dart';
|
||||||
import 'package:injectable/injectable.dart';
|
import 'package:injectable/injectable.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:revanced_manager/app/app.locator.dart';
|
import 'package:revanced_manager/app/app.locator.dart';
|
||||||
import 'package:revanced_manager/models/patch.dart';
|
import 'package:revanced_manager/models/patch.dart';
|
||||||
import 'package:revanced_manager/models/patched_application.dart';
|
import 'package:revanced_manager/models/patched_application.dart';
|
||||||
import 'package:revanced_manager/services/github_api.dart';
|
import 'package:revanced_manager/services/github_api.dart';
|
||||||
|
import 'package:revanced_manager/services/patcher_api.dart';
|
||||||
import 'package:revanced_manager/services/revanced_api.dart';
|
import 'package:revanced_manager/services/revanced_api.dart';
|
||||||
import 'package:revanced_manager/services/root_api.dart';
|
import 'package:revanced_manager/services/root_api.dart';
|
||||||
|
import 'package:revanced_manager/services/toast.dart';
|
||||||
|
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_checkbox_list_tile.dart';
|
||||||
|
import 'package:revanced_manager/utils/check_for_supported_patch.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:timeago/timeago.dart';
|
||||||
|
|
||||||
@lazySingleton
|
@lazySingleton
|
||||||
class ManagerAPI {
|
class ManagerAPI {
|
||||||
final RevancedAPI _revancedAPI = locator<RevancedAPI>();
|
final RevancedAPI _revancedAPI = locator<RevancedAPI>();
|
||||||
final GithubAPI _githubAPI = locator<GithubAPI>();
|
final GithubAPI _githubAPI = locator<GithubAPI>();
|
||||||
|
final Toast _toast = locator<Toast>();
|
||||||
final RootAPI _rootAPI = RootAPI();
|
final RootAPI _rootAPI = RootAPI();
|
||||||
final String patcherRepo = 'revanced-patcher';
|
final String patcherRepo = 'revanced-patcher';
|
||||||
final String cliRepo = 'revanced-cli';
|
final String cliRepo = 'revanced-cli';
|
||||||
late SharedPreferences _prefs;
|
late SharedPreferences _prefs;
|
||||||
String defaultApiUrl = 'https://releases.rvcd.win/';
|
List<Patch> patches = [];
|
||||||
|
List<Option> modifiedOptions = [];
|
||||||
|
List<Option> options = [];
|
||||||
|
Patch? selectedPatch;
|
||||||
|
BuildContext? ctx;
|
||||||
|
bool isRooted = false;
|
||||||
|
bool suggestedAppVersionSelected = true;
|
||||||
|
bool isDynamicThemeAvailable = false;
|
||||||
|
String storedPatchesFile = '/selected-patches.json';
|
||||||
|
String keystoreFile =
|
||||||
|
'/sdcard/Android/data/app.revanced.manager.flutter/files/revanced-manager.keystore';
|
||||||
|
String defaultKeystorePassword = 's3cur3p@ssw0rd';
|
||||||
|
String defaultApiUrl = 'https://api.revanced.app/';
|
||||||
|
String defaultRepoUrl = 'https://api.github.com';
|
||||||
String defaultPatcherRepo = 'revanced/revanced-patcher';
|
String defaultPatcherRepo = 'revanced/revanced-patcher';
|
||||||
String defaultPatchesRepo = 'revanced/revanced-patches';
|
String defaultPatchesRepo = 'revanced/revanced-patches';
|
||||||
String defaultIntegrationsRepo = 'revanced/revanced-integrations';
|
String defaultIntegrationsRepo = 'revanced/revanced-integrations';
|
||||||
String defaultCliRepo = 'revanced/revanced-cli';
|
String defaultCliRepo = 'revanced/revanced-cli';
|
||||||
String defaultManagerRepo = 'revanced/revanced-manager';
|
String defaultManagerRepo = 'revanced/revanced-manager';
|
||||||
|
String? patchesVersion = '';
|
||||||
|
String? integrationsVersion = '';
|
||||||
|
|
||||||
|
bool isDefaultPatchesRepo() {
|
||||||
|
return getPatchesRepo().toLowerCase() == defaultPatchesRepo;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isDefaultIntegrationsRepo() {
|
||||||
|
return getIntegrationsRepo().toLowerCase() ==
|
||||||
|
defaultIntegrationsRepo;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
_prefs = await SharedPreferences.getInstance();
|
_prefs = await SharedPreferences.getInstance();
|
||||||
|
isRooted = await _rootAPI.isRooted();
|
||||||
|
isDynamicThemeAvailable =
|
||||||
|
(await getSdkVersion()) >= 31; // ANDROID_12_SDK_VERSION = 31
|
||||||
|
storedPatchesFile =
|
||||||
|
(await getApplicationDocumentsDirectory()).path + storedPatchesFile;
|
||||||
|
|
||||||
|
// Migrate to new API URL if not done yet as the old one is sunset.
|
||||||
|
final bool hasMigrated = _prefs.getBool('migratedToNewApiUrl') ?? false;
|
||||||
|
if (!hasMigrated) {
|
||||||
|
final String apiUrl = getApiUrl().toLowerCase();
|
||||||
|
if (apiUrl.contains('releases.revanced.app')) {
|
||||||
|
await setApiUrl(''); // Reset to default.
|
||||||
|
_prefs.setBool('migratedToNewApiUrl', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> getSdkVersion() async {
|
||||||
|
final AndroidDeviceInfo info = await DeviceInfoPlugin().androidInfo;
|
||||||
|
return info.version.sdkInt;
|
||||||
}
|
}
|
||||||
|
|
||||||
String getApiUrl() {
|
String getApiUrl() {
|
||||||
@@ -38,9 +93,28 @@ class ManagerAPI {
|
|||||||
if (url.isEmpty || url == ' ') {
|
if (url.isEmpty || url == ' ') {
|
||||||
url = defaultApiUrl;
|
url = defaultApiUrl;
|
||||||
}
|
}
|
||||||
await _revancedAPI.initialize(url);
|
|
||||||
await _revancedAPI.clearAllCache();
|
await _revancedAPI.clearAllCache();
|
||||||
await _prefs.setString('apiUrl', url);
|
await _prefs.setString('apiUrl', url);
|
||||||
|
_toast.showBottom('settingsView.restartAppForChanges');
|
||||||
|
}
|
||||||
|
|
||||||
|
String getRepoUrl() {
|
||||||
|
return _prefs.getString('repoUrl') ?? defaultRepoUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setRepoUrl(String url) async {
|
||||||
|
if (url.isEmpty || url == ' ') {
|
||||||
|
url = defaultRepoUrl;
|
||||||
|
}
|
||||||
|
await _prefs.setString('repoUrl', url);
|
||||||
|
}
|
||||||
|
|
||||||
|
String getPatchesDownloadURL() {
|
||||||
|
return _prefs.getString('patchesDownloadURL') ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setPatchesDownloadURL(String value) async {
|
||||||
|
await _prefs.setString('patchesDownloadURL', value);
|
||||||
}
|
}
|
||||||
|
|
||||||
String getPatchesRepo() {
|
String getPatchesRepo() {
|
||||||
@@ -54,6 +128,109 @@ class ManagerAPI {
|
|||||||
await _prefs.setString('patchesRepo', value);
|
await _prefs.setString('patchesRepo', value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool getPatchesConsent() {
|
||||||
|
return _prefs.getBool('patchesConsent') ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setPatchesConsent(bool consent) async {
|
||||||
|
await _prefs.setBool('patchesConsent', consent);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isPatchesAutoUpdate() {
|
||||||
|
return _prefs.getBool('patchesAutoUpdate') ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isPatchesChangeEnabled() {
|
||||||
|
return _prefs.getBool('patchesChangeEnabled') ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setPatchesChangeEnabled(bool value) {
|
||||||
|
_prefs.setBool('patchesChangeEnabled', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool showPatchesChangeWarning() {
|
||||||
|
return _prefs.getBool('showPatchesChangeWarning') ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setPatchesChangeWarning(bool value) {
|
||||||
|
_prefs.setBool('showPatchesChangeWarning', !value);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isChangingToggleModified() {
|
||||||
|
return _prefs.getBool('isChangingToggleModified') ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setChangingToggleModified(bool value) {
|
||||||
|
_prefs.setBool('isChangingToggleModified', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setPatchesAutoUpdate(bool value) async {
|
||||||
|
await _prefs.setBool('patchesAutoUpdate', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Patch> getSavedPatches(String packageName) {
|
||||||
|
final List<String> patchesJson =
|
||||||
|
_prefs.getStringList('savedPatches-$packageName') ?? [];
|
||||||
|
final List<Patch> patches = patchesJson.map((String patchJson) {
|
||||||
|
return Patch.fromJson(jsonDecode(patchJson));
|
||||||
|
}).toList();
|
||||||
|
return patches;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> savePatches(List<Patch> patches, String packageName) async {
|
||||||
|
final List<String> patchesJson = patches.map((Patch patch) {
|
||||||
|
return jsonEncode(patch.toJson());
|
||||||
|
}).toList();
|
||||||
|
await _prefs.setStringList('savedPatches-$packageName', patchesJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
String getIntegrationsDownloadURL() {
|
||||||
|
return _prefs.getString('integrationsDownloadURL') ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setIntegrationsDownloadURL(String value) async {
|
||||||
|
await _prefs.setString('integrationsDownloadURL', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Patch> getUsedPatches(String packageName) {
|
||||||
|
final List<String> patchesJson =
|
||||||
|
_prefs.getStringList('usedPatches-$packageName') ?? [];
|
||||||
|
final List<Patch> patches = patchesJson.map((String patchJson) {
|
||||||
|
return Patch.fromJson(jsonDecode(patchJson));
|
||||||
|
}).toList();
|
||||||
|
return patches;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setUsedPatches(List<Patch> patches, String packageName) async {
|
||||||
|
final List<String> patchesJson = patches.map((Patch patch) {
|
||||||
|
return jsonEncode(patch.toJson());
|
||||||
|
}).toList();
|
||||||
|
await _prefs.setStringList('usedPatches-$packageName', patchesJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
Option? getPatchOption(String packageName, String patchName, String key) {
|
||||||
|
final String? optionJson =
|
||||||
|
_prefs.getString('patchOption-$packageName-$patchName-$key');
|
||||||
|
if (optionJson != null) {
|
||||||
|
final Option option = Option.fromJson(jsonDecode(optionJson));
|
||||||
|
return option;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setPatchOption(Option option, String patchName, String packageName) {
|
||||||
|
final String optionJson = jsonEncode(option.toJson());
|
||||||
|
_prefs.setString(
|
||||||
|
'patchOption-$packageName-$patchName-${option.key}',
|
||||||
|
optionJson,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearPatchOption(String packageName, String patchName, String key) {
|
||||||
|
_prefs.remove('patchOption-$packageName-$patchName-$key');
|
||||||
|
}
|
||||||
|
|
||||||
String getIntegrationsRepo() {
|
String getIntegrationsRepo() {
|
||||||
return _prefs.getString('integrationsRepo') ?? defaultIntegrationsRepo;
|
return _prefs.getString('integrationsRepo') ?? defaultIntegrationsRepo;
|
||||||
}
|
}
|
||||||
@@ -73,31 +250,83 @@ class ManagerAPI {
|
|||||||
await _prefs.setBool('useDynamicTheme', value);
|
await _prefs.setBool('useDynamicTheme', value);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool getUseDarkTheme() {
|
int getThemeMode() {
|
||||||
return _prefs.getBool('useDarkTheme') ?? false;
|
return _prefs.getInt('themeMode') ?? 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setUseDarkTheme(bool value) async {
|
Future<void> setThemeMode(int value) async {
|
||||||
await _prefs.setBool('useDarkTheme', value);
|
await _prefs.setInt('themeMode', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool areUniversalPatchesEnabled() {
|
||||||
|
return _prefs.getBool('universalPatchesEnabled') ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> enableUniversalPatchesStatus(bool value) async {
|
||||||
|
await _prefs.setBool('universalPatchesEnabled', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isVersionCompatibilityCheckEnabled() {
|
||||||
|
return _prefs.getBool('versionCompatibilityCheckEnabled') ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> enableVersionCompatibilityCheckStatus(bool value) async {
|
||||||
|
await _prefs.setBool('versionCompatibilityCheckEnabled', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isRequireSuggestedAppVersionEnabled() {
|
||||||
|
return _prefs.getBool('requireSuggestedAppVersionEnabled') ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> enableRequireSuggestedAppVersionStatus(bool value) async {
|
||||||
|
await _prefs.setBool('requireSuggestedAppVersionEnabled', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setKeystorePassword(String password) async {
|
||||||
|
await _prefs.setString('keystorePassword', password);
|
||||||
|
}
|
||||||
|
|
||||||
|
String getKeystorePassword() {
|
||||||
|
return _prefs.getString('keystorePassword') ?? defaultKeystorePassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteTempFolder() async {
|
||||||
|
final Directory dir = Directory('/data/local/tmp/revanced-manager');
|
||||||
|
if (await dir.exists()) {
|
||||||
|
await dir.delete(recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteKeystore() async {
|
||||||
|
final File keystore = File(
|
||||||
|
keystoreFile,
|
||||||
|
);
|
||||||
|
if (await keystore.exists()) {
|
||||||
|
await keystore.delete();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<PatchedApplication> getPatchedApps() {
|
List<PatchedApplication> getPatchedApps() {
|
||||||
List<String> apps = _prefs.getStringList('patchedApps') ?? [];
|
final List<String> apps = _prefs.getStringList('patchedApps') ?? [];
|
||||||
return apps.map((a) => PatchedApplication.fromJson(jsonDecode(a))).toList();
|
return apps.map((a) => PatchedApplication.fromJson(jsonDecode(a))).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setPatchedApps(List<PatchedApplication> patchedApps) async {
|
Future<void> setPatchedApps(
|
||||||
|
List<PatchedApplication> patchedApps,
|
||||||
|
) async {
|
||||||
if (patchedApps.length > 1) {
|
if (patchedApps.length > 1) {
|
||||||
patchedApps.sort((a, b) => a.name.compareTo(b.name));
|
patchedApps.sort((a, b) => a.name.compareTo(b.name));
|
||||||
}
|
}
|
||||||
await _prefs.setStringList('patchedApps',
|
await _prefs.setStringList(
|
||||||
patchedApps.map((a) => json.encode(a.toJson())).toList());
|
'patchedApps',
|
||||||
|
patchedApps.map((a) => json.encode(a.toJson())).toList(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> savePatchedApp(PatchedApplication app) async {
|
Future<void> savePatchedApp(PatchedApplication app) async {
|
||||||
List<PatchedApplication> patchedApps = getPatchedApps();
|
final List<PatchedApplication> patchedApps = getPatchedApps();
|
||||||
patchedApps.removeWhere((a) => a.packageName == app.packageName);
|
patchedApps.removeWhere((a) => a.packageName == app.packageName);
|
||||||
ApplicationWithIcon? installed = await DeviceApps.getApp(
|
final ApplicationWithIcon? installed = await DeviceApps.getApp(
|
||||||
app.packageName,
|
app.packageName,
|
||||||
true,
|
true,
|
||||||
) as ApplicationWithIcon?;
|
) as ApplicationWithIcon?;
|
||||||
@@ -111,14 +340,20 @@ class ManagerAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deletePatchedApp(PatchedApplication app) async {
|
Future<void> deletePatchedApp(PatchedApplication app) async {
|
||||||
List<PatchedApplication> patchedApps = getPatchedApps();
|
final List<PatchedApplication> patchedApps = getPatchedApps();
|
||||||
patchedApps.removeWhere((a) => a.packageName == app.packageName);
|
patchedApps.removeWhere((a) => a.packageName == app.packageName);
|
||||||
await setPatchedApps(patchedApps);
|
await setPatchedApps(patchedApps);
|
||||||
}
|
}
|
||||||
|
|
||||||
void clearAllData() {
|
Future<void> clearAllData() async {
|
||||||
_revancedAPI.clearAllCache();
|
try {
|
||||||
_githubAPI.clearAllCache();
|
_revancedAPI.clearAllCache();
|
||||||
|
_githubAPI.clearAllCache();
|
||||||
|
} on Exception catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, List<dynamic>>> getContributors() async {
|
Future<Map<String, List<dynamic>>> getContributors() async {
|
||||||
@@ -126,48 +361,109 @@ class ManagerAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Patch>> getPatches() async {
|
Future<List<Patch>> getPatches() async {
|
||||||
String repoName = getPatchesRepo();
|
if (patches.isNotEmpty) {
|
||||||
if (repoName == defaultPatchesRepo) {
|
return patches;
|
||||||
return await _revancedAPI.getPatches();
|
|
||||||
} else {
|
|
||||||
return await _githubAPI.getPatches(repoName);
|
|
||||||
}
|
}
|
||||||
|
final File? patchBundleFile = await downloadPatches();
|
||||||
|
final Directory appCache = await getTemporaryDirectory();
|
||||||
|
Directory('${appCache.path}/cache').createSync();
|
||||||
|
final Directory workDir =
|
||||||
|
Directory('${appCache.path}/cache').createTempSync('tmp-');
|
||||||
|
final Directory cacheDir = Directory('${workDir.path}/cache');
|
||||||
|
cacheDir.createSync();
|
||||||
|
if (patchBundleFile != null) {
|
||||||
|
try {
|
||||||
|
final String patchesJson = await PatcherAPI.patcherChannel.invokeMethod(
|
||||||
|
'getPatches',
|
||||||
|
{
|
||||||
|
'patchBundleFilePath': patchBundleFile.path,
|
||||||
|
'cacheDirPath': cacheDir.path,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
final List<dynamic> patchesJsonList = jsonDecode(patchesJson);
|
||||||
|
patches = patchesJsonList
|
||||||
|
.map((patchJson) => Patch.fromJson(patchJson))
|
||||||
|
.toList();
|
||||||
|
return patches;
|
||||||
|
} on Exception catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return List.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<File?> downloadPatches() async {
|
Future<File?> downloadPatches() async {
|
||||||
String repoName = getPatchesRepo();
|
try {
|
||||||
if (repoName == defaultPatchesRepo) {
|
final String repoName = getPatchesRepo();
|
||||||
return await _revancedAPI.getLatestReleaseFile(
|
final String currentVersion = await getCurrentPatchesVersion();
|
||||||
|
final String url = getPatchesDownloadURL();
|
||||||
|
return await _githubAPI.getPatchesReleaseFile(
|
||||||
'.jar',
|
'.jar',
|
||||||
defaultPatchesRepo,
|
repoName,
|
||||||
|
currentVersion,
|
||||||
|
url,
|
||||||
);
|
);
|
||||||
} else {
|
} on Exception catch (e) {
|
||||||
return await _githubAPI.getLatestReleaseFile('.jar', repoName);
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<File?> downloadIntegrations() async {
|
Future<File?> downloadIntegrations() async {
|
||||||
String repoName = getIntegrationsRepo();
|
try {
|
||||||
if (repoName == defaultIntegrationsRepo) {
|
final String repoName = getIntegrationsRepo();
|
||||||
return await _revancedAPI.getLatestReleaseFile(
|
final String currentVersion = await getCurrentIntegrationsVersion();
|
||||||
|
final String url = getIntegrationsDownloadURL();
|
||||||
|
return await _githubAPI.getPatchesReleaseFile(
|
||||||
'.apk',
|
'.apk',
|
||||||
defaultIntegrationsRepo,
|
repoName,
|
||||||
|
currentVersion,
|
||||||
|
url,
|
||||||
);
|
);
|
||||||
} else {
|
} on Exception catch (e) {
|
||||||
return await _githubAPI.getLatestReleaseFile('.apk', repoName);
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<File?> downloadManager() async {
|
Future<File?> downloadManager() async {
|
||||||
return await _revancedAPI.getLatestReleaseFile('.apk', defaultManagerRepo);
|
return await _revancedAPI.getLatestReleaseFile(
|
||||||
|
'.apk',
|
||||||
|
defaultManagerRepo,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> getLatestPatcherReleaseTime() async {
|
Future<String?> getLatestPatchesReleaseTime() async {
|
||||||
return await _revancedAPI.getLatestReleaseTime('.gz', defaultPatcherRepo);
|
if (isDefaultPatchesRepo()) {
|
||||||
|
return await _revancedAPI.getLatestReleaseTime(
|
||||||
|
'.json',
|
||||||
|
defaultPatchesRepo,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
final release =
|
||||||
|
await _githubAPI.getLatestPatchesRelease(getPatchesRepo());
|
||||||
|
if (release != null) {
|
||||||
|
final DateTime timestamp =
|
||||||
|
DateTime.parse(release['created_at'] as String);
|
||||||
|
return format(timestamp, locale: 'en_short');
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> getLatestManagerReleaseTime() async {
|
Future<String?> getLatestManagerReleaseTime() async {
|
||||||
return await _revancedAPI.getLatestReleaseTime('.apk', defaultManagerRepo);
|
return await _revancedAPI.getLatestReleaseTime(
|
||||||
|
'.apk',
|
||||||
|
defaultManagerRepo,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> getLatestManagerVersion() async {
|
Future<String?> getLatestManagerVersion() async {
|
||||||
@@ -177,17 +473,87 @@ class ManagerAPI {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<String?> getLatestIntegrationsVersion() async {
|
||||||
|
if (isDefaultIntegrationsRepo()) {
|
||||||
|
return await _revancedAPI.getLatestReleaseVersion(
|
||||||
|
'.apk',
|
||||||
|
defaultIntegrationsRepo,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
final release = await _githubAPI.getLatestRelease(getIntegrationsRepo());
|
||||||
|
if (release != null) {
|
||||||
|
return release['tag_name'];
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> getLatestPatchesVersion() async {
|
||||||
|
if (isDefaultPatchesRepo()) {
|
||||||
|
return await _revancedAPI.getLatestReleaseVersion(
|
||||||
|
'.json',
|
||||||
|
defaultPatchesRepo,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
final release =
|
||||||
|
await _githubAPI.getLatestPatchesRelease(getPatchesRepo());
|
||||||
|
if (release != null) {
|
||||||
|
return release['tag_name'];
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<String> getCurrentManagerVersion() async {
|
Future<String> getCurrentManagerVersion() async {
|
||||||
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
final PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||||
return packageInfo.version;
|
return packageInfo.version;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<String> getCurrentPatchesVersion() async {
|
||||||
|
patchesVersion = _prefs.getString('patchesVersion') ?? '0.0.0';
|
||||||
|
if (patchesVersion == '0.0.0' || isPatchesAutoUpdate()) {
|
||||||
|
final String newPatchesVersion =
|
||||||
|
await getLatestPatchesVersion() ?? '0.0.0';
|
||||||
|
if (patchesVersion != newPatchesVersion && newPatchesVersion != '0.0.0') {
|
||||||
|
await setCurrentPatchesVersion(newPatchesVersion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return patchesVersion!;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setCurrentPatchesVersion(String version) async {
|
||||||
|
await _prefs.setString('patchesVersion', version);
|
||||||
|
await setPatchesDownloadURL('');
|
||||||
|
await downloadPatches();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> getCurrentIntegrationsVersion() async {
|
||||||
|
integrationsVersion = _prefs.getString('integrationsVersion') ?? '0.0.0';
|
||||||
|
if (integrationsVersion == '0.0.0' || isPatchesAutoUpdate()) {
|
||||||
|
final String newIntegrationsVersion =
|
||||||
|
await getLatestIntegrationsVersion() ?? '0.0.0';
|
||||||
|
if (integrationsVersion != newIntegrationsVersion &&
|
||||||
|
newIntegrationsVersion != '0.0.0') {
|
||||||
|
await setCurrentIntegrationsVersion(newIntegrationsVersion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return integrationsVersion!;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setCurrentIntegrationsVersion(String version) async {
|
||||||
|
await _prefs.setString('integrationsVersion', version);
|
||||||
|
await setIntegrationsDownloadURL('');
|
||||||
|
await downloadIntegrations();
|
||||||
|
}
|
||||||
|
|
||||||
Future<List<PatchedApplication>> getAppsToRemove(
|
Future<List<PatchedApplication>> getAppsToRemove(
|
||||||
List<PatchedApplication> patchedApps,
|
List<PatchedApplication> patchedApps,
|
||||||
) async {
|
) async {
|
||||||
List<PatchedApplication> toRemove = [];
|
final List<PatchedApplication> toRemove = [];
|
||||||
for (PatchedApplication app in patchedApps) {
|
for (final PatchedApplication app in patchedApps) {
|
||||||
bool isRemove = await isAppUninstalled(app);
|
final bool isRemove = await isAppUninstalled(app);
|
||||||
if (isRemove) {
|
if (isRemove) {
|
||||||
toRemove.add(app);
|
toRemove.add(app);
|
||||||
}
|
}
|
||||||
@@ -195,101 +561,115 @@ class ManagerAPI {
|
|||||||
return toRemove;
|
return toRemove;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<PatchedApplication>> getUnsavedApps(
|
Future<List<PatchedApplication>> getMountedApps() async {
|
||||||
List<PatchedApplication> patchedApps,
|
final List<PatchedApplication> mountedApps = [];
|
||||||
) async {
|
final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
|
||||||
List<PatchedApplication> unsavedApps = [];
|
|
||||||
bool hasRootPermissions = await _rootAPI.hasRootPermissions();
|
|
||||||
if (hasRootPermissions) {
|
if (hasRootPermissions) {
|
||||||
List<String> installedApps = await _rootAPI.getInstalledApps();
|
final List<String> installedApps = await _rootAPI.getInstalledApps();
|
||||||
for (String packageName in installedApps) {
|
for (final String packageName in installedApps) {
|
||||||
if (!patchedApps.any((app) => app.packageName == packageName)) {
|
final ApplicationWithIcon? application = await DeviceApps.getApp(
|
||||||
ApplicationWithIcon? application = await DeviceApps.getApp(
|
packageName,
|
||||||
packageName,
|
|
||||||
true,
|
|
||||||
) as ApplicationWithIcon?;
|
|
||||||
if (application != null) {
|
|
||||||
unsavedApps.add(
|
|
||||||
PatchedApplication(
|
|
||||||
name: application.appName,
|
|
||||||
packageName: application.packageName,
|
|
||||||
originalPackageName: application.packageName,
|
|
||||||
version: application.versionName!,
|
|
||||||
apkFilePath: application.apkFilePath,
|
|
||||||
icon: application.icon,
|
|
||||||
patchDate: DateTime.now(),
|
|
||||||
isRooted: true,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
List<Application> userApps = await DeviceApps.getInstalledApplications(
|
|
||||||
includeSystemApps: false,
|
|
||||||
includeAppIcons: false,
|
|
||||||
);
|
|
||||||
for (Application app in userApps) {
|
|
||||||
if (app.packageName.startsWith('app.revanced') &&
|
|
||||||
!app.packageName.startsWith('app.revanced.manager.') &&
|
|
||||||
!patchedApps.any((uapp) => uapp.packageName == app.packageName)) {
|
|
||||||
ApplicationWithIcon? application = await DeviceApps.getApp(
|
|
||||||
app.packageName,
|
|
||||||
true,
|
true,
|
||||||
) as ApplicationWithIcon?;
|
) as ApplicationWithIcon?;
|
||||||
if (application != null) {
|
if (application != null) {
|
||||||
unsavedApps.add(
|
mountedApps.add(
|
||||||
PatchedApplication(
|
PatchedApplication(
|
||||||
name: application.appName,
|
name: application.appName,
|
||||||
packageName: application.packageName,
|
packageName: application.packageName,
|
||||||
originalPackageName: application.packageName,
|
|
||||||
version: application.versionName!,
|
version: application.versionName!,
|
||||||
apkFilePath: application.apkFilePath,
|
apkFilePath: application.apkFilePath,
|
||||||
icon: application.icon,
|
icon: application.icon,
|
||||||
patchDate: DateTime.now(),
|
patchDate: DateTime.now(),
|
||||||
isRooted: false,
|
isRooted: true,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return unsavedApps;
|
|
||||||
|
return mountedApps;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> reAssessSavedApps() async {
|
Future<void> showPatchesChangeWarningDialog(BuildContext context) {
|
||||||
List<PatchedApplication> patchedApps = getPatchedApps();
|
final ValueNotifier<bool> noShow =
|
||||||
List<PatchedApplication> unsavedApps = await getUnsavedApps(patchedApps);
|
ValueNotifier(!showPatchesChangeWarning());
|
||||||
patchedApps.addAll(unsavedApps);
|
return showDialog(
|
||||||
List<PatchedApplication> toRemove = await getAppsToRemove(patchedApps);
|
barrierDismissible: false,
|
||||||
|
context: context,
|
||||||
|
builder: (context) => PopScope(
|
||||||
|
canPop: false,
|
||||||
|
child: AlertDialog(
|
||||||
|
title: I18nText('warning'),
|
||||||
|
content: ValueListenableBuilder(
|
||||||
|
valueListenable: noShow,
|
||||||
|
builder: (context, value, child) {
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
I18nText(
|
||||||
|
'patchItem.patchesChangeWarningDialogText',
|
||||||
|
child: const Text(
|
||||||
|
'',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
HapticCheckboxListTile(
|
||||||
|
value: value,
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
title: I18nText(
|
||||||
|
'noShowAgain',
|
||||||
|
),
|
||||||
|
onChanged: (selected) {
|
||||||
|
noShow.value = selected!;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () {
|
||||||
|
setPatchesChangeWarning(noShow.value);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: I18nText('okButton'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> rePatchedSavedApps() async {
|
||||||
|
final List<PatchedApplication> patchedApps = getPatchedApps();
|
||||||
|
|
||||||
|
// Remove apps that are not installed anymore.
|
||||||
|
final List<PatchedApplication> toRemove =
|
||||||
|
await getAppsToRemove(patchedApps);
|
||||||
patchedApps.removeWhere((a) => toRemove.contains(a));
|
patchedApps.removeWhere((a) => toRemove.contains(a));
|
||||||
for (PatchedApplication app in patchedApps) {
|
|
||||||
app.hasUpdates =
|
// Determine all apps that are installed by mounting.
|
||||||
await hasAppUpdates(app.originalPackageName, app.patchDate);
|
final List<PatchedApplication> mountedApps = await getMountedApps();
|
||||||
app.changelog =
|
mountedApps.removeWhere(
|
||||||
await getAppChangelog(app.originalPackageName, app.patchDate);
|
(app) => patchedApps
|
||||||
if (!app.hasUpdates) {
|
.any((patchedApp) => patchedApp.packageName == app.packageName),
|
||||||
String? currentInstalledVersion =
|
);
|
||||||
(await DeviceApps.getApp(app.packageName))?.versionName;
|
patchedApps.addAll(mountedApps);
|
||||||
if (currentInstalledVersion != null) {
|
|
||||||
String currentSavedVersion = app.version;
|
|
||||||
int currentInstalledVersionInt = int.parse(
|
|
||||||
currentInstalledVersion.replaceAll(RegExp('[^0-9]'), ''));
|
|
||||||
int currentSavedVersionInt =
|
|
||||||
int.parse(currentSavedVersion.replaceAll(RegExp('[^0-9]'), ''));
|
|
||||||
if (currentInstalledVersionInt > currentSavedVersionInt) {
|
|
||||||
app.hasUpdates = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await setPatchedApps(patchedApps);
|
await setPatchedApps(patchedApps);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> isAppUninstalled(PatchedApplication app) async {
|
Future<bool> isAppUninstalled(PatchedApplication app) async {
|
||||||
bool existsRoot = false;
|
bool existsRoot = false;
|
||||||
bool existsNonRoot = await DeviceApps.isAppInstalled(app.packageName);
|
final bool existsNonRoot = await DeviceApps.isAppInstalled(app.packageName);
|
||||||
if (app.isRooted) {
|
if (app.isRooted) {
|
||||||
bool hasRootPermissions = await _rootAPI.hasRootPermissions();
|
final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
|
||||||
if (hasRootPermissions) {
|
if (hasRootPermissions) {
|
||||||
existsRoot = await _rootAPI.isAppInstalled(app.packageName);
|
existsRoot = await _rootAPI.isAppInstalled(app.packageName);
|
||||||
}
|
}
|
||||||
@@ -298,32 +678,6 @@ class ManagerAPI {
|
|||||||
return !existsNonRoot;
|
return !existsNonRoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> hasAppUpdates(String packageName, DateTime patchDate) async {
|
|
||||||
List<String> commits = await _githubAPI.getCommits(
|
|
||||||
packageName,
|
|
||||||
getPatchesRepo(),
|
|
||||||
patchDate,
|
|
||||||
);
|
|
||||||
return commits.isNotEmpty;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<String>> getAppChangelog(
|
|
||||||
String packageName, DateTime patchDate) async {
|
|
||||||
List<String> newCommits = await _githubAPI.getCommits(
|
|
||||||
packageName,
|
|
||||||
getPatchesRepo(),
|
|
||||||
patchDate,
|
|
||||||
);
|
|
||||||
if (newCommits.isEmpty) {
|
|
||||||
newCommits = await _githubAPI.getCommits(
|
|
||||||
packageName,
|
|
||||||
getPatchesRepo(),
|
|
||||||
patchDate,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return newCommits;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> isSplitApk(PatchedApplication patchedApp) async {
|
Future<bool> isSplitApk(PatchedApplication patchedApp) async {
|
||||||
Application? app;
|
Application? app;
|
||||||
if (patchedApp.isFromStorage) {
|
if (patchedApp.isFromStorage) {
|
||||||
@@ -333,4 +687,74 @@ class ManagerAPI {
|
|||||||
}
|
}
|
||||||
return app != null && app.isSplit;
|
return app != null && app.isSplit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setSelectedPatches(
|
||||||
|
String app,
|
||||||
|
List<String> patches,
|
||||||
|
) async {
|
||||||
|
final File selectedPatchesFile = File(storedPatchesFile);
|
||||||
|
final Map<String, dynamic> patchesMap = await readSelectedPatchesFile();
|
||||||
|
if (patches.isEmpty) {
|
||||||
|
patchesMap.remove(app);
|
||||||
|
} else {
|
||||||
|
patchesMap[app] = patches;
|
||||||
|
}
|
||||||
|
selectedPatchesFile.writeAsString(jsonEncode(patchesMap));
|
||||||
|
}
|
||||||
|
|
||||||
|
// get default patches for app
|
||||||
|
Future<List<String>> getDefaultPatches() async {
|
||||||
|
final List<Patch> patches = await getPatches();
|
||||||
|
final List<String> defaultPatches = [];
|
||||||
|
if (isVersionCompatibilityCheckEnabled() == true) {
|
||||||
|
defaultPatches.addAll(
|
||||||
|
patches
|
||||||
|
.where(
|
||||||
|
(element) =>
|
||||||
|
element.excluded == false && isPatchSupported(element),
|
||||||
|
)
|
||||||
|
.map((p) => p.name),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
defaultPatches.addAll(
|
||||||
|
patches
|
||||||
|
.where((element) => isPatchSupported(element))
|
||||||
|
.map((p) => p.name),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return defaultPatches;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<String>> getSelectedPatches(String app) async {
|
||||||
|
final Map<String, dynamic> patchesMap = await readSelectedPatchesFile();
|
||||||
|
final List<String> defaultPatches = await getDefaultPatches();
|
||||||
|
return List.from(patchesMap.putIfAbsent(app, () => defaultPatches));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>> readSelectedPatchesFile() async {
|
||||||
|
final File selectedPatchesFile = File(storedPatchesFile);
|
||||||
|
if (!selectedPatchesFile.existsSync()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
final String string = selectedPatchesFile.readAsStringSync();
|
||||||
|
if (string.trim().isEmpty) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return jsonDecode(string);
|
||||||
|
}
|
||||||
|
|
||||||
|
void resetAllOptions() {
|
||||||
|
_prefs.getKeys().where((key) => key.startsWith('patchOption-')).forEach(
|
||||||
|
(key) {
|
||||||
|
_prefs.remove(key);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> resetLastSelectedPatches() async {
|
||||||
|
final File selectedPatchesFile = File(storedPatchesFile);
|
||||||
|
if (selectedPatchesFile.existsSync()) {
|
||||||
|
selectedPatchesFile.deleteSync();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:app_installer/app_installer.dart';
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:device_apps/device_apps.dart';
|
import 'package:device_apps/device_apps.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_file_dialog/flutter_file_dialog.dart';
|
||||||
|
import 'package:flutter_i18n/widgets/I18nText.dart';
|
||||||
import 'package:injectable/injectable.dart';
|
import 'package:injectable/injectable.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:revanced_manager/app/app.locator.dart';
|
import 'package:revanced_manager/app/app.locator.dart';
|
||||||
@@ -10,7 +14,7 @@ import 'package:revanced_manager/models/patch.dart';
|
|||||||
import 'package:revanced_manager/models/patched_application.dart';
|
import 'package:revanced_manager/models/patched_application.dart';
|
||||||
import 'package:revanced_manager/services/manager_api.dart';
|
import 'package:revanced_manager/services/manager_api.dart';
|
||||||
import 'package:revanced_manager/services/root_api.dart';
|
import 'package:revanced_manager/services/root_api.dart';
|
||||||
import 'package:share_extend/share_extend.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
|
||||||
@lazySingleton
|
@lazySingleton
|
||||||
class PatcherAPI {
|
class PatcherAPI {
|
||||||
@@ -22,11 +26,15 @@ class PatcherAPI {
|
|||||||
late Directory _tmpDir;
|
late Directory _tmpDir;
|
||||||
late File _keyStoreFile;
|
late File _keyStoreFile;
|
||||||
List<Patch> _patches = [];
|
List<Patch> _patches = [];
|
||||||
File? _outFile;
|
List<Patch> _universalPatches = [];
|
||||||
|
List<String> _compatiblePackages = [];
|
||||||
|
Map filteredPatches = <String, List<Patch>>{};
|
||||||
|
File? outFile;
|
||||||
|
|
||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
await _loadPatches();
|
await loadPatches();
|
||||||
Directory appCache = await getTemporaryDirectory();
|
await _managerAPI.downloadIntegrations();
|
||||||
|
final Directory appCache = await getTemporaryDirectory();
|
||||||
_dataDir = await getExternalStorageDirectory() ?? appCache;
|
_dataDir = await getExternalStorageDirectory() ?? appCache;
|
||||||
_tmpDir = Directory('${appCache.path}/patcher');
|
_tmpDir = Directory('${appCache.path}/patcher');
|
||||||
_keyStoreFile = File('${_dataDir.path}/revanced-manager.keystore');
|
_keyStoreFile = File('${_dataDir.path}/revanced-manager.keystore');
|
||||||
@@ -39,214 +47,395 @@ class PatcherAPI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadPatches() async {
|
List<String> getCompatiblePackages() {
|
||||||
|
final List<String> compatiblePackages = [];
|
||||||
|
for (final Patch patch in _patches) {
|
||||||
|
for (final Package package in patch.compatiblePackages) {
|
||||||
|
if (!compatiblePackages.contains(package.name)) {
|
||||||
|
compatiblePackages.add(package.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return compatiblePackages;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Patch> getUniversalPatches() {
|
||||||
|
return _patches.where((patch) => patch.compatiblePackages.isEmpty).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> loadPatches() async {
|
||||||
try {
|
try {
|
||||||
if (_patches.isEmpty) {
|
if (_patches.isEmpty) {
|
||||||
_patches = await _managerAPI.getPatches();
|
_patches = await _managerAPI.getPatches();
|
||||||
}
|
}
|
||||||
} on Exception {
|
} on Exception catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
_patches = List.empty();
|
_patches = List.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_compatiblePackages = getCompatiblePackages();
|
||||||
|
_universalPatches = getUniversalPatches();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<ApplicationWithIcon>> getFilteredInstalledApps() async {
|
Future<List<ApplicationWithIcon>> getFilteredInstalledApps(
|
||||||
List<ApplicationWithIcon> filteredApps = [];
|
bool showUniversalPatches,
|
||||||
for (Patch patch in _patches) {
|
) async {
|
||||||
for (Package package in patch.compatiblePackages) {
|
final List<ApplicationWithIcon> filteredApps = [];
|
||||||
try {
|
final bool allAppsIncluded =
|
||||||
if (!filteredApps.any((app) => app.packageName == package.name)) {
|
_universalPatches.isNotEmpty && showUniversalPatches;
|
||||||
ApplicationWithIcon? app = await DeviceApps.getApp(
|
if (allAppsIncluded) {
|
||||||
package.name,
|
final appList = await DeviceApps.getInstalledApplications(
|
||||||
true,
|
includeAppIcons: true,
|
||||||
) as ApplicationWithIcon?;
|
onlyAppsWithLaunchIntent: true,
|
||||||
if (app != null) {
|
);
|
||||||
filteredApps.add(app);
|
|
||||||
}
|
for (final app in appList) {
|
||||||
|
filteredApps.add(app as ApplicationWithIcon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (final packageName in _compatiblePackages) {
|
||||||
|
try {
|
||||||
|
if (!filteredApps.any((app) => app.packageName == packageName)) {
|
||||||
|
final ApplicationWithIcon? app = await DeviceApps.getApp(
|
||||||
|
packageName,
|
||||||
|
true,
|
||||||
|
) as ApplicationWithIcon?;
|
||||||
|
if (app != null) {
|
||||||
|
filteredApps.add(app);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
}
|
||||||
continue;
|
} on Exception catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return filteredApps;
|
return filteredApps;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Patch>> getFilteredPatches(String packageName) async {
|
List<Patch> getFilteredPatches(String packageName) {
|
||||||
return _patches
|
if (!_compatiblePackages.contains(packageName)) {
|
||||||
.where((patch) =>
|
return _universalPatches;
|
||||||
!patch.name.contains('settings') &&
|
}
|
||||||
patch.compatiblePackages.any((pack) => pack.name == packageName))
|
|
||||||
|
final List<Patch> patches = _patches
|
||||||
|
.where(
|
||||||
|
(patch) =>
|
||||||
|
patch.compatiblePackages.isEmpty ||
|
||||||
|
!patch.name.contains('settings') &&
|
||||||
|
patch.compatiblePackages
|
||||||
|
.any((pack) => pack.name == packageName),
|
||||||
|
)
|
||||||
.toList();
|
.toList();
|
||||||
|
if (!_managerAPI.areUniversalPatchesEnabled()) {
|
||||||
|
filteredPatches[packageName] = patches
|
||||||
|
.where((patch) => patch.compatiblePackages.isNotEmpty)
|
||||||
|
.toList();
|
||||||
|
} else {
|
||||||
|
filteredPatches[packageName] = patches;
|
||||||
|
}
|
||||||
|
return filteredPatches[packageName];
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Patch>> getAppliedPatches(List<String> appliedPatches) async {
|
Future<List<Patch>> getAppliedPatches(
|
||||||
|
List<String> appliedPatches,
|
||||||
|
) async {
|
||||||
return _patches
|
return _patches
|
||||||
.where((patch) => appliedPatches.contains(patch.name))
|
.where((patch) => appliedPatches.contains(patch.name))
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool dependencyNeedsIntegrations(String name) {
|
|
||||||
return name.contains('integrations') ||
|
|
||||||
_patches.any(
|
|
||||||
(patch) =>
|
|
||||||
patch.name == name &&
|
|
||||||
(patch.dependencies.any(
|
|
||||||
(dep) => dependencyNeedsIntegrations(dep),
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> needsIntegrations(List<Patch> selectedPatches) async {
|
|
||||||
return selectedPatches.any(
|
|
||||||
(patch) => patch.dependencies.any(
|
|
||||||
(dep) => dependencyNeedsIntegrations(dep),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> needsResourcePatching(List<Patch> selectedPatches) async {
|
|
||||||
return selectedPatches.any(
|
|
||||||
(patch) => patch.dependencies.any(
|
|
||||||
(dep) => dep.contains('resource-'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> needsSettingsPatch(List<Patch> selectedPatches) async {
|
|
||||||
return selectedPatches.any(
|
|
||||||
(patch) => patch.dependencies.any(
|
|
||||||
(dep) => dep.contains('settings'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<String> getOriginalFilePath(
|
|
||||||
String packageName,
|
|
||||||
String originalFilePath,
|
|
||||||
) async {
|
|
||||||
bool hasRootPermissions = await _rootAPI.hasRootPermissions();
|
|
||||||
if (hasRootPermissions) {
|
|
||||||
originalFilePath = await _rootAPI.getOriginalFilePath(
|
|
||||||
packageName,
|
|
||||||
originalFilePath,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return originalFilePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> runPatcher(
|
Future<void> runPatcher(
|
||||||
String packageName,
|
String packageName,
|
||||||
String originalFilePath,
|
String apkFilePath,
|
||||||
List<Patch> selectedPatches,
|
List<Patch> selectedPatches,
|
||||||
) async {
|
) async {
|
||||||
bool mergeIntegrations = await needsIntegrations(selectedPatches);
|
final File? integrationsFile = await _managerAPI.downloadIntegrations();
|
||||||
bool includeSettings = await needsSettingsPatch(selectedPatches);
|
final Map<String, Map<String, dynamic>> options = {};
|
||||||
if (includeSettings) {
|
for (final patch in selectedPatches) {
|
||||||
try {
|
if (patch.options.isNotEmpty) {
|
||||||
Patch? settingsPatch = _patches.firstWhereOrNull(
|
final Map<String, dynamic> patchOptions = {};
|
||||||
(patch) =>
|
for (final option in patch.options) {
|
||||||
patch.name.contains('settings') &&
|
final patchOption =
|
||||||
patch.compatiblePackages.any((pack) => pack.name == packageName),
|
_managerAPI.getPatchOption(packageName, patch.name, option.key);
|
||||||
);
|
if (patchOption != null) {
|
||||||
if (settingsPatch != null) {
|
patchOptions[patchOption.key] = patchOption.value;
|
||||||
selectedPatches.add(settingsPatch);
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
options[patch.name] = patchOptions;
|
||||||
// ignore
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
File? patchBundleFile = await _managerAPI.downloadPatches();
|
|
||||||
File? integrationsFile;
|
if (integrationsFile != null) {
|
||||||
if (mergeIntegrations) {
|
|
||||||
integrationsFile = await _managerAPI.downloadIntegrations();
|
|
||||||
}
|
|
||||||
if (patchBundleFile != null) {
|
|
||||||
_dataDir.createSync();
|
_dataDir.createSync();
|
||||||
_tmpDir.createSync();
|
_tmpDir.createSync();
|
||||||
Directory workDir = _tmpDir.createTempSync('tmp-');
|
final Directory workDir = _tmpDir.createTempSync('tmp-');
|
||||||
File inputFile = File('${workDir.path}/base.apk');
|
final File inputFile = File('${workDir.path}/base.apk');
|
||||||
File patchedFile = File('${workDir.path}/patched.apk');
|
final File patchedFile = File('${workDir.path}/patched.apk');
|
||||||
_outFile = File('${workDir.path}/out.apk');
|
outFile = File('${workDir.path}/out.apk');
|
||||||
Directory cacheDir = Directory('${workDir.path}/cache');
|
final Directory cacheDir = Directory('${workDir.path}/cache');
|
||||||
cacheDir.createSync();
|
cacheDir.createSync();
|
||||||
await patcherChannel.invokeMethod(
|
final String originalFilePath = apkFilePath;
|
||||||
'runPatcher',
|
|
||||||
{
|
try {
|
||||||
'patchBundleFilePath': patchBundleFile.path,
|
await patcherChannel.invokeMethod(
|
||||||
'originalFilePath': await getOriginalFilePath(
|
'runPatcher',
|
||||||
packageName,
|
{
|
||||||
originalFilePath,
|
'originalFilePath': originalFilePath,
|
||||||
),
|
'inputFilePath': inputFile.path,
|
||||||
'inputFilePath': inputFile.path,
|
'patchedFilePath': patchedFile.path,
|
||||||
'patchedFilePath': patchedFile.path,
|
'outFilePath': outFile!.path,
|
||||||
'outFilePath': _outFile!.path,
|
'integrationsPath': integrationsFile.path,
|
||||||
'integrationsPath': mergeIntegrations ? integrationsFile!.path : '',
|
'selectedPatches': selectedPatches.map((p) => p.name).toList(),
|
||||||
'selectedPatches': selectedPatches.map((p) => p.name).toList(),
|
'options': options,
|
||||||
'cacheDirPath': cacheDir.path,
|
'cacheDirPath': cacheDir.path,
|
||||||
'mergeIntegrations': mergeIntegrations,
|
'keyStoreFilePath': _keyStoreFile.path,
|
||||||
'keyStoreFilePath': _keyStoreFile.path,
|
'keystorePassword': _managerAPI.getKeystorePassword(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
} on Exception catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> installPatchedFile(PatchedApplication patchedApp) async {
|
Future<void> stopPatcher() async {
|
||||||
if (_outFile != null) {
|
try {
|
||||||
try {
|
await patcherChannel.invokeMethod('stopPatcher');
|
||||||
if (patchedApp.isRooted) {
|
} on Exception catch (e) {
|
||||||
bool hasRootPermissions = await _rootAPI.hasRootPermissions();
|
if (kDebugMode) {
|
||||||
if (hasRootPermissions) {
|
print(e);
|
||||||
return _rootAPI.installApp(
|
}
|
||||||
patchedApp.packageName,
|
}
|
||||||
patchedApp.apkFilePath,
|
}
|
||||||
_outFile!.path,
|
|
||||||
);
|
Future<int> installPatchedFile(
|
||||||
}
|
BuildContext context,
|
||||||
} else {
|
PatchedApplication patchedApp,
|
||||||
await AppInstaller.installApk(_outFile!.path);
|
) async {
|
||||||
return await DeviceApps.isAppInstalled(patchedApp.packageName);
|
if (outFile != null) {
|
||||||
}
|
_managerAPI.ctx = context;
|
||||||
} on Exception {
|
try {
|
||||||
return false;
|
if (patchedApp.isRooted) {
|
||||||
|
final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
|
||||||
|
final packageVersion = await DeviceApps.getApp(patchedApp.packageName)
|
||||||
|
.then((app) => app?.versionName);
|
||||||
|
if (!hasRootPermissions) {
|
||||||
|
installErrorDialog(1);
|
||||||
|
} else if (packageVersion == null) {
|
||||||
|
installErrorDialog(1.2);
|
||||||
|
} else if (packageVersion == patchedApp.version) {
|
||||||
|
return await _rootAPI.install(
|
||||||
|
patchedApp.packageName,
|
||||||
|
patchedApp.apkFilePath,
|
||||||
|
outFile!.path,
|
||||||
|
)
|
||||||
|
? 0
|
||||||
|
: 1;
|
||||||
|
} else {
|
||||||
|
installErrorDialog(1.1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (await _rootAPI.hasRootPermissions()) {
|
||||||
|
await _rootAPI.uninstall(patchedApp.packageName);
|
||||||
|
}
|
||||||
|
if (context.mounted) {
|
||||||
|
return await installApk(
|
||||||
|
context,
|
||||||
|
outFile!.path,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} on Exception catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> installApk(
|
||||||
|
BuildContext context,
|
||||||
|
String apkPath,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
final status = await patcherChannel.invokeMethod('installApk', {
|
||||||
|
'apkPath': apkPath,
|
||||||
|
});
|
||||||
|
final int statusCode = status['status'];
|
||||||
|
final String message = status['message'];
|
||||||
|
final bool hasExtra =
|
||||||
|
message.contains('INSTALL_FAILED_VERIFICATION_FAILURE') ||
|
||||||
|
message.contains('INSTALL_FAILED_VERSION_DOWNGRADE');
|
||||||
|
if (statusCode == 0 || (statusCode == 3 && !hasExtra)) {
|
||||||
|
return statusCode;
|
||||||
|
} else {
|
||||||
|
_managerAPI.ctx = context;
|
||||||
|
return await installErrorDialog(
|
||||||
|
statusCode,
|
||||||
|
status,
|
||||||
|
hasExtra,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} on Exception catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> installErrorDialog(
|
||||||
|
num statusCode, [
|
||||||
|
status,
|
||||||
|
bool hasExtra = false,
|
||||||
|
]) async {
|
||||||
|
final String statusValue = InstallStatus.byCode(
|
||||||
|
hasExtra ? double.parse('$statusCode.1') : statusCode,
|
||||||
|
);
|
||||||
|
bool cleanInstall = false;
|
||||||
|
final bool isFixable = statusCode == 4 || statusCode == 5;
|
||||||
|
await showDialog(
|
||||||
|
context: _managerAPI.ctx!,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
|
title: I18nText('installErrorDialog.$statusValue'),
|
||||||
|
content: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
I18nText(
|
||||||
|
'installErrorDialog.${statusValue}_description',
|
||||||
|
translationParams: statusCode == 2
|
||||||
|
? {
|
||||||
|
'packageName': status['otherPackageName'],
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: (status == null)
|
||||||
|
? <Widget>[
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () async {
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
child: I18nText('okButton'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: <Widget>[
|
||||||
|
if (!isFixable)
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
child: I18nText('cancelButton'),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
child: I18nText('cancelButton'),
|
||||||
|
),
|
||||||
|
if (isFixable)
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () async {
|
||||||
|
final int response = await patcherChannel.invokeMethod(
|
||||||
|
'uninstallApp',
|
||||||
|
{'packageName': status['packageName']},
|
||||||
|
);
|
||||||
|
if (response == 0 && context.mounted) {
|
||||||
|
cleanInstall = true;
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: I18nText('okButton'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return cleanInstall ? 10 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void exportPatchedFile(String appName, String version) {
|
||||||
|
try {
|
||||||
|
if (outFile != null) {
|
||||||
|
final String newName = _getFileName(appName, version);
|
||||||
|
FlutterFileDialog.saveFile(
|
||||||
|
params: SaveFileDialogParams(
|
||||||
|
sourceFilePath: outFile!.path,
|
||||||
|
fileName: newName,
|
||||||
|
mimeTypesFilter: ['application/vnd.android.package-archive'],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} on Exception catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void sharePatchedFile(String appName, String version) {
|
void sharePatchedFile(String appName, String version) {
|
||||||
if (_outFile != null) {
|
try {
|
||||||
String prefix = appName.toLowerCase().replaceAll(' ', '-');
|
if (outFile != null) {
|
||||||
String newName = '$prefix-revanced_v$version.apk';
|
final String newName = _getFileName(appName, version);
|
||||||
int lastSeparator = _outFile!.path.lastIndexOf('/');
|
final int lastSeparator = outFile!.path.lastIndexOf('/');
|
||||||
String newPath = _outFile!.path.substring(0, lastSeparator + 1) + newName;
|
final String newPath =
|
||||||
File shareFile = _outFile!.copySync(newPath);
|
outFile!.path.substring(0, lastSeparator + 1) + newName;
|
||||||
ShareExtend.share(shareFile.path, 'file');
|
final File shareFile = outFile!.copySync(newPath);
|
||||||
|
Share.shareXFiles([XFile(shareFile.path)]);
|
||||||
|
}
|
||||||
|
} on Exception catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> sharePatcherLog(String logs) async {
|
String _getFileName(String appName, String version) {
|
||||||
Directory appCache = await getTemporaryDirectory();
|
final String patchVersion = _managerAPI.patchesVersion!;
|
||||||
Directory logDir = Directory('${appCache.path}/logs');
|
final String prefix = appName.toLowerCase().replaceAll(' ', '-');
|
||||||
|
final String newName =
|
||||||
|
'$prefix-revanced_v$version-patches_$patchVersion.apk';
|
||||||
|
return newName;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> exportPatcherLog(String logs) async {
|
||||||
|
final Directory appCache = await getTemporaryDirectory();
|
||||||
|
final Directory logDir = Directory('${appCache.path}/logs');
|
||||||
logDir.createSync();
|
logDir.createSync();
|
||||||
String dateTime = DateTime.now()
|
final String dateTime = DateTime.now()
|
||||||
.toIso8601String()
|
.toIso8601String()
|
||||||
.replaceAll('-', '')
|
.replaceAll('-', '')
|
||||||
.replaceAll(':', '')
|
.replaceAll(':', '')
|
||||||
.replaceAll('T', '')
|
.replaceAll('T', '')
|
||||||
.replaceAll('.', '');
|
.replaceAll('.', '');
|
||||||
File log = File('${logDir.path}/revanced-manager_patcher_$dateTime.log');
|
final String fileName = 'revanced-manager_patcher_$dateTime.txt';
|
||||||
|
final File log = File('${logDir.path}/$fileName');
|
||||||
log.writeAsStringSync(logs);
|
log.writeAsStringSync(logs);
|
||||||
ShareExtend.share(log.path, 'file');
|
FlutterFileDialog.saveFile(
|
||||||
|
params: SaveFileDialogParams(
|
||||||
|
sourceFilePath: log.path,
|
||||||
|
fileName: fileName,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String getRecommendedVersion(String packageName) {
|
String getSuggestedVersion(String packageName) {
|
||||||
Map<String, int> versions = {};
|
final Map<String, int> versions = {};
|
||||||
for (Patch patch in _patches) {
|
for (final Patch patch in _patches) {
|
||||||
Package? package = patch.compatiblePackages.firstWhereOrNull(
|
final Package? package = patch.compatiblePackages.firstWhereOrNull(
|
||||||
(pack) => pack.name == packageName,
|
(pack) => pack.name == packageName,
|
||||||
);
|
);
|
||||||
if (package != null) {
|
if (package != null) {
|
||||||
for (String version in package.versions) {
|
for (final String version in package.versions) {
|
||||||
versions.update(
|
versions.update(
|
||||||
version,
|
version,
|
||||||
(value) => versions[version]! + 1,
|
(value) => versions[version]! + 1,
|
||||||
@@ -256,7 +445,7 @@ class PatcherAPI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (versions.isNotEmpty) {
|
if (versions.isNotEmpty) {
|
||||||
var entries = versions.entries.toList()
|
final entries = versions.entries.toList()
|
||||||
..sort((a, b) => a.value.compareTo(b.value));
|
..sort((a, b) => a.value.compareTo(b.value));
|
||||||
versions
|
versions
|
||||||
..clear()
|
..clear()
|
||||||
@@ -267,3 +456,60 @@ class PatcherAPI {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum InstallStatus {
|
||||||
|
mountNoRoot(1),
|
||||||
|
mountVersionMismatch(1.1),
|
||||||
|
mountMissingInstallation(1.2),
|
||||||
|
|
||||||
|
statusFailureBlocked(2),
|
||||||
|
installFailedVerificationFailure(3.1),
|
||||||
|
statusFailureInvalid(4),
|
||||||
|
installFailedVersionDowngrade(4.1),
|
||||||
|
statusFailureConflict(5),
|
||||||
|
statusFailureStorage(6),
|
||||||
|
statusFailureIncompatible(7),
|
||||||
|
statusFailureTimeout(8);
|
||||||
|
|
||||||
|
const InstallStatus(this.statusCode);
|
||||||
|
final double statusCode;
|
||||||
|
|
||||||
|
static String byCode(num code) {
|
||||||
|
try {
|
||||||
|
return InstallStatus.values
|
||||||
|
.firstWhere((flag) => flag.statusCode == code)
|
||||||
|
.status;
|
||||||
|
} catch (e) {
|
||||||
|
return 'status_unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension InstallStatusExtension on InstallStatus {
|
||||||
|
String get status {
|
||||||
|
switch (this) {
|
||||||
|
case InstallStatus.mountNoRoot:
|
||||||
|
return 'mount_no_root';
|
||||||
|
case InstallStatus.mountVersionMismatch:
|
||||||
|
return 'mount_version_mismatch';
|
||||||
|
case InstallStatus.mountMissingInstallation:
|
||||||
|
return 'mount_missing_installation';
|
||||||
|
case InstallStatus.statusFailureBlocked:
|
||||||
|
return 'status_failure_blocked';
|
||||||
|
case InstallStatus.installFailedVerificationFailure:
|
||||||
|
return 'install_failed_verification_failure';
|
||||||
|
case InstallStatus.statusFailureInvalid:
|
||||||
|
return 'status_failure_invalid';
|
||||||
|
case InstallStatus.installFailedVersionDowngrade:
|
||||||
|
return 'install_failed_version_downgrade';
|
||||||
|
case InstallStatus.statusFailureConflict:
|
||||||
|
return 'status_failure_conflict';
|
||||||
|
case InstallStatus.statusFailureStorage:
|
||||||
|
return 'status_failure_storage';
|
||||||
|
case InstallStatus.statusFailureIncompatible:
|
||||||
|
return 'status_failure_incompatible';
|
||||||
|
case InstallStatus.statusFailureTimeout:
|
||||||
|
return 'status_failure_timeout';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,85 +1,69 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:native_dio_client/native_dio_client.dart';
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:dio_http_cache_lts/dio_http_cache_lts.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
import 'package:injectable/injectable.dart';
|
import 'package:injectable/injectable.dart';
|
||||||
import 'package:revanced_manager/models/patch.dart';
|
import 'package:revanced_manager/app/app.locator.dart';
|
||||||
import 'package:revanced_manager/utils/check_for_gms.dart';
|
import 'package:revanced_manager/services/download_manager.dart';
|
||||||
|
import 'package:synchronized/synchronized.dart';
|
||||||
import 'package:timeago/timeago.dart';
|
import 'package:timeago/timeago.dart';
|
||||||
|
|
||||||
@lazySingleton
|
@lazySingleton
|
||||||
class RevancedAPI {
|
class RevancedAPI {
|
||||||
late Dio _dio = Dio();
|
late final Dio _dio;
|
||||||
final DioCacheManager _dioCacheManager = DioCacheManager(CacheConfig());
|
late final DownloadManager _downloadManager = locator<DownloadManager>();
|
||||||
final Options _cacheOptions = buildCacheOptions(
|
|
||||||
const Duration(hours: 6),
|
|
||||||
maxStale: const Duration(days: 1),
|
|
||||||
);
|
|
||||||
|
|
||||||
Future<void> initialize(String apiUrl) async {
|
final Lock getToolsLock = Lock();
|
||||||
bool isGMSInstalled = await checkForGMS();
|
|
||||||
|
|
||||||
if (!isGMSInstalled) {
|
Future<void> initialize(String repoUrl) async {
|
||||||
_dio = Dio(BaseOptions(
|
_dio = _downloadManager.initDio(repoUrl);
|
||||||
baseUrl: apiUrl,
|
|
||||||
));
|
|
||||||
print('ReVanced API: Using default engine + $isGMSInstalled');
|
|
||||||
} else {
|
|
||||||
_dio = Dio(BaseOptions(
|
|
||||||
baseUrl: apiUrl,
|
|
||||||
))
|
|
||||||
..httpClientAdapter = NativeAdapter();
|
|
||||||
print('ReVanced API: Using CronetEngine + $isGMSInstalled');
|
|
||||||
}
|
|
||||||
_dio.interceptors.add(_dioCacheManager.interceptor);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> clearAllCache() async {
|
Future<void> clearAllCache() async {
|
||||||
await _dioCacheManager.clearAll();
|
await _downloadManager.clearAllCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, List<dynamic>>> getContributors() async {
|
Future<Map<String, List<dynamic>>> getContributors() async {
|
||||||
Map<String, List<dynamic>> contributors = {};
|
final Map<String, List<dynamic>> contributors = {};
|
||||||
try {
|
try {
|
||||||
var response = await _dio.get('/contributors', options: _cacheOptions);
|
final response = await _dio.get('/contributors');
|
||||||
List<dynamic> repositories = response.data['repositories'];
|
final List<dynamic> repositories = response.data['repositories'];
|
||||||
for (Map<String, dynamic> repo in repositories) {
|
for (final Map<String, dynamic> repo in repositories) {
|
||||||
String name = repo['name'];
|
final String name = repo['name'].toLowerCase();
|
||||||
contributors[name] = repo['contributors'];
|
contributors[name] = repo['contributors'];
|
||||||
}
|
}
|
||||||
} on Exception {
|
} on Exception catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
return contributors;
|
return contributors;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Patch>> getPatches() async {
|
|
||||||
try {
|
|
||||||
var response = await _dio.get('/patches', options: _cacheOptions);
|
|
||||||
List<dynamic> patches = response.data;
|
|
||||||
return patches.map((patch) => Patch.fromJson(patch)).toList();
|
|
||||||
} on Exception {
|
|
||||||
return List.empty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Map<String, dynamic>?> _getLatestRelease(
|
Future<Map<String, dynamic>?> _getLatestRelease(
|
||||||
String extension,
|
String extension,
|
||||||
String repoName,
|
String repoName,
|
||||||
) async {
|
) {
|
||||||
try {
|
return getToolsLock.synchronized(() async {
|
||||||
var response = await _dio.get('/tools', options: _cacheOptions);
|
try {
|
||||||
List<dynamic> tools = response.data['tools'];
|
final response = await _dio.get('/tools');
|
||||||
return tools.firstWhereOrNull(
|
final List<dynamic> tools = response.data['tools'];
|
||||||
(t) =>
|
return tools.firstWhereOrNull(
|
||||||
t['repository'] == repoName &&
|
(t) =>
|
||||||
(t['name'] as String).endsWith(extension),
|
(t['repository'] as String).toLowerCase() == repoName.toLowerCase() &&
|
||||||
);
|
(t['name'] as String).endsWith(extension),
|
||||||
} on Exception {
|
);
|
||||||
return null;
|
} on Exception catch (e) {
|
||||||
}
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> getLatestReleaseVersion(
|
Future<String?> getLatestReleaseVersion(
|
||||||
@@ -87,49 +71,100 @@ class RevancedAPI {
|
|||||||
String repoName,
|
String repoName,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
Map<String, dynamic>? release = await _getLatestRelease(
|
final Map<String, dynamic>? release = await _getLatestRelease(
|
||||||
extension,
|
extension,
|
||||||
repoName,
|
repoName,
|
||||||
);
|
);
|
||||||
if (release != null) {
|
if (release != null) {
|
||||||
return release['version'];
|
return release['version'];
|
||||||
}
|
}
|
||||||
} on Exception {
|
} on Exception catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<File?> getLatestReleaseFile(String extension, String repoName) async {
|
Future<File?> getLatestReleaseFile(
|
||||||
|
String extension,
|
||||||
|
String repoName,
|
||||||
|
) async {
|
||||||
try {
|
try {
|
||||||
Map<String, dynamic>? release = await _getLatestRelease(
|
final Map<String, dynamic>? release = await _getLatestRelease(
|
||||||
extension,
|
extension,
|
||||||
repoName,
|
repoName,
|
||||||
);
|
);
|
||||||
if (release != null) {
|
if (release != null) {
|
||||||
String url = release['browser_download_url'];
|
final String url = release['browser_download_url'];
|
||||||
return await DefaultCacheManager().getSingleFile(url);
|
return await _downloadManager.getSingleFile(url);
|
||||||
|
}
|
||||||
|
} on Exception catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
}
|
}
|
||||||
} on Exception {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StreamController<double> managerUpdateProgress =
|
||||||
|
StreamController<double>.broadcast();
|
||||||
|
|
||||||
|
void updateManagerDownloadProgress(int progress) {
|
||||||
|
managerUpdateProgress.add(progress.toDouble());
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<double> getManagerUpdateProgress() {
|
||||||
|
return managerUpdateProgress.stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
void disposeManagerUpdateProgress() {
|
||||||
|
managerUpdateProgress.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<File?> downloadManager() async {
|
||||||
|
final Map<String, dynamic>? release = await _getLatestRelease(
|
||||||
|
'.apk',
|
||||||
|
'revanced/revanced-manager',
|
||||||
|
);
|
||||||
|
File? outputFile;
|
||||||
|
await for (final result in _downloadManager.getFileStream(
|
||||||
|
release!['browser_download_url'] as String,
|
||||||
|
)) {
|
||||||
|
if (result is DownloadProgress) {
|
||||||
|
final totalSize = result.totalSize ?? 10000000;
|
||||||
|
final progress = (result.downloaded / totalSize * 100).round();
|
||||||
|
|
||||||
|
updateManagerDownloadProgress(progress);
|
||||||
|
} else if (result is FileInfo) {
|
||||||
|
disposeManagerUpdateProgress();
|
||||||
|
// The download is complete; convert the FileInfo object to a File object
|
||||||
|
outputFile = File(result.file.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return outputFile;
|
||||||
|
}
|
||||||
|
|
||||||
Future<String?> getLatestReleaseTime(
|
Future<String?> getLatestReleaseTime(
|
||||||
String extension,
|
String extension,
|
||||||
String repoName,
|
String repoName,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
Map<String, dynamic>? release = await _getLatestRelease(
|
final Map<String, dynamic>? release = await _getLatestRelease(
|
||||||
extension,
|
extension,
|
||||||
repoName,
|
repoName,
|
||||||
);
|
);
|
||||||
if (release != null) {
|
if (release != null) {
|
||||||
DateTime timestamp = DateTime.parse(release['timestamp'] as String);
|
final DateTime timestamp =
|
||||||
|
DateTime.parse(release['timestamp'] as String);
|
||||||
return format(timestamp, locale: 'en_short');
|
return format(timestamp, locale: 'en_short');
|
||||||
}
|
}
|
||||||
} on Exception {
|
} on Exception catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,15 +1,21 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:root/root.dart';
|
import 'package:root/root.dart';
|
||||||
|
|
||||||
class RootAPI {
|
class RootAPI {
|
||||||
final String _managerDirPath = '/data/local/tmp/revanced-manager';
|
// TODO(aAbed): remove in the future, keep it for now during migration.
|
||||||
final String _postFsDataDirPath = '/data/adb/post-fs-data.d';
|
final String _postFsDataDirPath = '/data/adb/post-fs-data.d';
|
||||||
|
|
||||||
|
final String _revancedDirPath = '/data/adb/revanced';
|
||||||
final String _serviceDDirPath = '/data/adb/service.d';
|
final String _serviceDDirPath = '/data/adb/service.d';
|
||||||
|
|
||||||
Future<bool> isRooted() async {
|
Future<bool> isRooted() async {
|
||||||
try {
|
try {
|
||||||
bool? isRooted = await Root.isRootAvailable();
|
final bool? isRooted = await Root.isRootAvailable();
|
||||||
return isRooted != null && isRooted;
|
return isRooted != null && isRooted;
|
||||||
} on Exception {
|
} on Exception catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -22,7 +28,10 @@ class RootAPI {
|
|||||||
return isRooted != null && isRooted;
|
return isRooted != null && isRooted;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
} on Exception {
|
} on Exception catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -33,127 +42,146 @@ class RootAPI {
|
|||||||
seLinux,
|
seLinux,
|
||||||
String filePath,
|
String filePath,
|
||||||
) async {
|
) async {
|
||||||
if (permissions.isNotEmpty) {
|
try {
|
||||||
|
final StringBuffer commands = StringBuffer();
|
||||||
|
if (permissions.isNotEmpty) {
|
||||||
|
commands.writeln('chmod $permissions $filePath');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ownerGroup.isNotEmpty) {
|
||||||
|
commands.writeln('chown $ownerGroup $filePath');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seLinux.isNotEmpty) {
|
||||||
|
commands.writeln('chcon $seLinux $filePath');
|
||||||
|
}
|
||||||
|
|
||||||
await Root.exec(
|
await Root.exec(
|
||||||
cmd: 'chmod $permissions "$filePath"',
|
cmd: commands.toString(),
|
||||||
);
|
|
||||||
}
|
|
||||||
if (ownerGroup.isNotEmpty) {
|
|
||||||
await Root.exec(
|
|
||||||
cmd: 'chown $ownerGroup "$filePath"',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (seLinux.isNotEmpty) {
|
|
||||||
await Root.exec(
|
|
||||||
cmd: 'chcon $seLinux "$filePath"',
|
|
||||||
);
|
);
|
||||||
|
} on Exception catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> isAppInstalled(String packageName) async {
|
Future<bool> isAppInstalled(String packageName) async {
|
||||||
if (packageName.isNotEmpty) {
|
if (packageName.isNotEmpty) {
|
||||||
String? res = await Root.exec(
|
return fileExists('$_serviceDDirPath/$packageName.sh');
|
||||||
cmd: 'ls "$_managerDirPath/$packageName"',
|
|
||||||
);
|
|
||||||
if (res != null && res.isNotEmpty) {
|
|
||||||
res = await Root.exec(
|
|
||||||
cmd: 'ls "$_serviceDDirPath/$packageName.sh"',
|
|
||||||
);
|
|
||||||
return res != null && res.isNotEmpty;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<String>> getInstalledApps() async {
|
Future<List<String>> getInstalledApps() async {
|
||||||
|
final List<String> apps = List.empty(growable: true);
|
||||||
try {
|
try {
|
||||||
String? res = await Root.exec(
|
final String? res = await Root.exec(cmd: 'ls $_revancedDirPath');
|
||||||
cmd: 'ls "$_managerDirPath"',
|
|
||||||
);
|
|
||||||
if (res != null) {
|
if (res != null) {
|
||||||
List<String> apps = res.split('\n');
|
final List<String> list = res.split('\n');
|
||||||
apps.removeWhere((pack) => pack.isEmpty);
|
list.removeWhere((pack) => pack.isEmpty);
|
||||||
return apps.map((pack) => pack.trim()).toList();
|
apps.addAll(list.map((pack) => pack.trim()).toList());
|
||||||
|
}
|
||||||
|
} on Exception catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
}
|
}
|
||||||
} on Exception {
|
|
||||||
return List.empty();
|
|
||||||
}
|
}
|
||||||
return List.empty();
|
return apps;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deleteApp(String packageName, String originalFilePath) async {
|
Future<void> uninstall(String packageName) async {
|
||||||
await Root.exec(
|
await Root.exec(
|
||||||
cmd: 'am force-stop "$packageName"',
|
cmd: '''
|
||||||
);
|
grep $packageName /proc/mounts | while read -r line; do echo \$line | cut -d " " -f 2 | sed "s/apk.*/apk/" | xargs -r umount -l; done
|
||||||
await Root.exec(
|
rm -rf $_revancedDirPath/$packageName $_serviceDDirPath/$packageName.sh
|
||||||
cmd: 'su -mm -c "umount -l $originalFilePath"',
|
''',
|
||||||
);
|
|
||||||
await Root.exec(
|
|
||||||
cmd: 'rm -rf "$_managerDirPath/$packageName"',
|
|
||||||
);
|
|
||||||
await Root.exec(
|
|
||||||
cmd: 'rm -rf "$_serviceDDirPath/$packageName.sh"',
|
|
||||||
);
|
|
||||||
await Root.exec(
|
|
||||||
cmd: 'rm -rf "$_postFsDataDirPath/$packageName.sh"',
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> installApp(
|
// TODO(aAbed): remove in the future, keep it for now during migration.
|
||||||
|
Future<void> removeOrphanedFiles() async {
|
||||||
|
await Root.exec(
|
||||||
|
cmd: '''
|
||||||
|
find $_revancedDirPath -type f -name original.apk -delete
|
||||||
|
for file in "$_serviceDDirPath"/*; do
|
||||||
|
filename=\$(basename "\$file")
|
||||||
|
if [ -f "$_postFsDataDirPath/\$filename" ]; then
|
||||||
|
rm "$_postFsDataDirPath/\$filename"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
''',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> install(
|
||||||
String packageName,
|
String packageName,
|
||||||
String originalFilePath,
|
String originalFilePath,
|
||||||
String patchedFilePath,
|
String patchedFilePath,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
await deleteApp(packageName, originalFilePath);
|
|
||||||
await Root.exec(
|
|
||||||
cmd: 'mkdir -p "$_managerDirPath/$packageName"',
|
|
||||||
);
|
|
||||||
await setPermissions(
|
await setPermissions(
|
||||||
'0755',
|
'0755',
|
||||||
'shell:shell',
|
'shell:shell',
|
||||||
'',
|
'',
|
||||||
'$_managerDirPath/$packageName',
|
'$_revancedDirPath/$packageName',
|
||||||
);
|
);
|
||||||
await saveOriginalFilePath(packageName, originalFilePath);
|
await installPatchedApk(packageName, patchedFilePath);
|
||||||
await installServiceDScript(packageName);
|
await installServiceDScript(packageName);
|
||||||
await installPostFsDataScript(packageName);
|
await runMountScript(packageName);
|
||||||
await installApk(packageName, patchedFilePath);
|
|
||||||
await mountApk(packageName, originalFilePath);
|
|
||||||
return true;
|
return true;
|
||||||
} on Exception {
|
} on Exception catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> installServiceDScript(String packageName) async {
|
Future<void> installServiceDScript(String packageName) async {
|
||||||
String content = '#!/system/bin/sh\n'
|
|
||||||
'while [ "\$(getprop sys.boot_completed | tr -d \'"\'"\'\\\\r\'"\'"\')" != "1" ]; do sleep 1; done\n'
|
|
||||||
'base_path=$_managerDirPath/$packageName/base.apk\n'
|
|
||||||
'stock_path=\$(pm path $packageName | grep base | sed \'"\'"\'s/package://g\'"\'"\')\n'
|
|
||||||
'[ ! -z \$stock_path ] && mount -o bind \$base_path \$stock_path';
|
|
||||||
String scriptFilePath = '$_serviceDDirPath/$packageName.sh';
|
|
||||||
await Root.exec(
|
await Root.exec(
|
||||||
cmd: 'echo \'$content\' > "$scriptFilePath"',
|
cmd: 'mkdir -p $_serviceDDirPath',
|
||||||
|
);
|
||||||
|
final String mountScript = '''
|
||||||
|
#!/system/bin/sh
|
||||||
|
# Mount using Magisk mirror, if available.
|
||||||
|
MAGISKTMP="\$( magisk --path )" || MAGISKTMP=/sbin
|
||||||
|
MIRROR="\$MAGISKTMP/.magisk/mirror"
|
||||||
|
if [ ! -f \$MIRROR ]; then
|
||||||
|
MIRROR=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
until [ "\$(getprop sys.boot_completed)" = 1 ]; do sleep 3; done
|
||||||
|
until [ -d "/sdcard/Android" ]; do sleep 1; done
|
||||||
|
|
||||||
|
# Unmount any existing installation to prevent multiple unnecessary mounts.
|
||||||
|
grep $packageName /proc/mounts | while read -r line; do echo \$line | cut -d " " -f 2 | sed "s/apk.*/apk/" | xargs -r umount -l; done
|
||||||
|
|
||||||
|
base_path=$_revancedDirPath/$packageName/base.apk
|
||||||
|
stock_path=\$(pm path $packageName | grep base | sed "s/package://g" )
|
||||||
|
|
||||||
|
chcon u:object_r:apk_data_file:s0 \$base_path
|
||||||
|
mount -o bind \$MIRROR\$base_path \$stock_path
|
||||||
|
|
||||||
|
# Kill the app to force it to restart the mounted APK in case it is already running
|
||||||
|
am force-stop $packageName
|
||||||
|
'''
|
||||||
|
.trimMultilineString();
|
||||||
|
final String scriptFilePath = '$_serviceDDirPath/$packageName.sh';
|
||||||
|
await Root.exec(
|
||||||
|
cmd: 'echo \'$mountScript\' > "$scriptFilePath"',
|
||||||
);
|
);
|
||||||
await setPermissions('0744', '', '', scriptFilePath);
|
await setPermissions('0744', '', '', scriptFilePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> installPostFsDataScript(String packageName) async {
|
Future<void> installPatchedApk(
|
||||||
String content = '#!/system/bin/sh\n'
|
String packageName, String patchedFilePath,) async {
|
||||||
'stock_path=\$(pm path $packageName | grep base | sed \'"\'"\'s/package://g\'"\'"\')\n'
|
final String newPatchedFilePath = '$_revancedDirPath/$packageName/base.apk';
|
||||||
'[ ! -z \$stock_path ] && umount -l \$stock_path';
|
|
||||||
String scriptFilePath = '$_postFsDataDirPath/$packageName.sh';
|
|
||||||
await Root.exec(
|
await Root.exec(
|
||||||
cmd: 'echo \'$content\' > "$scriptFilePath"',
|
cmd: '''
|
||||||
);
|
mkdir -p $_revancedDirPath/$packageName
|
||||||
await setPermissions('0744', '', '', scriptFilePath);
|
cp "$patchedFilePath" $newPatchedFilePath
|
||||||
}
|
''',
|
||||||
|
|
||||||
Future<void> installApk(String packageName, String patchedFilePath) async {
|
|
||||||
String newPatchedFilePath = '$_managerDirPath/$packageName/base.apk';
|
|
||||||
await Root.exec(
|
|
||||||
cmd: 'cp "$patchedFilePath" "$newPatchedFilePath"',
|
|
||||||
);
|
);
|
||||||
await setPermissions(
|
await setPermissions(
|
||||||
'0644',
|
'0644',
|
||||||
@@ -163,65 +191,30 @@ class RootAPI {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> mountApk(String packageName, String originalFilePath) async {
|
Future<void> runMountScript(
|
||||||
String newPatchedFilePath = '$_managerDirPath/$packageName/base.apk';
|
|
||||||
await Root.exec(
|
|
||||||
cmd: 'am force-stop "$packageName"',
|
|
||||||
);
|
|
||||||
await Root.exec(
|
|
||||||
cmd: 'su -mm -c "umount -l $originalFilePath"',
|
|
||||||
);
|
|
||||||
await Root.exec(
|
|
||||||
cmd: 'su -mm -c "mount -o bind $newPatchedFilePath $originalFilePath"',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> isMounted(String packageName) async {
|
|
||||||
String? res = await Root.exec(
|
|
||||||
cmd: 'cat /proc/mounts | grep $packageName',
|
|
||||||
);
|
|
||||||
return res != null && res.isNotEmpty;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<String> getOriginalFilePath(
|
|
||||||
String packageName,
|
String packageName,
|
||||||
String originalFilePath,
|
|
||||||
) async {
|
) async {
|
||||||
bool isInstalled = await isAppInstalled(packageName);
|
await Root.exec(cmd: '.$_serviceDDirPath/$packageName.sh');
|
||||||
if (isInstalled && await isMounted(packageName)) {
|
}
|
||||||
originalFilePath = '$_managerDirPath/$packageName/original.apk';
|
|
||||||
await setPermissions(
|
Future<bool> fileExists(String path) async {
|
||||||
'0644',
|
try {
|
||||||
'shell:shell',
|
final String? res = await Root.exec(
|
||||||
'u:object_r:apk_data_file:s0',
|
cmd: 'ls $path',
|
||||||
originalFilePath,
|
|
||||||
);
|
);
|
||||||
|
return res != null && res.isNotEmpty;
|
||||||
|
} on Exception catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
return originalFilePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> saveOriginalFilePath(
|
|
||||||
String packageName,
|
|
||||||
String originalFilePath,
|
|
||||||
) async {
|
|
||||||
String originalRootPath = '$_managerDirPath/$packageName/original.apk';
|
|
||||||
await Root.exec(
|
|
||||||
cmd: 'mkdir -p "$_managerDirPath/$packageName"',
|
|
||||||
);
|
|
||||||
await setPermissions(
|
|
||||||
'0755',
|
|
||||||
'shell:shell',
|
|
||||||
'',
|
|
||||||
'$_managerDirPath/$packageName',
|
|
||||||
);
|
|
||||||
await Root.exec(
|
|
||||||
cmd: 'cp "$originalFilePath" "$originalRootPath"',
|
|
||||||
);
|
|
||||||
await setPermissions(
|
|
||||||
'0644',
|
|
||||||
'shell:shell',
|
|
||||||
'u:object_r:apk_data_file:s0',
|
|
||||||
originalFilePath,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove leading spaces manually until
|
||||||
|
// https://github.com/dart-lang/language/issues/559 is closed
|
||||||
|
extension StringExtension on String {
|
||||||
|
String trimMultilineString() =>
|
||||||
|
split('\n').map((line) => line.trim()).join('\n').trim();
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,4 +20,15 @@ class Toast {
|
|||||||
gravity: t.ToastGravity.CENTER,
|
gravity: t.ToastGravity.CENTER,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void showBottom(String text) {
|
||||||
|
t.Fluttertoast.showToast(
|
||||||
|
msg: FlutterI18n.translate(
|
||||||
|
_fToast.context!,
|
||||||
|
text,
|
||||||
|
),
|
||||||
|
toastLength: t.Toast.LENGTH_LONG,
|
||||||
|
gravity: t.ToastGravity.BOTTOM,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,8 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
|
||||||
var lightCustomColorScheme = ColorScheme.fromSeed(
|
var lightCustomColorScheme = ColorScheme.fromSeed(
|
||||||
seedColor: Colors.blue,
|
seedColor: Colors.purple,
|
||||||
brightness: Brightness.light,
|
primary: const Color(0xFF4C51C0),
|
||||||
primary: const Color(0xff1B73E8),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
var lightCustomTheme = ThemeData(
|
var lightCustomTheme = ThemeData(
|
||||||
@@ -13,7 +12,7 @@ var lightCustomTheme = ThemeData(
|
|||||||
navigationBarTheme: NavigationBarThemeData(
|
navigationBarTheme: NavigationBarThemeData(
|
||||||
labelTextStyle: MaterialStateProperty.all(
|
labelTextStyle: MaterialStateProperty.all(
|
||||||
TextStyle(
|
TextStyle(
|
||||||
color: lightCustomColorScheme.secondary,
|
color: lightCustomColorScheme.onSurface,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -22,10 +21,10 @@ var lightCustomTheme = ThemeData(
|
|||||||
);
|
);
|
||||||
|
|
||||||
var darkCustomColorScheme = ColorScheme.fromSeed(
|
var darkCustomColorScheme = ColorScheme.fromSeed(
|
||||||
seedColor: Colors.blue,
|
seedColor: Colors.purple,
|
||||||
brightness: Brightness.dark,
|
brightness: Brightness.dark,
|
||||||
primary: const Color(0xffA5CAFF),
|
primary: const Color(0xFFBFC1FF),
|
||||||
surface: const Color(0xff1B1A1D),
|
surface: const Color(0xFF131316),
|
||||||
);
|
);
|
||||||
|
|
||||||
var darkCustomTheme = ThemeData(
|
var darkCustomTheme = ThemeData(
|
||||||
@@ -34,13 +33,12 @@ var darkCustomTheme = ThemeData(
|
|||||||
navigationBarTheme: NavigationBarThemeData(
|
navigationBarTheme: NavigationBarThemeData(
|
||||||
labelTextStyle: MaterialStateProperty.all(
|
labelTextStyle: MaterialStateProperty.all(
|
||||||
TextStyle(
|
TextStyle(
|
||||||
color: darkCustomColorScheme.secondary,
|
color: darkCustomColorScheme.onSurface,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
canvasColor: const Color(0xff1B1A1D),
|
canvasColor: const Color(0xFF131316),
|
||||||
scaffoldBackgroundColor: const Color(0xff1B1A1D),
|
scaffoldBackgroundColor: const Color(0xFF131316),
|
||||||
toggleableActiveColor: const Color(0xffA5CAFF),
|
|
||||||
textTheme: GoogleFonts.robotoTextTheme(ThemeData.dark().textTheme),
|
textTheme: GoogleFonts.robotoTextTheme(ThemeData.dark().textTheme),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,94 +1,123 @@
|
|||||||
|
import 'dart:ui';
|
||||||
import 'package:dynamic_color/dynamic_color.dart';
|
import 'package:dynamic_color/dynamic_color.dart';
|
||||||
import 'package:dynamic_themes/dynamic_themes.dart';
|
import 'package:dynamic_themes/dynamic_themes.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'package:revanced_manager/app/app.locator.dart';
|
||||||
import 'package:revanced_manager/app/app.router.dart';
|
import 'package:revanced_manager/app/app.router.dart';
|
||||||
|
import 'package:revanced_manager/services/manager_api.dart';
|
||||||
import 'package:revanced_manager/theme.dart';
|
import 'package:revanced_manager/theme.dart';
|
||||||
import 'package:stacked_services/stacked_services.dart';
|
import 'package:stacked_services/stacked_services.dart';
|
||||||
|
|
||||||
class DynamicThemeBuilder extends StatelessWidget {
|
class DynamicThemeBuilder extends StatefulWidget {
|
||||||
|
const DynamicThemeBuilder({
|
||||||
|
super.key,
|
||||||
|
required this.title,
|
||||||
|
required this.home,
|
||||||
|
required this.localizationsDelegates,
|
||||||
|
});
|
||||||
final String title;
|
final String title;
|
||||||
final Widget home;
|
final Widget home;
|
||||||
final Iterable<LocalizationsDelegate> localizationsDelegates;
|
final Iterable<LocalizationsDelegate> localizationsDelegates;
|
||||||
|
|
||||||
const DynamicThemeBuilder({
|
@override
|
||||||
Key? key,
|
State<DynamicThemeBuilder> createState() => _DynamicThemeBuilderState();
|
||||||
required this.title,
|
}
|
||||||
required this.home,
|
|
||||||
required this.localizationsDelegates,
|
class _DynamicThemeBuilderState extends State<DynamicThemeBuilder>
|
||||||
}) : super(key: key);
|
with WidgetsBindingObserver {
|
||||||
|
Brightness brightness = PlatformDispatcher.instance.platformBrightness;
|
||||||
|
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangePlatformBrightness() {
|
||||||
|
setState(() {
|
||||||
|
brightness = PlatformDispatcher.instance.platformBrightness;
|
||||||
|
});
|
||||||
|
if (_managerAPI.getThemeMode() < 2) {
|
||||||
|
SystemChrome.setSystemUIOverlayStyle(
|
||||||
|
SystemUiOverlayStyle(
|
||||||
|
systemNavigationBarIconBrightness: brightness == Brightness.light
|
||||||
|
? Brightness.dark
|
||||||
|
: Brightness.light,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return DynamicColorBuilder(
|
return DynamicColorBuilder(
|
||||||
builder: (lightColorScheme, darkColorScheme) {
|
builder: (lightColorScheme, darkColorScheme) {
|
||||||
ThemeData lightDynamicTheme = ThemeData(
|
final ThemeData lightDynamicTheme = ThemeData(
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
canvasColor: lightColorScheme?.background,
|
|
||||||
navigationBarTheme: NavigationBarThemeData(
|
navigationBarTheme: NavigationBarThemeData(
|
||||||
backgroundColor: lightColorScheme?.background,
|
|
||||||
indicatorColor: lightColorScheme?.primary.withAlpha(150),
|
|
||||||
labelTextStyle: MaterialStateProperty.all(
|
labelTextStyle: MaterialStateProperty.all(
|
||||||
GoogleFonts.roboto(
|
GoogleFonts.roboto(
|
||||||
color: lightColorScheme?.secondary,
|
color: lightColorScheme?.onSurface,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
iconTheme: MaterialStateProperty.all(
|
|
||||||
IconThemeData(
|
|
||||||
color: lightColorScheme?.secondary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
scaffoldBackgroundColor: lightColorScheme?.background,
|
|
||||||
colorScheme: lightColorScheme?.harmonized(),
|
colorScheme: lightColorScheme?.harmonized(),
|
||||||
toggleableActiveColor: lightColorScheme?.primary,
|
|
||||||
textTheme: GoogleFonts.robotoTextTheme(ThemeData.light().textTheme),
|
textTheme: GoogleFonts.robotoTextTheme(ThemeData.light().textTheme),
|
||||||
);
|
);
|
||||||
ThemeData darkDynamicTheme = ThemeData(
|
final ThemeData darkDynamicTheme = ThemeData(
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
canvasColor: darkColorScheme?.background,
|
|
||||||
navigationBarTheme: NavigationBarThemeData(
|
navigationBarTheme: NavigationBarThemeData(
|
||||||
backgroundColor: darkColorScheme?.background,
|
|
||||||
indicatorColor: darkColorScheme?.primary.withOpacity(0.4),
|
|
||||||
labelTextStyle: MaterialStateProperty.all(
|
labelTextStyle: MaterialStateProperty.all(
|
||||||
GoogleFonts.roboto(
|
GoogleFonts.roboto(
|
||||||
color: darkColorScheme?.secondary,
|
color: darkColorScheme?.onSurface,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
iconTheme: MaterialStateProperty.all(
|
|
||||||
IconThemeData(
|
|
||||||
color: darkColorScheme?.secondary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
scaffoldBackgroundColor: darkColorScheme?.background,
|
|
||||||
colorScheme: darkColorScheme?.harmonized(),
|
colorScheme: darkColorScheme?.harmonized(),
|
||||||
toggleableActiveColor: darkColorScheme?.primary,
|
|
||||||
textTheme: GoogleFonts.robotoTextTheme(ThemeData.dark().textTheme),
|
textTheme: GoogleFonts.robotoTextTheme(ThemeData.dark().textTheme),
|
||||||
);
|
);
|
||||||
return DynamicTheme(
|
return DynamicTheme(
|
||||||
themeCollection: ThemeCollection(
|
themeCollection: ThemeCollection(
|
||||||
themes: {
|
themes: {
|
||||||
0: lightCustomTheme,
|
0: brightness == Brightness.light
|
||||||
1: darkCustomTheme,
|
? lightCustomTheme
|
||||||
2: lightDynamicTheme,
|
: darkCustomTheme,
|
||||||
3: darkDynamicTheme,
|
1: brightness == Brightness.light
|
||||||
|
? lightDynamicTheme
|
||||||
|
: darkDynamicTheme,
|
||||||
|
2: lightCustomTheme,
|
||||||
|
3: lightDynamicTheme,
|
||||||
|
4: darkCustomTheme,
|
||||||
|
5: darkDynamicTheme,
|
||||||
},
|
},
|
||||||
fallbackTheme: lightCustomTheme,
|
fallbackTheme: PlatformDispatcher.instance.platformBrightness ==
|
||||||
|
Brightness.light
|
||||||
|
? lightCustomTheme
|
||||||
|
: darkCustomTheme,
|
||||||
),
|
),
|
||||||
builder: (context, theme) => MaterialApp(
|
builder: (context, theme) => MaterialApp(
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
title: title,
|
title: widget.title,
|
||||||
navigatorKey: StackedService.navigatorKey,
|
navigatorKey: StackedService.navigatorKey,
|
||||||
onGenerateRoute: StackedRouter().onGenerateRoute,
|
onGenerateRoute: StackedRouter().onGenerateRoute,
|
||||||
theme: theme,
|
theme: theme,
|
||||||
home: home,
|
home: widget.home,
|
||||||
localizationsDelegates: localizationsDelegates,
|
localizationsDelegates: widget.localizationsDelegates,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart' hide SearchBar;
|
||||||
import 'package:flutter_i18n/flutter_i18n.dart';
|
import 'package:flutter_i18n/flutter_i18n.dart';
|
||||||
import 'package:revanced_manager/ui/widgets/appSelectorView/installed_app_item.dart';
|
|
||||||
import 'package:revanced_manager/ui/widgets/shared/search_bar.dart';
|
|
||||||
import 'package:revanced_manager/ui/widgets/appSelectorView/app_skeleton_loader.dart';
|
|
||||||
import 'package:stacked/stacked.dart' hide SkeletonLoader;
|
|
||||||
import 'package:revanced_manager/ui/views/app_selector/app_selector_viewmodel.dart';
|
import 'package:revanced_manager/ui/views/app_selector/app_selector_viewmodel.dart';
|
||||||
|
import 'package:revanced_manager/ui/widgets/appSelectorView/app_skeleton_loader.dart';
|
||||||
|
import 'package:revanced_manager/ui/widgets/appSelectorView/installed_app_item.dart';
|
||||||
|
import 'package:revanced_manager/ui/widgets/appSelectorView/not_installed_app_item.dart';
|
||||||
|
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_floating_action_button_extended.dart';
|
||||||
|
import 'package:revanced_manager/ui/widgets/shared/search_bar.dart';
|
||||||
|
import 'package:stacked/stacked.dart' hide SkeletonLoader;
|
||||||
|
|
||||||
class AppSelectorView extends StatefulWidget {
|
class AppSelectorView extends StatefulWidget {
|
||||||
const AppSelectorView({Key? key}) : super(key: key);
|
const AppSelectorView({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AppSelectorView> createState() => _AppSelectorViewState();
|
State<AppSelectorView> createState() => _AppSelectorViewState();
|
||||||
@@ -19,16 +21,14 @@ class _AppSelectorViewState extends State<AppSelectorView> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ViewModelBuilder<AppSelectorViewModel>.reactive(
|
return ViewModelBuilder<AppSelectorViewModel>.reactive(
|
||||||
onModelReady: (model) => model.initialize(),
|
onViewModelReady: (model) => model.initialize(),
|
||||||
viewModelBuilder: () => AppSelectorViewModel(),
|
viewModelBuilder: () => AppSelectorViewModel(),
|
||||||
builder: (context, model, child) => Scaffold(
|
builder: (context, model, child) => Scaffold(
|
||||||
resizeToAvoidBottomInset: false,
|
floatingActionButton: HapticFloatingActionButtonExtended(
|
||||||
floatingActionButton: FloatingActionButton.extended(
|
|
||||||
label: I18nText('appSelectorView.storageButton'),
|
label: I18nText('appSelectorView.storageButton'),
|
||||||
icon: const Icon(Icons.sd_storage),
|
icon: const Icon(Icons.sd_storage),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
model.selectAppFromStorage(context);
|
model.selectAppFromStorage(context);
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
body: CustomScrollView(
|
body: CustomScrollView(
|
||||||
@@ -36,32 +36,28 @@ class _AppSelectorViewState extends State<AppSelectorView> {
|
|||||||
SliverAppBar(
|
SliverAppBar(
|
||||||
pinned: true,
|
pinned: true,
|
||||||
floating: true,
|
floating: true,
|
||||||
snap: false,
|
|
||||||
title: I18nText(
|
title: I18nText(
|
||||||
'appSelectorView.viewTitle',
|
'appSelectorView.viewTitle',
|
||||||
child: Text(
|
),
|
||||||
'',
|
titleTextStyle: TextStyle(
|
||||||
style: TextStyle(
|
fontSize: 22.0,
|
||||||
color: Theme.of(context).textTheme.headline6!.color,
|
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
Icons.arrow_back,
|
Icons.arrow_back,
|
||||||
color: Theme.of(context).textTheme.headline6!.color,
|
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||||
),
|
),
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
),
|
),
|
||||||
bottom: PreferredSize(
|
bottom: PreferredSize(
|
||||||
preferredSize: const Size.fromHeight(64.0),
|
preferredSize: const Size.fromHeight(66.0),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
vertical: 8.0,
|
vertical: 8.0,
|
||||||
horizontal: 12.0,
|
horizontal: 12.0,
|
||||||
),
|
),
|
||||||
child: SearchBar(
|
child: SearchBar(
|
||||||
showSelectIcon: false,
|
|
||||||
hintText: FlutterI18n.translate(
|
hintText: FlutterI18n.translate(
|
||||||
context,
|
context,
|
||||||
'appSelectorView.searchBarHint',
|
'appSelectorView.searchBarHint',
|
||||||
@@ -78,26 +74,66 @@ class _AppSelectorViewState extends State<AppSelectorView> {
|
|||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: model.noApps
|
child: model.noApps
|
||||||
? Center(
|
? Center(
|
||||||
child: I18nText('appSelectorCard.noAppsLabel'),
|
child: I18nText(
|
||||||
|
'appSelectorCard.noAppsLabel',
|
||||||
|
child: Text(
|
||||||
|
'',
|
||||||
|
style: TextStyle(
|
||||||
|
color:
|
||||||
|
Theme.of(context).textTheme.titleLarge!.color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: model.apps.isEmpty
|
: model.allApps.isEmpty
|
||||||
? const AppSkeletonLoader()
|
? const AppSkeletonLoader()
|
||||||
: Padding(
|
: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12.0)
|
padding: const EdgeInsets.symmetric(horizontal: 12.0)
|
||||||
.copyWith(bottom: 80),
|
.copyWith(
|
||||||
|
bottom:
|
||||||
|
MediaQuery.viewPaddingOf(context).bottom + 8.0,
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: model
|
children: [
|
||||||
.getFilteredApps(_query)
|
...model.getFilteredApps(_query).map(
|
||||||
.map((app) => InstalledAppItem(
|
(app) => InstalledAppItem(
|
||||||
name: app.appName,
|
name: app.appName,
|
||||||
pkgName: app.packageName,
|
pkgName: app.packageName,
|
||||||
icon: app.icon,
|
icon: app.icon,
|
||||||
|
patchesCount:
|
||||||
|
model.patchesCount(app.packageName),
|
||||||
|
suggestedVersion:
|
||||||
|
model.getSuggestedVersion(
|
||||||
|
app.packageName,
|
||||||
|
),
|
||||||
|
installedVersion: app.versionName!,
|
||||||
|
onTap: () => model.canSelectInstalled(
|
||||||
|
context,
|
||||||
|
app.packageName,
|
||||||
|
),
|
||||||
|
onLinkTap: () =>
|
||||||
|
model.searchSuggestedVersionOnWeb(
|
||||||
|
packageName: app.packageName,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
...model.getFilteredAppsNames(_query).map(
|
||||||
|
(app) => NotInstalledAppItem(
|
||||||
|
name: app,
|
||||||
|
patchesCount: model.patchesCount(app),
|
||||||
|
suggestedVersion:
|
||||||
|
model.getSuggestedVersion(app),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
model.selectApp(app);
|
model.showDownloadToast();
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
},
|
||||||
))
|
onLinkTap: () =>
|
||||||
.toList(),
|
model.searchSuggestedVersionOnWeb(
|
||||||
|
packageName: app,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 70.0),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,79 +1,327 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:device_apps/device_apps.dart';
|
import 'package:device_apps/device_apps.dart';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_file_dialog/flutter_file_dialog.dart';
|
||||||
|
import 'package:flutter_i18n/flutter_i18n.dart';
|
||||||
import 'package:revanced_manager/app/app.locator.dart';
|
import 'package:revanced_manager/app/app.locator.dart';
|
||||||
|
import 'package:revanced_manager/models/patch.dart';
|
||||||
import 'package:revanced_manager/models/patched_application.dart';
|
import 'package:revanced_manager/models/patched_application.dart';
|
||||||
|
import 'package:revanced_manager/services/manager_api.dart';
|
||||||
import 'package:revanced_manager/services/patcher_api.dart';
|
import 'package:revanced_manager/services/patcher_api.dart';
|
||||||
import 'package:revanced_manager/services/toast.dart';
|
import 'package:revanced_manager/services/toast.dart';
|
||||||
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
||||||
|
import 'package:revanced_manager/utils/check_for_supported_patch.dart';
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
|
|
||||||
class AppSelectorViewModel extends BaseViewModel {
|
class AppSelectorViewModel extends BaseViewModel {
|
||||||
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
|
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
|
||||||
|
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||||
final Toast _toast = locator<Toast>();
|
final Toast _toast = locator<Toast>();
|
||||||
final List<ApplicationWithIcon> apps = [];
|
final List<ApplicationWithIcon> apps = [];
|
||||||
|
List<String> allApps = [];
|
||||||
bool noApps = false;
|
bool noApps = false;
|
||||||
|
bool isRooted = false;
|
||||||
|
int patchesCount(String packageName) {
|
||||||
|
return _patcherAPI.getFilteredPatches(packageName).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Patch> patches = [];
|
||||||
|
|
||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
apps.addAll(await _patcherAPI.getFilteredInstalledApps());
|
patches = await _managerAPI.getPatches();
|
||||||
apps.sort((a, b) => a.appName.compareTo(b.appName));
|
isRooted = _managerAPI.isRooted;
|
||||||
noApps = apps.isEmpty;
|
|
||||||
|
apps.addAll(
|
||||||
|
await _patcherAPI
|
||||||
|
.getFilteredInstalledApps(_managerAPI.areUniversalPatchesEnabled()),
|
||||||
|
);
|
||||||
|
apps.sort(
|
||||||
|
(a, b) => _patcherAPI
|
||||||
|
.getFilteredPatches(b.packageName)
|
||||||
|
.length
|
||||||
|
.compareTo(_patcherAPI.getFilteredPatches(a.packageName).length),
|
||||||
|
);
|
||||||
|
getAllApps();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void selectApp(ApplicationWithIcon application) async {
|
List<String> getAllApps() {
|
||||||
|
allApps = patches
|
||||||
|
.expand((e) => e.compatiblePackages.map((p) => p.name))
|
||||||
|
.toSet()
|
||||||
|
.where((name) => !apps.any((app) => app.packageName == name))
|
||||||
|
.toList();
|
||||||
|
noApps = allApps.isEmpty;
|
||||||
|
return allApps;
|
||||||
|
}
|
||||||
|
|
||||||
|
String getSuggestedVersion(String packageName) {
|
||||||
|
return _patcherAPI.getSuggestedVersion(packageName);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> checkSplitApk(String packageName) async {
|
||||||
|
final app = await DeviceApps.getApp(packageName);
|
||||||
|
if (app != null) {
|
||||||
|
return app.isSplit;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> searchSuggestedVersionOnWeb({
|
||||||
|
required String packageName,
|
||||||
|
}) async {
|
||||||
|
final String suggestedVersion = getSuggestedVersion(packageName);
|
||||||
|
|
||||||
|
if (suggestedVersion.isNotEmpty) {
|
||||||
|
await openDefaultBrowser('$packageName apk version $suggestedVersion');
|
||||||
|
} else {
|
||||||
|
await openDefaultBrowser('$packageName apk');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> openDefaultBrowser(String query) async {
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
try {
|
||||||
|
const platform = MethodChannel('app.revanced.manager.flutter/browser');
|
||||||
|
await platform.invokeMethod('openBrowser', {'query': query});
|
||||||
|
} catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw 'Platform not supported';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> selectApp(
|
||||||
|
BuildContext context,
|
||||||
|
ApplicationWithIcon application, [
|
||||||
|
bool isFromStorage = false,
|
||||||
|
]) async {
|
||||||
|
final String suggestedVersion =
|
||||||
|
getSuggestedVersion(application.packageName);
|
||||||
|
if (application.versionName != suggestedVersion &&
|
||||||
|
suggestedVersion.isNotEmpty) {
|
||||||
|
_managerAPI.suggestedAppVersionSelected = false;
|
||||||
|
if (_managerAPI.isRequireSuggestedAppVersionEnabled() &&
|
||||||
|
context.mounted) {
|
||||||
|
return showRequireSuggestedAppVersionDialog(
|
||||||
|
context,
|
||||||
|
application.versionName!,
|
||||||
|
suggestedVersion,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_managerAPI.suggestedAppVersionSelected = true;
|
||||||
|
}
|
||||||
locator<PatcherViewModel>().selectedApp = PatchedApplication(
|
locator<PatcherViewModel>().selectedApp = PatchedApplication(
|
||||||
name: application.appName,
|
name: application.appName,
|
||||||
packageName: application.packageName,
|
packageName: application.packageName,
|
||||||
originalPackageName: application.packageName,
|
|
||||||
version: application.versionName!,
|
version: application.versionName!,
|
||||||
apkFilePath: application.apkFilePath,
|
apkFilePath: application.apkFilePath,
|
||||||
icon: application.icon,
|
icon: application.icon,
|
||||||
patchDate: DateTime.now(),
|
patchDate: DateTime.now(),
|
||||||
|
isFromStorage: isFromStorage,
|
||||||
|
);
|
||||||
|
await locator<PatcherViewModel>().loadLastSelectedPatches();
|
||||||
|
if (context.mounted) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> canSelectInstalled(
|
||||||
|
BuildContext context,
|
||||||
|
String packageName,
|
||||||
|
) async {
|
||||||
|
final app =
|
||||||
|
await DeviceApps.getApp(packageName, true) as ApplicationWithIcon?;
|
||||||
|
if (app != null) {
|
||||||
|
final bool isSplitApk = await checkSplitApk(packageName);
|
||||||
|
if (isRooted || !isSplitApk) {
|
||||||
|
if (context.mounted) {
|
||||||
|
await selectApp(context, app);
|
||||||
|
}
|
||||||
|
final List<Option> requiredNullOptions = getNullRequiredOptions(
|
||||||
|
locator<PatcherViewModel>().selectedPatches,
|
||||||
|
packageName,
|
||||||
|
);
|
||||||
|
if (requiredNullOptions.isNotEmpty) {
|
||||||
|
locator<PatcherViewModel>().showRequiredOptionDialog();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (context.mounted) {
|
||||||
|
return showSelectFromStorageDialog(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future showRequireSuggestedAppVersionDialog(
|
||||||
|
BuildContext context,
|
||||||
|
String selectedVersion,
|
||||||
|
String suggestedVersion,
|
||||||
|
) async {
|
||||||
|
return showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: I18nText('warning'),
|
||||||
|
content: I18nText(
|
||||||
|
'appSelectorView.requireSuggestedAppVersionDialogText',
|
||||||
|
translationParams: {
|
||||||
|
'suggested': suggestedVersion,
|
||||||
|
'selected': selectedVersion,
|
||||||
|
},
|
||||||
|
child: const Text(
|
||||||
|
'',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: I18nText('okButton'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future showSelectFromStorageDialog(BuildContext context) async {
|
||||||
|
return showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (innerContext) => SimpleDialog(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
contentPadding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 20, vertical: 20),
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Icon(
|
||||||
|
Icons.block,
|
||||||
|
size: 28,
|
||||||
|
color: Theme.of(innerContext).colorScheme.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
I18nText(
|
||||||
|
'appSelectorView.featureNotAvailable',
|
||||||
|
child: const Text(
|
||||||
|
'',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
wordSpacing: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
I18nText(
|
||||||
|
'appSelectorView.featureNotAvailableText',
|
||||||
|
child: const Text(
|
||||||
|
'',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () async {
|
||||||
|
Navigator.pop(innerContext);
|
||||||
|
await selectAppFromStorage(context);
|
||||||
|
},
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.sd_card),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
I18nText('appSelectorView.selectFromStorageButton'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(innerContext);
|
||||||
|
},
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
I18nText('cancelButton'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
locator<PatcherViewModel>().selectedPatches.clear();
|
|
||||||
locator<PatcherViewModel>().notifyListeners();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> selectAppFromStorage(BuildContext context) async {
|
Future<void> selectAppFromStorage(BuildContext context) async {
|
||||||
try {
|
try {
|
||||||
FilePickerResult? result = await FilePicker.platform.pickFiles(
|
final String? result = await FlutterFileDialog.pickFile(
|
||||||
type: FileType.custom,
|
params: const OpenFileDialogParams(
|
||||||
allowedExtensions: ['apk'],
|
fileExtensionsFilter: ['apk'],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
if (result != null && result.files.single.path != null) {
|
if (result != null) {
|
||||||
File apkFile = File(result.files.single.path!);
|
final File apkFile = File(result);
|
||||||
ApplicationWithIcon? application = await DeviceApps.getAppFromStorage(
|
final List<String> pathSplit = result.split('/');
|
||||||
|
pathSplit.removeLast();
|
||||||
|
final Directory filePickerCacheDir = Directory(pathSplit.join('/'));
|
||||||
|
final Iterable<File> deletableFiles =
|
||||||
|
(await filePickerCacheDir.list().toList()).whereType<File>();
|
||||||
|
for (final file in deletableFiles) {
|
||||||
|
if (file.path != apkFile.path && file.path.endsWith('.apk')) {
|
||||||
|
file.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final ApplicationWithIcon? application =
|
||||||
|
await DeviceApps.getAppFromStorage(
|
||||||
apkFile.path,
|
apkFile.path,
|
||||||
true,
|
true,
|
||||||
) as ApplicationWithIcon?;
|
) as ApplicationWithIcon?;
|
||||||
if (application != null) {
|
if (application != null && context.mounted) {
|
||||||
locator<PatcherViewModel>().selectedApp = PatchedApplication(
|
await selectApp(context, application, true);
|
||||||
name: application.appName,
|
|
||||||
packageName: application.packageName,
|
|
||||||
originalPackageName: application.packageName,
|
|
||||||
version: application.versionName!,
|
|
||||||
apkFilePath: result.files.single.path!,
|
|
||||||
icon: application.icon,
|
|
||||||
patchDate: DateTime.now(),
|
|
||||||
isFromStorage: true,
|
|
||||||
);
|
|
||||||
locator<PatcherViewModel>().selectedPatches.clear();
|
|
||||||
locator<PatcherViewModel>().notifyListeners();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} on Exception {
|
} on Exception catch (e) {
|
||||||
_toast.show('appSelectorView.errorMessage');
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
|
_toast.showBottom('appSelectorView.errorMessage');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<ApplicationWithIcon> getFilteredApps(String query) {
|
List<ApplicationWithIcon> getFilteredApps(String query) {
|
||||||
return apps
|
return apps
|
||||||
.where((app) =>
|
.where(
|
||||||
query.isEmpty ||
|
(app) =>
|
||||||
query.length < 2 ||
|
query.isEmpty ||
|
||||||
app.appName.toLowerCase().contains(query.toLowerCase()))
|
query.length < 2 ||
|
||||||
|
app.appName.toLowerCase().contains(query.toLowerCase()) ||
|
||||||
|
app.packageName.toLowerCase().contains(query.toLowerCase()),
|
||||||
|
)
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<String> getFilteredAppsNames(String query) {
|
||||||
|
return allApps
|
||||||
|
.where(
|
||||||
|
(app) =>
|
||||||
|
query.isEmpty ||
|
||||||
|
query.length < 2 ||
|
||||||
|
app.toLowerCase().contains(query.toLowerCase()),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
void showDownloadToast() =>
|
||||||
|
_toast.showBottom('appSelectorView.downloadToast');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart';
|
|||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
|
|
||||||
class ContributorsView extends StatelessWidget {
|
class ContributorsView extends StatelessWidget {
|
||||||
const ContributorsView({Key? key}) : super(key: key);
|
const ContributorsView({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ViewModelBuilder<ContributorsViewModel>.reactive(
|
return ViewModelBuilder<ContributorsViewModel>.reactive(
|
||||||
viewModelBuilder: () => ContributorsViewModel(),
|
viewModelBuilder: () => ContributorsViewModel(),
|
||||||
onModelReady: (model) => model.getContributors(),
|
onViewModelReady: (model) => model.getContributors(),
|
||||||
builder: (context, model, child) => Scaffold(
|
builder: (context, model, child) => Scaffold(
|
||||||
body: CustomScrollView(
|
body: CustomScrollView(
|
||||||
slivers: <Widget>[
|
slivers: <Widget>[
|
||||||
@@ -23,7 +23,7 @@ class ContributorsView extends StatelessWidget {
|
|||||||
child: Text(
|
child: Text(
|
||||||
'',
|
'',
|
||||||
style: GoogleFonts.inter(
|
style: GoogleFonts.inter(
|
||||||
color: Theme.of(context).textTheme.headline6!.color,
|
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -57,6 +57,7 @@ class ContributorsView extends StatelessWidget {
|
|||||||
title: 'contributorsView.managerContributors',
|
title: 'contributorsView.managerContributors',
|
||||||
contributors: model.managerContributors,
|
contributors: model.managerContributors,
|
||||||
),
|
),
|
||||||
|
SizedBox(height: MediaQuery.viewPaddingOf(context).bottom),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ class ContributorsViewModel extends BaseViewModel {
|
|||||||
List<dynamic> managerContributors = [];
|
List<dynamic> managerContributors = [];
|
||||||
|
|
||||||
Future<void> getContributors() async {
|
Future<void> getContributors() async {
|
||||||
Map<String, List<dynamic>> contributors =
|
final Map<String, List<dynamic>> contributors =
|
||||||
await _managerAPI.getContributors();
|
await _managerAPI.getContributors();
|
||||||
patcherContributors = contributors[_managerAPI.defaultPatcherRepo] ?? [];
|
patcherContributors = contributors[_managerAPI.defaultPatcherRepo] ?? [];
|
||||||
patchesContributors = contributors[_managerAPI.getPatchesRepo()] ?? [];
|
patchesContributors = contributors[_managerAPI.getPatchesRepo().toLowerCase()] ?? [];
|
||||||
integrationsContributors =
|
integrationsContributors =
|
||||||
contributors[_managerAPI.getIntegrationsRepo()] ?? [];
|
contributors[_managerAPI.getIntegrationsRepo().toLowerCase()] ?? [];
|
||||||
cliContributors = contributors[_managerAPI.defaultCliRepo] ?? [];
|
cliContributors = contributors[_managerAPI.defaultCliRepo] ?? [];
|
||||||
managerContributors = contributors[_managerAPI.defaultManagerRepo] ?? [];
|
managerContributors = contributors[_managerAPI.defaultManagerRepo] ?? [];
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|||||||
@@ -1,29 +1,25 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_i18n/flutter_i18n.dart';
|
import 'package:flutter_i18n/flutter_i18n.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:animations/animations.dart';
|
|
||||||
import 'package:revanced_manager/app/app.locator.dart';
|
import 'package:revanced_manager/app/app.locator.dart';
|
||||||
import 'package:revanced_manager/ui/views/home/home_viewmodel.dart';
|
import 'package:revanced_manager/ui/views/home/home_viewmodel.dart';
|
||||||
import 'package:revanced_manager/ui/widgets/homeView/available_updates_card.dart';
|
|
||||||
import 'package:revanced_manager/ui/widgets/homeView/installed_apps_card.dart';
|
import 'package:revanced_manager/ui/widgets/homeView/installed_apps_card.dart';
|
||||||
import 'package:revanced_manager/ui/widgets/homeView/latest_commit_card.dart';
|
import 'package:revanced_manager/ui/widgets/homeView/latest_commit_card.dart';
|
||||||
import 'package:revanced_manager/ui/widgets/shared/custom_chip.dart';
|
|
||||||
import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart';
|
import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart';
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
|
|
||||||
class HomeView extends StatelessWidget {
|
class HomeView extends StatelessWidget {
|
||||||
const HomeView({Key? key}) : super(key: key);
|
const HomeView({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ViewModelBuilder<HomeViewModel>.reactive(
|
return ViewModelBuilder<HomeViewModel>.reactive(
|
||||||
disposeViewModel: false,
|
disposeViewModel: false,
|
||||||
onModelReady: (model) => model.initialize(context),
|
fireOnViewModelReadyOnce: true,
|
||||||
|
onViewModelReady: (model) => model.initialize(context),
|
||||||
viewModelBuilder: () => locator<HomeViewModel>(),
|
viewModelBuilder: () => locator<HomeViewModel>(),
|
||||||
builder: (context, model, child) => Scaffold(
|
builder: (context, model, child) => Scaffold(
|
||||||
body: RefreshIndicator(
|
body: RefreshIndicator(
|
||||||
color: Theme.of(context).colorScheme.secondary,
|
|
||||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
|
||||||
onRefresh: () => model.forceRefresh(context),
|
onRefresh: () => model.forceRefresh(context),
|
||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
slivers: <Widget>[
|
slivers: <Widget>[
|
||||||
@@ -34,7 +30,7 @@ class HomeView extends StatelessWidget {
|
|||||||
child: Text(
|
child: Text(
|
||||||
'',
|
'',
|
||||||
style: GoogleFonts.inter(
|
style: GoogleFonts.inter(
|
||||||
color: Theme.of(context).textTheme.headline6!.color,
|
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -48,63 +44,21 @@ class HomeView extends StatelessWidget {
|
|||||||
'homeView.updatesSubtitle',
|
'homeView.updatesSubtitle',
|
||||||
child: Text(
|
child: Text(
|
||||||
'',
|
'',
|
||||||
style: Theme.of(context).textTheme.headline6!,
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
LatestCommitCard(
|
LatestCommitCard(model: model, parentContext: context),
|
||||||
onPressed: () =>
|
|
||||||
model.showUpdateConfirmationDialog(context),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 23),
|
const SizedBox(height: 23),
|
||||||
I18nText(
|
I18nText(
|
||||||
'homeView.patchedSubtitle',
|
'homeView.patchedSubtitle',
|
||||||
child: Text(
|
child: Text(
|
||||||
'',
|
'',
|
||||||
style: Theme.of(context).textTheme.headline6!,
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 10),
|
||||||
Row(
|
InstalledAppsCard(),
|
||||||
children: <Widget>[
|
|
||||||
DashboardChip(
|
|
||||||
label: I18nText('homeView.updatesAvailable'),
|
|
||||||
isSelected: model.showUpdatableApps,
|
|
||||||
onSelected: (value) {
|
|
||||||
model.toggleUpdatableApps(true);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(width: 10),
|
|
||||||
DashboardChip(
|
|
||||||
label: I18nText('homeView.installed'),
|
|
||||||
isSelected: !model.showUpdatableApps,
|
|
||||||
onSelected: (value) {
|
|
||||||
model.toggleUpdatableApps(false);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 14),
|
|
||||||
PageTransitionSwitcher(
|
|
||||||
transitionBuilder:
|
|
||||||
(child, primaryAnimation, secondaryAnimation) {
|
|
||||||
return FadeThroughTransition(
|
|
||||||
animation: primaryAnimation,
|
|
||||||
secondaryAnimation: secondaryAnimation,
|
|
||||||
fillColor: Colors.transparent,
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
layoutBuilder: (entries) {
|
|
||||||
return Stack(
|
|
||||||
alignment: Alignment.topCenter,
|
|
||||||
children: entries,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: model.showUpdatableApps
|
|
||||||
? AvailableUpdatesCard()
|
|
||||||
: InstalledAppsCard(),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,55 +1,91 @@
|
|||||||
// ignore_for_file: use_build_context_synchronously
|
// ignore_for_file: use_build_context_synchronously
|
||||||
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:app_installer/app_installer.dart';
|
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||||
import 'package:cross_connectivity/cross_connectivity.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:device_apps/device_apps.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_i18n/flutter_i18n.dart';
|
import 'package:flutter_i18n/flutter_i18n.dart';
|
||||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
import 'package:injectable/injectable.dart';
|
import 'package:injectable/injectable.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:revanced_manager/app/app.locator.dart';
|
import 'package:revanced_manager/app/app.locator.dart';
|
||||||
import 'package:revanced_manager/app/app.router.dart';
|
import 'package:revanced_manager/app/app.router.dart';
|
||||||
import 'package:revanced_manager/models/patched_application.dart';
|
import 'package:revanced_manager/models/patched_application.dart';
|
||||||
|
import 'package:revanced_manager/services/github_api.dart';
|
||||||
import 'package:revanced_manager/services/manager_api.dart';
|
import 'package:revanced_manager/services/manager_api.dart';
|
||||||
import 'package:revanced_manager/services/patcher_api.dart';
|
import 'package:revanced_manager/services/patcher_api.dart';
|
||||||
|
import 'package:revanced_manager/services/revanced_api.dart';
|
||||||
import 'package:revanced_manager/services/toast.dart';
|
import 'package:revanced_manager/services/toast.dart';
|
||||||
import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart';
|
import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart';
|
||||||
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
||||||
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
import 'package:revanced_manager/ui/widgets/homeView/update_confirmation_dialog.dart';
|
||||||
|
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_checkbox_list_tile.dart';
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
import 'package:stacked_services/stacked_services.dart';
|
import 'package:stacked_services/stacked_services.dart';
|
||||||
import 'package:timezone/timezone.dart' as tz;
|
|
||||||
|
|
||||||
@lazySingleton
|
@lazySingleton
|
||||||
class HomeViewModel extends BaseViewModel {
|
class HomeViewModel extends BaseViewModel {
|
||||||
final NavigationService _navigationService = locator<NavigationService>();
|
final NavigationService _navigationService = locator<NavigationService>();
|
||||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||||
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
|
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
|
||||||
|
final GithubAPI _githubAPI = locator<GithubAPI>();
|
||||||
|
final RevancedAPI _revancedAPI = locator<RevancedAPI>();
|
||||||
final Toast _toast = locator<Toast>();
|
final Toast _toast = locator<Toast>();
|
||||||
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
|
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
|
||||||
DateTime? _lastUpdate;
|
bool showUpdatableApps = false;
|
||||||
bool showUpdatableApps = true;
|
|
||||||
List<PatchedApplication> patchedInstalledApps = [];
|
List<PatchedApplication> patchedInstalledApps = [];
|
||||||
List<PatchedApplication> patchedUpdatableApps = [];
|
String? _latestManagerVersion = '';
|
||||||
|
File? downloadedApk;
|
||||||
|
|
||||||
Future<void> initialize(BuildContext context) async {
|
Future<void> initialize(BuildContext context) async {
|
||||||
|
_managerAPI.rePatchedSavedApps().then((_) => _getPatchedApps());
|
||||||
|
|
||||||
|
_latestManagerVersion = await _managerAPI.getLatestManagerVersion();
|
||||||
|
if (!_managerAPI.getPatchesConsent()) {
|
||||||
|
await showPatchesConsent(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _patcherAPI.initialize();
|
||||||
|
|
||||||
await flutterLocalNotificationsPlugin.initialize(
|
await flutterLocalNotificationsPlugin.initialize(
|
||||||
const InitializationSettings(
|
const InitializationSettings(
|
||||||
android: AndroidInitializationSettings('ic_notification'),
|
android: AndroidInitializationSettings('ic_notification'),
|
||||||
),
|
),
|
||||||
onSelectNotification: (p) =>
|
onDidReceiveNotificationResponse: (response) async {
|
||||||
DeviceApps.openApp('app.revanced.manager.flutter'),
|
if (response.id == 0) {
|
||||||
|
_toast.showBottom('homeView.installingMessage');
|
||||||
|
final File? managerApk = await _managerAPI.downloadManager();
|
||||||
|
if (managerApk != null) {
|
||||||
|
await _patcherAPI.installApk(context, managerApk.path);
|
||||||
|
} else {
|
||||||
|
_toast.showBottom('homeView.errorDownloadMessage');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
flutterLocalNotificationsPlugin
|
flutterLocalNotificationsPlugin
|
||||||
.resolvePlatformSpecificImplementation<
|
.resolvePlatformSpecificImplementation<
|
||||||
AndroidFlutterLocalNotificationsPlugin>()
|
AndroidFlutterLocalNotificationsPlugin>()
|
||||||
?.requestPermission();
|
?.requestNotificationsPermission();
|
||||||
bool isConnected = await Connectivity().checkConnection();
|
|
||||||
|
final bool isConnected =
|
||||||
|
await Connectivity().checkConnectivity() != ConnectivityResult.none;
|
||||||
if (!isConnected) {
|
if (!isConnected) {
|
||||||
_toast.show('homeView.noConnection');
|
_toast.showBottom('homeView.noConnection');
|
||||||
|
}
|
||||||
|
|
||||||
|
final NotificationAppLaunchDetails? notificationAppLaunchDetails =
|
||||||
|
await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails();
|
||||||
|
if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? false) {
|
||||||
|
_toast.showBottom('homeView.installingMessage');
|
||||||
|
final File? managerApk = await _managerAPI.downloadManager();
|
||||||
|
if (managerApk != null) {
|
||||||
|
await _patcherAPI.installApk(context, managerApk.path);
|
||||||
|
} else {
|
||||||
|
_toast.showBottom('homeView.errorDownloadMessage');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_getPatchedApps();
|
|
||||||
_managerAPI.reAssessSavedApps().then((_) => _getPatchedApps());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void navigateToAppInfo(PatchedApplication app) {
|
void navigateToAppInfo(PatchedApplication app) {
|
||||||
@@ -64,7 +100,7 @@ class HomeViewModel extends BaseViewModel {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void navigateToPatcher(PatchedApplication app) async {
|
Future<void> navigateToPatcher(PatchedApplication app) async {
|
||||||
locator<PatcherViewModel>().selectedApp = app;
|
locator<PatcherViewModel>().selectedApp = app;
|
||||||
locator<PatcherViewModel>().selectedPatches =
|
locator<PatcherViewModel>().selectedPatches =
|
||||||
await _patcherAPI.getAppliedPatches(app.appliedPatches);
|
await _patcherAPI.getAppliedPatches(app.appliedPatches);
|
||||||
@@ -73,112 +109,366 @@ class HomeViewModel extends BaseViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _getPatchedApps() {
|
void _getPatchedApps() {
|
||||||
patchedInstalledApps = _managerAPI
|
patchedInstalledApps = _managerAPI.getPatchedApps().toList();
|
||||||
.getPatchedApps()
|
|
||||||
.where((app) => app.hasUpdates == false)
|
|
||||||
.toList();
|
|
||||||
patchedUpdatableApps = _managerAPI
|
|
||||||
.getPatchedApps()
|
|
||||||
.where((app) => app.hasUpdates == true)
|
|
||||||
.toList();
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> hasManagerUpdates() async {
|
Future<bool> hasManagerUpdates() async {
|
||||||
String? latestVersion = await _managerAPI.getLatestManagerVersion();
|
|
||||||
String currentVersion = await _managerAPI.getCurrentManagerVersion();
|
String currentVersion = await _managerAPI.getCurrentManagerVersion();
|
||||||
|
|
||||||
|
// add v to current version
|
||||||
|
if (!currentVersion.startsWith('v')) {
|
||||||
|
currentVersion = 'v$currentVersion';
|
||||||
|
}
|
||||||
|
|
||||||
|
_latestManagerVersion =
|
||||||
|
await _managerAPI.getLatestManagerVersion() ?? currentVersion;
|
||||||
|
|
||||||
|
if (_latestManagerVersion != currentVersion) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> hasPatchesUpdates() async {
|
||||||
|
final String? latestVersion = await _managerAPI.getLatestPatchesVersion();
|
||||||
|
final String currentVersion = await _managerAPI.getCurrentPatchesVersion();
|
||||||
if (latestVersion != null) {
|
if (latestVersion != null) {
|
||||||
try {
|
try {
|
||||||
int latestVersionInt =
|
final int latestVersionInt =
|
||||||
int.parse(latestVersion.replaceAll(RegExp('[^0-9]'), ''));
|
int.parse(latestVersion.replaceAll(RegExp('[^0-9]'), ''));
|
||||||
int currentVersionInt =
|
final int currentVersionInt =
|
||||||
int.parse(currentVersion.replaceAll(RegExp('[^0-9]'), ''));
|
int.parse(currentVersion.replaceAll(RegExp('[^0-9]'), ''));
|
||||||
return latestVersionInt > currentVersionInt;
|
return latestVersionInt > currentVersionInt;
|
||||||
} on Exception {
|
} on Exception catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateManager(BuildContext context) async {
|
Future<File?> downloadManager() async {
|
||||||
try {
|
try {
|
||||||
_toast.show('homeView.downloadingMessage');
|
final response = await _revancedAPI.downloadManager();
|
||||||
File? managerApk = await _managerAPI.downloadManager();
|
final bytes = await response!.readAsBytes();
|
||||||
if (managerApk != null) {
|
final tempDir = await getTemporaryDirectory();
|
||||||
await flutterLocalNotificationsPlugin.zonedSchedule(
|
final tempFile = File('${tempDir.path}/revanced-manager.apk');
|
||||||
0,
|
await tempFile.writeAsBytes(bytes);
|
||||||
FlutterI18n.translate(
|
return tempFile;
|
||||||
context,
|
} on Exception catch (e) {
|
||||||
'homeView.notificationTitle',
|
if (kDebugMode) {
|
||||||
),
|
print(e);
|
||||||
FlutterI18n.translate(
|
|
||||||
context,
|
|
||||||
'homeView.notificationText',
|
|
||||||
),
|
|
||||||
tz.TZDateTime.now(tz.local).add(const Duration(seconds: 5)),
|
|
||||||
const NotificationDetails(
|
|
||||||
android: AndroidNotificationDetails(
|
|
||||||
'revanced_manager_channel',
|
|
||||||
'ReVanced Manager Channel',
|
|
||||||
importance: Importance.max,
|
|
||||||
priority: Priority.high,
|
|
||||||
ticker: 'ticker',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
androidAllowWhileIdle: true,
|
|
||||||
uiLocalNotificationDateInterpretation:
|
|
||||||
UILocalNotificationDateInterpretation.absoluteTime,
|
|
||||||
);
|
|
||||||
_toast.show('homeView.installingMessage');
|
|
||||||
await AppInstaller.installApk(managerApk.path);
|
|
||||||
} else {
|
|
||||||
_toast.show('homeView.errorDownloadMessage');
|
|
||||||
}
|
}
|
||||||
} on Exception {
|
return null;
|
||||||
_toast.show('homeView.errorInstallMessage');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> showUpdateConfirmationDialog(BuildContext parentContext) async {
|
Future<void> showPatchesConsent(BuildContext context) async {
|
||||||
return showDialog(
|
final ValueNotifier<bool> autoUpdate = ValueNotifier(true);
|
||||||
context: parentContext,
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: I18nText('homeView.updateDialogTitle'),
|
title: const Text('Download ReVanced Patches?'),
|
||||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
content: ValueListenableBuilder(
|
||||||
content: I18nText('homeView.updateDialogText'),
|
valueListenable: autoUpdate,
|
||||||
actions: <Widget>[
|
builder: (context, value, child) {
|
||||||
CustomMaterialButton(
|
return Column(
|
||||||
isFilled: false,
|
mainAxisSize: MainAxisSize.min,
|
||||||
label: I18nText('noButton'),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
children: [
|
||||||
),
|
I18nText(
|
||||||
CustomMaterialButton(
|
'homeView.patchesConsentDialogText',
|
||||||
label: I18nText('yesButton'),
|
child: Text(
|
||||||
onPressed: () {
|
'',
|
||||||
Navigator.of(context).pop();
|
style: TextStyle(
|
||||||
updateManager(parentContext);
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||||
|
child: I18nText(
|
||||||
|
'homeView.patchesConsentDialogText2',
|
||||||
|
translationParams: {
|
||||||
|
'url': _managerAPI.defaultApiUrl.split('/')[2],
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
'',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Theme.of(context).colorScheme.error,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
HapticCheckboxListTile(
|
||||||
|
value: value,
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
title: I18nText(
|
||||||
|
'homeView.patchesConsentDialogText3',
|
||||||
|
),
|
||||||
|
subtitle: I18nText(
|
||||||
|
'homeView.patchesConsentDialogText3Sub',
|
||||||
|
),
|
||||||
|
onChanged: (selected) {
|
||||||
|
autoUpdate.value = selected!;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await _managerAPI.setPatchesConsent(false);
|
||||||
|
SystemNavigator.pop();
|
||||||
},
|
},
|
||||||
)
|
child: I18nText('quitButton'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await _managerAPI.setPatchesConsent(true);
|
||||||
|
await _managerAPI.setPatchesAutoUpdate(autoUpdate.value);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: I18nText('okButton'),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> getLatestPatcherReleaseTime() async {
|
Future<void> updatePatches(BuildContext context) async {
|
||||||
return _managerAPI.getLatestPatcherReleaseTime();
|
_toast.showBottom('homeView.downloadingMessage');
|
||||||
|
final String patchesVersion =
|
||||||
|
await _managerAPI.getLatestPatchesVersion() ?? '0.0.0';
|
||||||
|
final String integrationsVersion =
|
||||||
|
await _managerAPI.getLatestIntegrationsVersion() ?? '0.0.0';
|
||||||
|
if (patchesVersion != '0.0.0' && integrationsVersion != '0.0.0') {
|
||||||
|
await _managerAPI.setCurrentPatchesVersion(patchesVersion);
|
||||||
|
await _managerAPI.setCurrentIntegrationsVersion(integrationsVersion);
|
||||||
|
_toast.showBottom('homeView.downloadedMessage');
|
||||||
|
forceRefresh(context);
|
||||||
|
} else {
|
||||||
|
_toast.showBottom('homeView.errorDownloadMessage');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> getLatestManagerReleaseTime() async {
|
Future<void> updateManager(BuildContext context) async {
|
||||||
|
final ValueNotifier<bool> downloaded = ValueNotifier(false);
|
||||||
|
try {
|
||||||
|
_toast.showBottom('homeView.downloadingMessage');
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => ValueListenableBuilder(
|
||||||
|
valueListenable: downloaded,
|
||||||
|
builder: (context, value, child) {
|
||||||
|
return SimpleDialog(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
|
contentPadding: const EdgeInsets.all(16.0),
|
||||||
|
title: I18nText(
|
||||||
|
!value
|
||||||
|
? 'homeView.downloadingMessage'
|
||||||
|
: 'homeView.downloadedMessage',
|
||||||
|
child: Text(
|
||||||
|
'',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.new_releases_outlined,
|
||||||
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8.0),
|
||||||
|
Text(
|
||||||
|
'$_latestManagerVersion',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16.0),
|
||||||
|
if (!value)
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
StreamBuilder<double>(
|
||||||
|
initialData: 0.0,
|
||||||
|
stream: _revancedAPI.managerUpdateProgress.stream,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
return LinearProgressIndicator(
|
||||||
|
value: snapshot.data! * 0.01,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
|
Theme.of(context).colorScheme.secondary,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16.0),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: FilledButton(
|
||||||
|
onPressed: () {
|
||||||
|
_revancedAPI.disposeManagerUpdateProgress();
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: I18nText('cancelButton'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (value)
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
I18nText(
|
||||||
|
'homeView.installUpdate',
|
||||||
|
child: Text(
|
||||||
|
'',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16.0),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: I18nText('cancelButton'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8.0),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: FilledButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await _patcherAPI.installApk(
|
||||||
|
context,
|
||||||
|
downloadedApk!.path,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: I18nText('updateButton'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final File? managerApk = await downloadManager();
|
||||||
|
if (managerApk != null) {
|
||||||
|
downloaded.value = true;
|
||||||
|
downloadedApk = managerApk;
|
||||||
|
// await flutterLocalNotificationsPlugin.zonedSchedule(
|
||||||
|
// 0,
|
||||||
|
// FlutterI18n.translate(
|
||||||
|
// context,
|
||||||
|
// 'homeView.notificationTitle',
|
||||||
|
// ),
|
||||||
|
// FlutterI18n.translate(
|
||||||
|
// context,
|
||||||
|
// 'homeView.notificationText',
|
||||||
|
// ),
|
||||||
|
// tz.TZDateTime.now(tz.local).add(const Duration(seconds: 5)),
|
||||||
|
// const NotificationDetails(
|
||||||
|
// android: AndroidNotificationDetails(
|
||||||
|
// 'revanced_manager_channel',
|
||||||
|
// 'ReVanced Manager Channel',
|
||||||
|
// importance: Importance.max,
|
||||||
|
// priority: Priority.high,
|
||||||
|
// ticker: 'ticker',
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// androidAllowWhileIdle: true,
|
||||||
|
// uiLocalNotificationDateInterpretation:
|
||||||
|
// UILocalNotificationDateInterpretation.absoluteTime,
|
||||||
|
// );
|
||||||
|
_toast.showBottom('homeView.installingMessage');
|
||||||
|
await _patcherAPI.installApk(context, managerApk.path);
|
||||||
|
} else {
|
||||||
|
_toast.showBottom('homeView.errorDownloadMessage');
|
||||||
|
}
|
||||||
|
} on Exception catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
|
_toast.showBottom('homeView.errorInstallMessage');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updatesAreDisabled() {
|
||||||
|
_toast.showBottom('homeView.updatesDisabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> showUpdateConfirmationDialog(
|
||||||
|
BuildContext parentContext,
|
||||||
|
bool isPatches,
|
||||||
|
) {
|
||||||
|
return showModalBottomSheet(
|
||||||
|
context: parentContext,
|
||||||
|
isScrollControlled: true,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(24.0)),
|
||||||
|
),
|
||||||
|
builder: (context) => UpdateConfirmationDialog(
|
||||||
|
isPatches: isPatches,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>?> getLatestManagerRelease() {
|
||||||
|
return _githubAPI.getLatestManagerRelease(_managerAPI.defaultManagerRepo);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>?> getLatestPatchesRelease() {
|
||||||
|
return _githubAPI.getLatestPatchesRelease(_managerAPI.defaultPatchesRepo);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> getLatestPatchesReleaseTime() {
|
||||||
|
return _managerAPI.getLatestPatchesReleaseTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> getLatestManagerReleaseTime() {
|
||||||
return _managerAPI.getLatestManagerReleaseTime();
|
return _managerAPI.getLatestManagerReleaseTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> forceRefresh(BuildContext context) async {
|
Future<void> forceRefresh(BuildContext context) async {
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
_managerAPI.clearAllData();
|
||||||
if (_lastUpdate == null ||
|
_toast.showBottom('homeView.refreshSuccess');
|
||||||
_lastUpdate!.difference(DateTime.now()).inSeconds > 60) {
|
|
||||||
_managerAPI.clearAllData();
|
|
||||||
}
|
|
||||||
initialize(context);
|
initialize(context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,25 +2,76 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_i18n/flutter_i18n.dart';
|
import 'package:flutter_i18n/flutter_i18n.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:revanced_manager/ui/views/installer/installer_viewmodel.dart';
|
import 'package:revanced_manager/ui/views/installer/installer_viewmodel.dart';
|
||||||
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
|
||||||
import 'package:revanced_manager/ui/widgets/installerView/gradient_progress_indicator.dart';
|
import 'package:revanced_manager/ui/widgets/installerView/gradient_progress_indicator.dart';
|
||||||
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
|
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
|
||||||
import 'package:revanced_manager/ui/widgets/shared/custom_popup_menu.dart';
|
|
||||||
import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart';
|
import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart';
|
||||||
|
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_floating_action_button_extended.dart';
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
|
|
||||||
class InstallerView extends StatelessWidget {
|
class InstallerView extends StatelessWidget {
|
||||||
const InstallerView({Key? key}) : super(key: key);
|
const InstallerView({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ViewModelBuilder<InstallerViewModel>.reactive(
|
return ViewModelBuilder<InstallerViewModel>.reactive(
|
||||||
onModelReady: (model) => model.initialize(context),
|
onViewModelReady: (model) => model.initialize(context),
|
||||||
viewModelBuilder: () => InstallerViewModel(),
|
viewModelBuilder: () => InstallerViewModel(),
|
||||||
builder: (context, model, child) => WillPopScope(
|
builder: (context, model, child) => PopScope(
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
top: false,
|
top: false,
|
||||||
|
bottom: model.isPatching,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
|
floatingActionButton: Visibility(
|
||||||
|
visible: !model.isPatching && !model.hasErrors,
|
||||||
|
child: HapticFloatingActionButtonExtended(
|
||||||
|
label: I18nText(
|
||||||
|
model.isInstalled
|
||||||
|
? 'installerView.openButton'
|
||||||
|
: 'installerView.installButton',
|
||||||
|
),
|
||||||
|
icon: model.isInstalled
|
||||||
|
? const Icon(Icons.open_in_new)
|
||||||
|
: const Icon(Icons.file_download_outlined),
|
||||||
|
onPressed: model.isInstalled
|
||||||
|
? () => {
|
||||||
|
model.openApp(),
|
||||||
|
model.cleanPatcher(),
|
||||||
|
Navigator.of(context).pop(),
|
||||||
|
}
|
||||||
|
: () => model.installTypeDialog(context),
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
floatingActionButtonLocation:
|
||||||
|
FloatingActionButtonLocation.endContained,
|
||||||
|
bottomNavigationBar: Visibility(
|
||||||
|
visible: !model.isPatching,
|
||||||
|
child: BottomAppBar(
|
||||||
|
child: Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Visibility(
|
||||||
|
visible: !model.hasErrors,
|
||||||
|
child: IconButton.filledTonal(
|
||||||
|
tooltip: FlutterI18n.translate(
|
||||||
|
context,
|
||||||
|
'installerView.exportApkButtonTooltip',
|
||||||
|
),
|
||||||
|
icon: const Icon(Icons.save),
|
||||||
|
onPressed: () => model.onButtonPressed(0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton.filledTonal(
|
||||||
|
tooltip: FlutterI18n.translate(
|
||||||
|
context,
|
||||||
|
'installerView.exportLogButtonTooltip',
|
||||||
|
),
|
||||||
|
icon: const Icon(Icons.post_add),
|
||||||
|
onPressed: () => model.onButtonPressed(1),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
body: CustomScrollView(
|
body: CustomScrollView(
|
||||||
controller: model.scrollController,
|
controller: model.scrollController,
|
||||||
slivers: <Widget>[
|
slivers: <Widget>[
|
||||||
@@ -28,43 +79,16 @@ class InstallerView extends StatelessWidget {
|
|||||||
title: Text(
|
title: Text(
|
||||||
model.headerLogs,
|
model.headerLogs,
|
||||||
style: GoogleFonts.inter(
|
style: GoogleFonts.inter(
|
||||||
color: Theme.of(context).textTheme.headline6!.color,
|
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||||
),
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
onBackButtonPressed: () => model.onWillPop(context),
|
onBackButtonPressed: () => model.onWillPop(context),
|
||||||
actions: <Widget>[
|
|
||||||
Visibility(
|
|
||||||
visible: !model.isPatching,
|
|
||||||
child: CustomPopupMenu(
|
|
||||||
onSelected: (value) => model.onMenuSelection(value),
|
|
||||||
children: {
|
|
||||||
if (!model.hasErrors)
|
|
||||||
0: I18nText(
|
|
||||||
'installerView.shareApkMenuOption',
|
|
||||||
child: const Text(
|
|
||||||
'',
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
1: I18nText(
|
|
||||||
'installerView.shareLogMenuOption',
|
|
||||||
child: const Text(
|
|
||||||
'',
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
bottom: PreferredSize(
|
bottom: PreferredSize(
|
||||||
preferredSize: const Size(double.infinity, 1.0),
|
preferredSize: const Size(double.infinity, 1.0),
|
||||||
child:
|
child: GradientProgressIndicator(progress: model.progress),
|
||||||
GradientProgressIndicator(progress: model.progress!)),
|
),
|
||||||
),
|
),
|
||||||
SliverPadding(
|
SliverPadding(
|
||||||
padding: const EdgeInsets.all(20.0),
|
padding: const EdgeInsets.all(20.0),
|
||||||
@@ -84,71 +108,11 @@ class InstallerView extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SliverFillRemaining(
|
|
||||||
hasScrollBody: false,
|
|
||||||
child: Align(
|
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
child: Visibility(
|
|
||||||
visible: !model.isPatching && !model.hasErrors,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(20.0).copyWith(top: 0.0),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
Visibility(
|
|
||||||
visible: model.isInstalled,
|
|
||||||
child: CustomMaterialButton(
|
|
||||||
label: I18nText('installerView.openButton'),
|
|
||||||
isExpanded: true,
|
|
||||||
onPressed: () {
|
|
||||||
model.openApp();
|
|
||||||
model.cleanPatcher();
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Visibility(
|
|
||||||
visible: !model.isInstalled && model.isRooted,
|
|
||||||
child: CustomMaterialButton(
|
|
||||||
isFilled: false,
|
|
||||||
label:
|
|
||||||
I18nText('installerView.installRootButton'),
|
|
||||||
isExpanded: true,
|
|
||||||
onPressed: () => model.installResult(
|
|
||||||
context,
|
|
||||||
true,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Visibility(
|
|
||||||
visible: !model.isInstalled,
|
|
||||||
child: const SizedBox(
|
|
||||||
width: 16,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Visibility(
|
|
||||||
visible: !model.isInstalled,
|
|
||||||
child: CustomMaterialButton(
|
|
||||||
label: I18nText('installerView.installButton'),
|
|
||||||
isExpanded: true,
|
|
||||||
onPressed: () => model.installResult(
|
|
||||||
context,
|
|
||||||
false,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onWillPop: () => model.onWillPop(context),
|
onPopInvoked: (bool didPop) => model.onWillPop(context),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// ignore_for_file: use_build_context_synchronously
|
// ignore_for_file: use_build_context_synchronously
|
||||||
import 'package:device_apps/device_apps.dart';
|
import 'package:device_apps/device_apps.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_background/flutter_background.dart';
|
import 'package:flutter_background/flutter_background.dart';
|
||||||
@@ -12,10 +13,12 @@ import 'package:revanced_manager/services/manager_api.dart';
|
|||||||
import 'package:revanced_manager/services/patcher_api.dart';
|
import 'package:revanced_manager/services/patcher_api.dart';
|
||||||
import 'package:revanced_manager/services/root_api.dart';
|
import 'package:revanced_manager/services/root_api.dart';
|
||||||
import 'package:revanced_manager/services/toast.dart';
|
import 'package:revanced_manager/services/toast.dart';
|
||||||
|
import 'package:revanced_manager/ui/views/home/home_viewmodel.dart';
|
||||||
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
||||||
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
import 'package:revanced_manager/utils/about_info.dart';
|
||||||
|
import 'package:screenshot_callback/screenshot_callback.dart';
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
import 'package:wakelock/wakelock.dart';
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||||
|
|
||||||
class InstallerViewModel extends BaseViewModel {
|
class InstallerViewModel extends BaseViewModel {
|
||||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||||
@@ -28,6 +31,7 @@ class InstallerViewModel extends BaseViewModel {
|
|||||||
'app.revanced.manager.flutter/installer',
|
'app.revanced.manager.flutter/installer',
|
||||||
);
|
);
|
||||||
final ScrollController scrollController = ScrollController();
|
final ScrollController scrollController = ScrollController();
|
||||||
|
final ScreenshotCallback screenshotCallback = ScreenshotCallback();
|
||||||
double? progress = 0.0;
|
double? progress = 0.0;
|
||||||
String logs = '';
|
String logs = '';
|
||||||
String headerLogs = '';
|
String headerLogs = '';
|
||||||
@@ -35,6 +39,9 @@ class InstallerViewModel extends BaseViewModel {
|
|||||||
bool isPatching = true;
|
bool isPatching = true;
|
||||||
bool isInstalled = false;
|
bool isInstalled = false;
|
||||||
bool hasErrors = false;
|
bool hasErrors = false;
|
||||||
|
bool isCanceled = false;
|
||||||
|
bool cancel = false;
|
||||||
|
bool showPopupScreenshotWarning = true;
|
||||||
|
|
||||||
Future<void> initialize(BuildContext context) async {
|
Future<void> initialize(BuildContext context) async {
|
||||||
isRooted = await _rootAPI.isRooted();
|
isRooted = await _rootAPI.isRooted();
|
||||||
@@ -50,18 +57,24 @@ class InstallerViewModel extends BaseViewModel {
|
|||||||
context,
|
context,
|
||||||
'installerView.notificationText',
|
'installerView.notificationText',
|
||||||
),
|
),
|
||||||
notificationImportance: AndroidNotificationImportance.Default,
|
|
||||||
notificationIcon: const AndroidResource(
|
notificationIcon: const AndroidResource(
|
||||||
name: 'ic_notification',
|
name: 'ic_notification',
|
||||||
defType: 'drawable',
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
).then((value) => FlutterBackground.enableBackgroundExecution());
|
).then((value) => FlutterBackground.enableBackgroundExecution());
|
||||||
} on Exception {
|
} on Exception catch (e) {
|
||||||
// ignore
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
|
} // ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await Wakelock.enable();
|
screenshotCallback.addListener(() {
|
||||||
|
if (showPopupScreenshotWarning) {
|
||||||
|
showPopupScreenshotWarning = false;
|
||||||
|
screenshotDetected(context);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await WakelockPlus.enable();
|
||||||
await handlePlatformChannelMethods();
|
await handlePlatformChannelMethods();
|
||||||
await runPatcher();
|
await runPatcher();
|
||||||
}
|
}
|
||||||
@@ -71,10 +84,10 @@ class InstallerViewModel extends BaseViewModel {
|
|||||||
switch (call.method) {
|
switch (call.method) {
|
||||||
case 'update':
|
case 'update':
|
||||||
if (call.arguments != null) {
|
if (call.arguments != null) {
|
||||||
Map<dynamic, dynamic> arguments = call.arguments;
|
final Map<dynamic, dynamic> arguments = call.arguments;
|
||||||
double progress = arguments['progress'];
|
final double progress = arguments['progress'];
|
||||||
String header = arguments['header'];
|
final String header = arguments['header'];
|
||||||
String log = arguments['log'];
|
final String log = arguments['log'];
|
||||||
update(progress, header, log);
|
update(progress, header, log);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -82,7 +95,7 @@ class InstallerViewModel extends BaseViewModel {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void update(double value, String header, String log) {
|
Future<void> update(double value, String header, String log) async {
|
||||||
if (value >= 0.0) {
|
if (value >= 0.0) {
|
||||||
progress = value;
|
progress = value;
|
||||||
}
|
}
|
||||||
@@ -91,12 +104,18 @@ class InstallerViewModel extends BaseViewModel {
|
|||||||
isPatching = true;
|
isPatching = true;
|
||||||
isInstalled = false;
|
isInstalled = false;
|
||||||
hasErrors = false;
|
hasErrors = false;
|
||||||
} else if (value == 1.0) {
|
} else if (value == .85) {
|
||||||
isPatching = false;
|
isPatching = false;
|
||||||
hasErrors = false;
|
hasErrors = false;
|
||||||
|
await _managerAPI.savePatches(
|
||||||
|
_patcherAPI.getFilteredPatches(_app.packageName),
|
||||||
|
_app.packageName,
|
||||||
|
);
|
||||||
|
await _managerAPI.setUsedPatches(_patches, _app.packageName);
|
||||||
} else if (value == -100.0) {
|
} else if (value == -100.0) {
|
||||||
isPatching = false;
|
isPatching = false;
|
||||||
hasErrors = true;
|
hasErrors = true;
|
||||||
|
progress = 0.0;
|
||||||
}
|
}
|
||||||
if (header.isNotEmpty) {
|
if (header.isNotEmpty) {
|
||||||
headerLogs = header;
|
headerLogs = header;
|
||||||
@@ -109,10 +128,10 @@ class InstallerViewModel extends BaseViewModel {
|
|||||||
if (logs[logs.length - 1] == '\n') {
|
if (logs[logs.length - 1] == '\n') {
|
||||||
logs = logs.substring(0, logs.length - 1);
|
logs = logs.substring(0, logs.length - 1);
|
||||||
}
|
}
|
||||||
Future.delayed(const Duration(milliseconds: 500)).then((value) {
|
Future.delayed(const Duration(milliseconds: 100)).then((value) {
|
||||||
scrollController.animateTo(
|
scrollController.animateTo(
|
||||||
scrollController.position.maxScrollExtent,
|
scrollController.position.maxScrollExtent,
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 100),
|
||||||
curve: Curves.fastOutSlowIn,
|
curve: Curves.fastOutSlowIn,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -121,125 +140,424 @@ class InstallerViewModel extends BaseViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> runPatcher() async {
|
Future<void> runPatcher() async {
|
||||||
update(0.0, 'Initializing...', 'Initializing installer');
|
try {
|
||||||
if (_patches.isNotEmpty) {
|
await _patcherAPI.runPatcher(
|
||||||
try {
|
_app.packageName,
|
||||||
update(0.1, '', 'Creating working directory');
|
_app.apkFilePath,
|
||||||
await _patcherAPI.runPatcher(
|
_patches,
|
||||||
_app.packageName,
|
);
|
||||||
_app.apkFilePath,
|
} on Exception catch (e) {
|
||||||
_patches,
|
update(
|
||||||
);
|
-100.0,
|
||||||
} catch (e) {
|
'Failed...',
|
||||||
update(
|
'Something went wrong:\n$e',
|
||||||
-100.0,
|
);
|
||||||
'Aborting...',
|
if (kDebugMode) {
|
||||||
'An error occurred! Aborting\nError:\n$e',
|
print(e);
|
||||||
);
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
update(-100.0, 'Aborting...', 'No app or patches selected! Aborting');
|
// Necessary to reset the state of patches so that they
|
||||||
}
|
// can be reloaded again.
|
||||||
if (FlutterBackground.isBackgroundExecutionEnabled) {
|
_managerAPI.patches.clear();
|
||||||
try {
|
await _patcherAPI.loadPatches();
|
||||||
FlutterBackground.disableBackgroundExecution();
|
|
||||||
} on Exception {
|
try {
|
||||||
// ignore
|
if (FlutterBackground.isBackgroundExecutionEnabled) {
|
||||||
|
try {
|
||||||
|
FlutterBackground.disableBackgroundExecution();
|
||||||
|
} on Exception catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
|
} // ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await WakelockPlus.disable();
|
||||||
|
} on Exception catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await Wakelock.disable();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void installResult(BuildContext context, bool installAsRoot) async {
|
void _trimLogs(List<String> logLines, String keyword, String? newString) {
|
||||||
_app.isRooted = installAsRoot;
|
final lineCount = logLines.where((line) => line.endsWith(keyword)).length;
|
||||||
bool hasMicroG = _patches.any((p) => p.name.endsWith('microg-support'));
|
final index = logLines.indexWhere((line) => line.endsWith(keyword));
|
||||||
bool rootMicroG = installAsRoot && hasMicroG;
|
if (newString != null && lineCount > 0) {
|
||||||
bool rootFromStorage = installAsRoot && _app.isFromStorage;
|
logLines.insert(
|
||||||
bool ytWithoutRootMicroG =
|
index,
|
||||||
!installAsRoot && !hasMicroG && _app.packageName.contains('youtube');
|
newString.replaceAll('{lineCount}', lineCount.toString()),
|
||||||
if (rootMicroG || rootFromStorage || ytWithoutRootMicroG) {
|
);
|
||||||
return showDialog(
|
}
|
||||||
context: context,
|
logLines.removeWhere((lines) => lines.endsWith(keyword));
|
||||||
builder: (context) => AlertDialog(
|
}
|
||||||
title: I18nText('installerView.installErrorDialogTitle'),
|
|
||||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
dynamic _getPatchOptionValue(String patchName, Option option) {
|
||||||
content: I18nText(
|
final Option? savedOption =
|
||||||
rootMicroG
|
_managerAPI.getPatchOption(_app.packageName, patchName, option.key);
|
||||||
? 'installerView.installErrorDialogText1'
|
if (savedOption != null) {
|
||||||
: rootFromStorage
|
return savedOption.value;
|
||||||
? 'installerView.installErrorDialogText3'
|
} else {
|
||||||
: 'installerView.installErrorDialogText2',
|
return option.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatPatches(List<Patch> patches, String noneString) {
|
||||||
|
return patches.isEmpty
|
||||||
|
? noneString
|
||||||
|
: patches.map((p) {
|
||||||
|
final optionsChanged = p.options
|
||||||
|
.where((o) => _getPatchOptionValue(p.name, o) != o.value)
|
||||||
|
.toList();
|
||||||
|
return p.name +
|
||||||
|
(optionsChanged.isEmpty
|
||||||
|
? ''
|
||||||
|
: ' [${optionsChanged.map((o) => '${o.title}: ${_getPatchOptionValue(p.name, o)}').join(", ")}]');
|
||||||
|
}).join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getSuggestedVersion(String packageName) {
|
||||||
|
String suggestedVersion = _patcherAPI.getSuggestedVersion(_app.packageName);
|
||||||
|
if (suggestedVersion.isEmpty) {
|
||||||
|
suggestedVersion = 'Any';
|
||||||
|
}
|
||||||
|
return suggestedVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> copyLogs() async {
|
||||||
|
final info = await AboutInfo.getInfo();
|
||||||
|
|
||||||
|
// Trim out extra lines
|
||||||
|
final logsTrimmed = logs.split('\n');
|
||||||
|
_trimLogs(logsTrimmed, 'succeeded', 'Applied {lineCount} patches');
|
||||||
|
_trimLogs(logsTrimmed, '.dex', 'Compiled {lineCount} dex files');
|
||||||
|
|
||||||
|
// Get patches added / removed
|
||||||
|
final defaultPatches = _patcherAPI
|
||||||
|
.getFilteredPatches(_app.packageName)
|
||||||
|
.where((p) => !p.excluded)
|
||||||
|
.toList();
|
||||||
|
final appliedPatchesNames = _patches.map((p) => p.name).toList();
|
||||||
|
|
||||||
|
final patchesAdded = _patches.where((p) => p.excluded).toList();
|
||||||
|
final patchesRemoved = defaultPatches
|
||||||
|
.where((p) => !appliedPatchesNames.contains(p.name))
|
||||||
|
.map((p) => p.name)
|
||||||
|
.toList();
|
||||||
|
final patchesOptionsChanged = defaultPatches
|
||||||
|
.where(
|
||||||
|
(p) =>
|
||||||
|
appliedPatchesNames.contains(p.name) &&
|
||||||
|
p.options.any((o) => _getPatchOptionValue(p.name, o) != o.value),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
// Add Info
|
||||||
|
final formattedLogs = [
|
||||||
|
'- Device Info',
|
||||||
|
'ReVanced Manager: ${info['version']}',
|
||||||
|
'Model: ${info['model']}',
|
||||||
|
'Android version: ${info['androidVersion']}',
|
||||||
|
'Supported architectures: ${info['supportedArch'].join(", ")}',
|
||||||
|
'Root permissions: ${isRooted ? 'Yes' : 'No'}', //
|
||||||
|
|
||||||
|
'\n- Patch Info',
|
||||||
|
'App: ${_app.packageName} v${_app.version} (Suggested: ${_getSuggestedVersion(_app.packageName)})',
|
||||||
|
'Patches version: ${_managerAPI.patchesVersion}',
|
||||||
|
'Patches added: ${_formatPatches(patchesAdded, 'Default')}',
|
||||||
|
'Patches removed: ${patchesRemoved.isEmpty ? 'None' : patchesRemoved.join(', ')}',
|
||||||
|
'Default patch options changed: ${_formatPatches(patchesOptionsChanged, 'None')}', //
|
||||||
|
|
||||||
|
'\n- Settings',
|
||||||
|
'Allow changing patch selection: ${_managerAPI.isPatchesChangeEnabled()}',
|
||||||
|
'Version compatibility check: ${_managerAPI.isVersionCompatibilityCheckEnabled()}',
|
||||||
|
'Show universal patches: ${_managerAPI.areUniversalPatchesEnabled()}',
|
||||||
|
'Patches source: ${_managerAPI.getPatchesRepo()}',
|
||||||
|
'Integration source: ${_managerAPI.getIntegrationsRepo()}', //
|
||||||
|
|
||||||
|
'\n- Logs',
|
||||||
|
logsTrimmed.join('\n'),
|
||||||
|
];
|
||||||
|
|
||||||
|
Clipboard.setData(ClipboardData(text: formattedLogs.join('\n')));
|
||||||
|
_toast.showBottom('installerView.copiedToClipboard');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> screenshotDetected(BuildContext context) async {
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: I18nText(
|
||||||
|
'warning',
|
||||||
|
),
|
||||||
|
icon: const Icon(Icons.warning),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: I18nText('installerView.screenshotDetected'),
|
||||||
|
),
|
||||||
|
actions: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: I18nText('noButton'),
|
||||||
),
|
),
|
||||||
actions: <Widget>[
|
FilledButton(
|
||||||
CustomMaterialButton(
|
onPressed: () {
|
||||||
label: I18nText('okButton'),
|
copyLogs();
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
showPopupScreenshotWarning = true;
|
||||||
)
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: I18nText('yesButton'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> installTypeDialog(BuildContext context) async {
|
||||||
|
final ValueNotifier<int> installType = ValueNotifier(0);
|
||||||
|
if (isRooted) {
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (innerContext) => AlertDialog(
|
||||||
|
title: I18nText(
|
||||||
|
'installerView.installType',
|
||||||
|
),
|
||||||
|
icon: const Icon(Icons.file_download_outlined),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: ValueListenableBuilder(
|
||||||
|
valueListenable: installType,
|
||||||
|
builder: (context, value, child) {
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 20,
|
||||||
|
vertical: 10,
|
||||||
|
),
|
||||||
|
child: I18nText(
|
||||||
|
'installerView.installTypeDescription',
|
||||||
|
child: Text(
|
||||||
|
'',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
RadioListTile(
|
||||||
|
title: I18nText('installerView.installNonRootType'),
|
||||||
|
contentPadding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
value: 0,
|
||||||
|
groupValue: value,
|
||||||
|
onChanged: (selected) {
|
||||||
|
installType.value = selected!;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
RadioListTile(
|
||||||
|
title: I18nText('installerView.installRootType'),
|
||||||
|
contentPadding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
value: 1,
|
||||||
|
groupValue: value,
|
||||||
|
onChanged: (selected) {
|
||||||
|
installType.value = selected!;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: I18nText(
|
||||||
|
'installerView.warning',
|
||||||
|
child: Text(
|
||||||
|
'',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Theme.of(context).colorScheme.error,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(innerContext).pop();
|
||||||
|
},
|
||||||
|
child: I18nText('cancelButton'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(innerContext).pop();
|
||||||
|
installResult(context, installType.value == 1);
|
||||||
|
},
|
||||||
|
child: I18nText('installerView.installButton'),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
update(
|
await showDialog(
|
||||||
1.0,
|
context: context,
|
||||||
'Installing...',
|
barrierDismissible: false,
|
||||||
_app.isRooted
|
builder: (innerContext) => AlertDialog(
|
||||||
? 'Installing patched file using root method'
|
title: I18nText(
|
||||||
: 'Installing patched file using nonroot method',
|
'warning',
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.all(16),
|
||||||
|
content: I18nText('installerView.warning'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(innerContext).pop();
|
||||||
|
},
|
||||||
|
child: I18nText('cancelButton'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(innerContext).pop();
|
||||||
|
installResult(context, false);
|
||||||
|
},
|
||||||
|
child: I18nText('installerView.installButton'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
isInstalled = await _patcherAPI.installPatchedFile(_app);
|
}
|
||||||
if (isInstalled) {
|
}
|
||||||
update(1.0, 'Installed!', 'Installed!');
|
|
||||||
_app.isFromStorage = false;
|
Future<void> stopPatcher() async {
|
||||||
_app.patchDate = DateTime.now();
|
try {
|
||||||
_app.appliedPatches = _patches.map((p) => p.name).toList();
|
isCanceled = true;
|
||||||
if (hasMicroG) {
|
update(0.5, 'Canceling...', 'Canceling patching process');
|
||||||
_app.name += ' ReVanced';
|
await _patcherAPI.stopPatcher();
|
||||||
_app.packageName = _app.packageName.replaceFirst(
|
await WakelockPlus.disable();
|
||||||
'com.google.',
|
update(-100.0, 'Canceled...', 'Press back to exit');
|
||||||
'app.revanced.',
|
} on Exception catch (e) {
|
||||||
);
|
if (kDebugMode) {
|
||||||
}
|
print(e);
|
||||||
await _managerAPI.savePatchedApp(_app);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void shareResult() {
|
Future<void> installResult(BuildContext context, bool installAsRoot) async {
|
||||||
_patcherAPI.sharePatchedFile(_app.name, _app.version);
|
try {
|
||||||
|
_app.isRooted = installAsRoot;
|
||||||
|
if (headerLogs != 'Installing...') {
|
||||||
|
update(
|
||||||
|
.85,
|
||||||
|
'Installing...',
|
||||||
|
_app.isRooted ? 'Mounting patched app' : 'Installing patched app',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final int response = await _patcherAPI.installPatchedFile(context, _app);
|
||||||
|
if (response == 0) {
|
||||||
|
isInstalled = true;
|
||||||
|
_app.isFromStorage = false;
|
||||||
|
_app.patchDate = DateTime.now();
|
||||||
|
_app.appliedPatches = _patches.map((p) => p.name).toList();
|
||||||
|
|
||||||
|
// In case a patch changed the app name or package name,
|
||||||
|
// update the app info.
|
||||||
|
final app =
|
||||||
|
await DeviceApps.getAppFromStorage(_patcherAPI.outFile!.path);
|
||||||
|
if (app != null) {
|
||||||
|
_app.name = app.appName;
|
||||||
|
_app.packageName = app.packageName;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _managerAPI.savePatchedApp(_app);
|
||||||
|
await locator<HomeViewModel>().initialize(context);
|
||||||
|
|
||||||
|
update(1.0, 'Installed', 'Installed');
|
||||||
|
} else if (response == 3) {
|
||||||
|
update(
|
||||||
|
.85,
|
||||||
|
'Installation canceled',
|
||||||
|
'Installation canceled',
|
||||||
|
);
|
||||||
|
} else if (response == 10) {
|
||||||
|
installResult(context, installAsRoot);
|
||||||
|
} else {
|
||||||
|
update(
|
||||||
|
.85,
|
||||||
|
'Installation failed',
|
||||||
|
'Installation failed',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} on Exception catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void shareLog() {
|
void exportResult() {
|
||||||
_patcherAPI.sharePatcherLog(logs);
|
try {
|
||||||
|
_patcherAPI.exportPatchedFile(_app.name, _app.version);
|
||||||
|
} on Exception catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> cleanPatcher() async {
|
Future<void> cleanPatcher() async {
|
||||||
_patcherAPI.cleanPatcher();
|
try {
|
||||||
locator<PatcherViewModel>().selectedApp = null;
|
_patcherAPI.cleanPatcher();
|
||||||
locator<PatcherViewModel>().selectedPatches.clear();
|
locator<PatcherViewModel>().selectedApp = null;
|
||||||
locator<PatcherViewModel>().notifyListeners();
|
locator<PatcherViewModel>().selectedPatches.clear();
|
||||||
|
locator<PatcherViewModel>().notifyListeners();
|
||||||
|
} on Exception catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void openApp() {
|
void openApp() {
|
||||||
DeviceApps.openApp(_app.packageName);
|
DeviceApps.openApp(_app.packageName);
|
||||||
}
|
}
|
||||||
|
|
||||||
void onMenuSelection(int value) {
|
void onButtonPressed(int value) {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case 0:
|
case 0:
|
||||||
shareResult();
|
exportResult();
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
shareLog();
|
copyLogs();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> onWillPop(BuildContext context) async {
|
Future<bool> onWillPop(BuildContext context) async {
|
||||||
if (isPatching) {
|
if (isPatching) {
|
||||||
_toast.show('installerView.noExit');
|
if (!cancel) {
|
||||||
|
cancel = true;
|
||||||
|
_toast.showBottom('installerView.pressBackAgain');
|
||||||
|
} else if (!isCanceled) {
|
||||||
|
await stopPatcher();
|
||||||
|
} else {
|
||||||
|
_toast.showBottom('installerView.noExit');
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
cleanPatcher();
|
if (!cancel) {
|
||||||
|
cleanPatcher();
|
||||||
|
} else {
|
||||||
|
_patcherAPI.cleanPatcher();
|
||||||
|
}
|
||||||
|
screenshotCallback.dispose();
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,65 +6,71 @@ import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart';
|
|||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
|
|
||||||
class NavigationView extends StatelessWidget {
|
class NavigationView extends StatelessWidget {
|
||||||
const NavigationView({Key? key}) : super(key: key);
|
const NavigationView({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ViewModelBuilder<NavigationViewModel>.reactive(
|
return ViewModelBuilder<NavigationViewModel>.reactive(
|
||||||
onModelReady: (model) => model.initialize(context),
|
onViewModelReady: (model) => model.initialize(context),
|
||||||
viewModelBuilder: () => locator<NavigationViewModel>(),
|
viewModelBuilder: () => locator<NavigationViewModel>(),
|
||||||
builder: (context, model, child) => Scaffold(
|
builder: (context, model, child) => PopScope(
|
||||||
body: PageTransitionSwitcher(
|
canPop: model.currentIndex == 0,
|
||||||
duration: const Duration(milliseconds: 400),
|
onPopInvoked: (bool didPop) => {
|
||||||
transitionBuilder: (
|
if (!didPop) model.setIndex(0),
|
||||||
Widget child,
|
},
|
||||||
Animation<double> animation,
|
child: Scaffold(
|
||||||
Animation<double> secondaryAnimation,
|
body: PageTransitionSwitcher(
|
||||||
) {
|
duration: const Duration(milliseconds: 400),
|
||||||
return FadeThroughTransition(
|
transitionBuilder: (
|
||||||
animation: animation,
|
Widget child,
|
||||||
secondaryAnimation: secondaryAnimation,
|
Animation<double> animation,
|
||||||
fillColor: Theme.of(context).colorScheme.surface,
|
Animation<double> secondaryAnimation,
|
||||||
child: child,
|
) {
|
||||||
);
|
return FadeThroughTransition(
|
||||||
},
|
animation: animation,
|
||||||
child: model.getViewForIndex(model.currentIndex),
|
secondaryAnimation: secondaryAnimation,
|
||||||
),
|
fillColor: Theme.of(context).colorScheme.surface,
|
||||||
bottomNavigationBar: NavigationBar(
|
child: child,
|
||||||
onDestinationSelected: model.setIndex,
|
);
|
||||||
selectedIndex: model.currentIndex,
|
},
|
||||||
destinations: <Widget>[
|
child: model.getViewForIndex(model.currentIndex),
|
||||||
NavigationDestination(
|
),
|
||||||
icon: model.isIndexSelected(0)
|
bottomNavigationBar: NavigationBar(
|
||||||
? const Icon(Icons.dashboard)
|
onDestinationSelected: model.setIndex,
|
||||||
: const Icon(Icons.dashboard_outlined),
|
selectedIndex: model.currentIndex,
|
||||||
label: FlutterI18n.translate(
|
destinations: <Widget>[
|
||||||
context,
|
NavigationDestination(
|
||||||
'navigationView.dashboardTab',
|
icon: model.isIndexSelected(0)
|
||||||
|
? const Icon(Icons.dashboard)
|
||||||
|
: const Icon(Icons.dashboard_outlined),
|
||||||
|
label: FlutterI18n.translate(
|
||||||
|
context,
|
||||||
|
'navigationView.dashboardTab',
|
||||||
|
),
|
||||||
|
tooltip: '',
|
||||||
),
|
),
|
||||||
tooltip: '',
|
NavigationDestination(
|
||||||
),
|
icon: model.isIndexSelected(1)
|
||||||
NavigationDestination(
|
? const Icon(Icons.build)
|
||||||
icon: model.isIndexSelected(1)
|
: const Icon(Icons.build_outlined),
|
||||||
? const Icon(Icons.build)
|
label: FlutterI18n.translate(
|
||||||
: const Icon(Icons.build_outlined),
|
context,
|
||||||
label: FlutterI18n.translate(
|
'navigationView.patcherTab',
|
||||||
context,
|
),
|
||||||
'navigationView.patcherTab',
|
tooltip: '',
|
||||||
),
|
),
|
||||||
tooltip: '',
|
NavigationDestination(
|
||||||
),
|
icon: model.isIndexSelected(2)
|
||||||
NavigationDestination(
|
? const Icon(Icons.settings)
|
||||||
icon: model.isIndexSelected(2)
|
: const Icon(Icons.settings_outlined),
|
||||||
? const Icon(Icons.settings)
|
label: FlutterI18n.translate(
|
||||||
: const Icon(Icons.settings_outlined),
|
context,
|
||||||
label: FlutterI18n.translate(
|
'navigationView.settingsTab',
|
||||||
context,
|
),
|
||||||
'navigationView.settingsTab',
|
tooltip: '',
|
||||||
),
|
),
|
||||||
tooltip: '',
|
],
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// ignore_for_file: use_build_context_synchronously
|
// ignore_for_file: use_build_context_synchronously
|
||||||
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:dynamic_themes/dynamic_themes.dart';
|
import 'package:dynamic_themes/dynamic_themes.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
@@ -15,23 +16,37 @@ import 'package:stacked/stacked.dart';
|
|||||||
|
|
||||||
@lazySingleton
|
@lazySingleton
|
||||||
class NavigationViewModel extends IndexTrackingViewModel {
|
class NavigationViewModel extends IndexTrackingViewModel {
|
||||||
void initialize(BuildContext context) async {
|
Future<void> initialize(BuildContext context) async {
|
||||||
locator<Toast>().initialize(context);
|
locator<Toast>().initialize(context);
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
|
||||||
if (prefs.getBool('permissionsRequested') == null) {
|
if (prefs.getBool('permissionsRequested') == null) {
|
||||||
|
await Permission.storage.request();
|
||||||
await prefs.setBool('permissionsRequested', true);
|
await prefs.setBool('permissionsRequested', true);
|
||||||
RootAPI().hasRootPermissions().then(
|
await RootAPI().hasRootPermissions().then(
|
||||||
(value) => Permission.requestInstallPackages.request().then(
|
(value) => Permission.requestInstallPackages.request().then(
|
||||||
(value) => Permission.ignoreBatteryOptimizations.request(),
|
(value) => Permission.ignoreBatteryOptimizations.request(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (prefs.getBool('useDarkTheme') == null) {
|
|
||||||
bool isDark =
|
final dynamicTheme = DynamicTheme.of(context)!;
|
||||||
MediaQuery.of(context).platformBrightness != Brightness.light;
|
if (prefs.getInt('themeMode') == null) {
|
||||||
await prefs.setBool('useDarkTheme', isDark);
|
await prefs.setInt('themeMode', 0);
|
||||||
await DynamicTheme.of(context)!.setTheme(isDark ? 1 : 0);
|
await dynamicTheme.setTheme(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Force disable Material You on Android 11 and below
|
||||||
|
if (dynamicTheme.themeId.isOdd) {
|
||||||
|
const int android12SdkVersion = 31;
|
||||||
|
final AndroidDeviceInfo info = await DeviceInfoPlugin().androidInfo;
|
||||||
|
if (info.version.sdkInt < android12SdkVersion) {
|
||||||
|
await prefs.setInt('themeMode', 0);
|
||||||
|
await prefs.setBool('useDynamicTheme', false);
|
||||||
|
await dynamicTheme.setTheme(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||||
SystemChrome.setSystemUIOverlayStyle(
|
SystemChrome.setSystemUIOverlayStyle(
|
||||||
SystemUiOverlayStyle(
|
SystemUiOverlayStyle(
|
||||||
|
|||||||
132
lib/ui/views/patch_options/patch_options_view.dart
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_i18n/flutter_i18n.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'package:revanced_manager/models/patch.dart';
|
||||||
|
import 'package:revanced_manager/ui/views/patch_options/patch_options_viewmodel.dart';
|
||||||
|
import 'package:revanced_manager/ui/widgets/patchesSelectorView/patch_options_fields.dart';
|
||||||
|
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_floating_action_button_extended.dart';
|
||||||
|
import 'package:stacked/stacked.dart';
|
||||||
|
|
||||||
|
class PatchOptionsView extends StatelessWidget {
|
||||||
|
const PatchOptionsView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ViewModelBuilder<PatchOptionsViewModel>.reactive(
|
||||||
|
onViewModelReady: (model) => model.initialize(),
|
||||||
|
viewModelBuilder: () => PatchOptionsViewModel(),
|
||||||
|
builder: (context, model, child) => GestureDetector(
|
||||||
|
onTap: () => FocusScope.of(context).unfocus(),
|
||||||
|
child: Scaffold(
|
||||||
|
floatingActionButton: HapticFloatingActionButtonExtended(
|
||||||
|
onPressed: () async {
|
||||||
|
final bool saved = model.saveOptions(context);
|
||||||
|
if (saved && context.mounted) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label: I18nText('patchOptionsView.saveOptions'),
|
||||||
|
icon: const Icon(Icons.save),
|
||||||
|
),
|
||||||
|
body: CustomScrollView(
|
||||||
|
slivers: <Widget>[
|
||||||
|
SliverAppBar(
|
||||||
|
title: I18nText(
|
||||||
|
'patchOptionsView.viewTitle',
|
||||||
|
child: Text(
|
||||||
|
'',
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
model.resetOptions();
|
||||||
|
},
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.history,
|
||||||
|
),
|
||||||
|
tooltip: FlutterI18n.translate(
|
||||||
|
context,
|
||||||
|
'patchOptionsView.resetOptionsTooltip',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
for (final Option option in model.visibleOptions)
|
||||||
|
if (option.valueType == 'String' ||
|
||||||
|
option.valueType == 'Int')
|
||||||
|
IntAndStringPatchOption(
|
||||||
|
patchOption: option,
|
||||||
|
removeOption: (option) {
|
||||||
|
model.removeOption(option);
|
||||||
|
},
|
||||||
|
onChanged: (value, option) {
|
||||||
|
model.modifyOptions(value, option);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else if (option.valueType == 'Boolean')
|
||||||
|
BooleanPatchOption(
|
||||||
|
patchOption: option,
|
||||||
|
removeOption: (option) {
|
||||||
|
model.removeOption(option);
|
||||||
|
},
|
||||||
|
onChanged: (value, option) {
|
||||||
|
model.modifyOptions(value, option);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else if (option.valueType == 'StringArray' ||
|
||||||
|
option.valueType == 'IntArray' ||
|
||||||
|
option.valueType == 'LongArray')
|
||||||
|
IntStringLongListPatchOption(
|
||||||
|
patchOption: option,
|
||||||
|
removeOption: (option) {
|
||||||
|
model.removeOption(option);
|
||||||
|
},
|
||||||
|
onChanged: (value, option) {
|
||||||
|
model.modifyOptions(value, option);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else
|
||||||
|
UnsupportedPatchOption(
|
||||||
|
patchOption: option,
|
||||||
|
),
|
||||||
|
if (model.visibleOptions.length !=
|
||||||
|
model.options.length) ...[
|
||||||
|
const SizedBox(
|
||||||
|
height: 8,
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () {
|
||||||
|
model.showAddOptionDialog(context);
|
||||||
|
},
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.add),
|
||||||
|
I18nText('patchOptionsView.addOptions'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const SizedBox(
|
||||||
|
height: 80,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
268
lib/ui/views/patch_options/patch_options_viewmodel.dart
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_i18n/widgets/I18nText.dart';
|
||||||
|
import 'package:revanced_manager/app/app.locator.dart';
|
||||||
|
import 'package:revanced_manager/models/patch.dart';
|
||||||
|
import 'package:revanced_manager/services/manager_api.dart';
|
||||||
|
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
||||||
|
import 'package:revanced_manager/ui/views/patches_selector/patches_selector_viewmodel.dart';
|
||||||
|
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
|
||||||
|
import 'package:stacked/stacked.dart';
|
||||||
|
|
||||||
|
class PatchOptionsViewModel extends BaseViewModel {
|
||||||
|
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||||
|
final String selectedApp =
|
||||||
|
locator<PatcherViewModel>().selectedApp!.packageName;
|
||||||
|
List<Option> options = [];
|
||||||
|
List<Option> savedOptions = [];
|
||||||
|
List<Option> visibleOptions = [];
|
||||||
|
|
||||||
|
Future<void> initialize() async {
|
||||||
|
options = getDefaultOptions();
|
||||||
|
for (final Option option in options) {
|
||||||
|
final Option? savedOption = _managerAPI.getPatchOption(
|
||||||
|
selectedApp,
|
||||||
|
_managerAPI.selectedPatch!.name,
|
||||||
|
option.key,
|
||||||
|
);
|
||||||
|
if (savedOption != null) {
|
||||||
|
savedOptions.add(savedOption);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (savedOptions.isNotEmpty) {
|
||||||
|
visibleOptions = [
|
||||||
|
...savedOptions,
|
||||||
|
...options.where(
|
||||||
|
(option) =>
|
||||||
|
option.required &&
|
||||||
|
!savedOptions.any((sOption) => sOption.key == option.key),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
visibleOptions = [
|
||||||
|
...options.where((option) => option.required),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void addOption(Option option) {
|
||||||
|
visibleOptions.add(option);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeOption(Option option) {
|
||||||
|
visibleOptions.removeWhere((vOption) => vOption.key == option.key);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool saveOptions(BuildContext context) {
|
||||||
|
final List<Option> requiredNullOptions = [];
|
||||||
|
for (final Option option in options) {
|
||||||
|
if (!visibleOptions.any((vOption) => vOption.key == option.key)) {
|
||||||
|
_managerAPI.clearPatchOption(
|
||||||
|
selectedApp,
|
||||||
|
_managerAPI.selectedPatch!.name,
|
||||||
|
option.key,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (final Option option in visibleOptions) {
|
||||||
|
if (option.required && option.value == null) {
|
||||||
|
requiredNullOptions.add(option);
|
||||||
|
} else {
|
||||||
|
_managerAPI.setPatchOption(
|
||||||
|
option,
|
||||||
|
_managerAPI.selectedPatch!.name,
|
||||||
|
selectedApp,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (requiredNullOptions.isNotEmpty) {
|
||||||
|
showRequiredOptionNullDialog(
|
||||||
|
context,
|
||||||
|
requiredNullOptions,
|
||||||
|
_managerAPI,
|
||||||
|
selectedApp,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void modifyOptions(dynamic value, Option option) {
|
||||||
|
final Option modifiedOption = Option(
|
||||||
|
title: option.title,
|
||||||
|
description: option.description,
|
||||||
|
values: option.values,
|
||||||
|
valueType: option.valueType,
|
||||||
|
value: value,
|
||||||
|
required: option.required,
|
||||||
|
key: option.key,
|
||||||
|
);
|
||||||
|
visibleOptions[visibleOptions
|
||||||
|
.indexWhere((vOption) => vOption.key == option.key)] = modifiedOption;
|
||||||
|
_managerAPI.modifiedOptions
|
||||||
|
.removeWhere((mOption) => mOption.key == option.key);
|
||||||
|
_managerAPI.modifiedOptions.add(modifiedOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Option> getDefaultOptions() {
|
||||||
|
final List<Option> defaultOptions = [];
|
||||||
|
for (final option in _managerAPI.options) {
|
||||||
|
final Option defaultOption = Option(
|
||||||
|
title: option.title,
|
||||||
|
description: option.description,
|
||||||
|
values: option.values,
|
||||||
|
valueType: option.valueType,
|
||||||
|
value: option.value is List ? option.value.toList() : option.value,
|
||||||
|
required: option.required,
|
||||||
|
key: option.key,
|
||||||
|
);
|
||||||
|
defaultOptions.add(defaultOption);
|
||||||
|
}
|
||||||
|
return defaultOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
void resetOptions() {
|
||||||
|
_managerAPI.modifiedOptions.clear();
|
||||||
|
visibleOptions =
|
||||||
|
getDefaultOptions().where((option) => option.required).toList();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> showAddOptionDialog(BuildContext context) async {
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
I18nText(
|
||||||
|
'patchOptionsView.addOptions',
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: I18nText('cancelButton'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
contentPadding: const EdgeInsets.all(8),
|
||||||
|
content: Wrap(
|
||||||
|
spacing: 14,
|
||||||
|
runSpacing: 14,
|
||||||
|
children: options
|
||||||
|
.where(
|
||||||
|
(option) =>
|
||||||
|
!visibleOptions.any((vOption) => vOption.key == option.key),
|
||||||
|
)
|
||||||
|
.map((e) {
|
||||||
|
return CustomCard(
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
|
onTap: () {
|
||||||
|
addOption(e);
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
e.title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
e.description,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> showRequiredOptionNullDialog(
|
||||||
|
BuildContext context,
|
||||||
|
List<Option> options,
|
||||||
|
ManagerAPI managerAPI,
|
||||||
|
String selectedApp,
|
||||||
|
) async {
|
||||||
|
final List<String> optionsTitles = [];
|
||||||
|
for (final option in options) {
|
||||||
|
optionsTitles.add('• ${option.title}');
|
||||||
|
}
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: I18nText('notice'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
if (managerAPI.isPatchesChangeEnabled()) {
|
||||||
|
locator<PatcherViewModel>()
|
||||||
|
.selectedPatches
|
||||||
|
.remove(managerAPI.selectedPatch);
|
||||||
|
locator<PatcherViewModel>().notifyListeners();
|
||||||
|
for (final option in options) {
|
||||||
|
managerAPI.clearPatchOption(
|
||||||
|
selectedApp,
|
||||||
|
managerAPI.selectedPatch!.name,
|
||||||
|
option.key,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Navigator.of(context)
|
||||||
|
..pop()
|
||||||
|
..pop()
|
||||||
|
..pop();
|
||||||
|
} else {
|
||||||
|
PatchesSelectorViewModel().showPatchesChangeDialog(context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: I18nText('patchOptionsView.deselectPatch'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: I18nText('okButton'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
content: I18nText(
|
||||||
|
'patchOptionsView.requiredOptionNull',
|
||||||
|
translationParams: {
|
||||||
|
'options': optionsTitles.join('\n'),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,10 +6,11 @@ import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
|||||||
import 'package:revanced_manager/ui/widgets/patcherView/app_selector_card.dart';
|
import 'package:revanced_manager/ui/widgets/patcherView/app_selector_card.dart';
|
||||||
import 'package:revanced_manager/ui/widgets/patcherView/patch_selector_card.dart';
|
import 'package:revanced_manager/ui/widgets/patcherView/patch_selector_card.dart';
|
||||||
import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart';
|
import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart';
|
||||||
|
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_floating_action_button_extended.dart';
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
|
|
||||||
class PatcherView extends StatelessWidget {
|
class PatcherView extends StatelessWidget {
|
||||||
const PatcherView({Key? key}) : super(key: key);
|
const PatcherView({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -19,10 +20,17 @@ class PatcherView extends StatelessWidget {
|
|||||||
builder: (context, model, child) => Scaffold(
|
builder: (context, model, child) => Scaffold(
|
||||||
floatingActionButton: Visibility(
|
floatingActionButton: Visibility(
|
||||||
visible: model.showPatchButton(),
|
visible: model.showPatchButton(),
|
||||||
child: FloatingActionButton.extended(
|
child: HapticFloatingActionButtonExtended(
|
||||||
label: I18nText('patcherView.patchButton'),
|
label: I18nText('patcherView.patchButton'),
|
||||||
icon: const Icon(Icons.build),
|
icon: const Icon(Icons.build),
|
||||||
onPressed: () => model.showPatchConfirmationDialog(context),
|
onPressed: () async {
|
||||||
|
if (model.checkRequiredPatchOption(context)) {
|
||||||
|
final bool proceed = model.showRemovedPatchesDialog(context);
|
||||||
|
if (proceed && context.mounted) {
|
||||||
|
model.showArmv7WarningDialog(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: CustomScrollView(
|
body: CustomScrollView(
|
||||||
@@ -34,7 +42,7 @@ class PatcherView extends StatelessWidget {
|
|||||||
child: Text(
|
child: Text(
|
||||||
'',
|
'',
|
||||||
style: GoogleFonts.inter(
|
style: GoogleFonts.inter(
|
||||||
color: Theme.of(context).textTheme.headline6!.color,
|
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -45,7 +53,10 @@ class PatcherView extends StatelessWidget {
|
|||||||
delegate: SliverChildListDelegate.fixed(
|
delegate: SliverChildListDelegate.fixed(
|
||||||
<Widget>[
|
<Widget>[
|
||||||
AppSelectorCard(
|
AppSelectorCard(
|
||||||
onPressed: () => model.navigateToAppSelector(),
|
onPressed: () => {
|
||||||
|
model.navigateToAppSelector(),
|
||||||
|
model.ctx = context,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Opacity(
|
Opacity(
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
|
// ignore_for_file: use_build_context_synchronously
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_i18n/flutter_i18n.dart';
|
import 'package:flutter_i18n/flutter_i18n.dart';
|
||||||
import 'package:injectable/injectable.dart';
|
import 'package:injectable/injectable.dart';
|
||||||
import 'package:revanced_manager/app/app.locator.dart';
|
import 'package:revanced_manager/app/app.locator.dart';
|
||||||
@@ -7,7 +13,8 @@ import 'package:revanced_manager/models/patch.dart';
|
|||||||
import 'package:revanced_manager/models/patched_application.dart';
|
import 'package:revanced_manager/models/patched_application.dart';
|
||||||
import 'package:revanced_manager/services/manager_api.dart';
|
import 'package:revanced_manager/services/manager_api.dart';
|
||||||
import 'package:revanced_manager/services/patcher_api.dart';
|
import 'package:revanced_manager/services/patcher_api.dart';
|
||||||
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
import 'package:revanced_manager/utils/about_info.dart';
|
||||||
|
import 'package:revanced_manager/utils/check_for_supported_patch.dart';
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
import 'package:stacked_services/stacked_services.dart';
|
import 'package:stacked_services/stacked_services.dart';
|
||||||
|
|
||||||
@@ -16,8 +23,11 @@ class PatcherViewModel extends BaseViewModel {
|
|||||||
final NavigationService _navigationService = locator<NavigationService>();
|
final NavigationService _navigationService = locator<NavigationService>();
|
||||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||||
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
|
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
|
||||||
|
Set<String> savedPatchNames = {};
|
||||||
PatchedApplication? selectedApp;
|
PatchedApplication? selectedApp;
|
||||||
|
BuildContext? ctx;
|
||||||
List<Patch> selectedPatches = [];
|
List<Patch> selectedPatches = [];
|
||||||
|
List<String> removedPatches = [];
|
||||||
|
|
||||||
void navigateToAppSelector() {
|
void navigateToAppSelector() {
|
||||||
_navigationService.navigateTo(Routes.appSelectorView);
|
_navigationService.navigateTo(Routes.appSelectorView);
|
||||||
@@ -39,72 +49,190 @@ class PatcherViewModel extends BaseViewModel {
|
|||||||
return selectedApp == null;
|
return selectedApp == null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> isValidPatchConfig() async {
|
bool showRemovedPatchesDialog(BuildContext context) {
|
||||||
bool needsResourcePatching = await _patcherAPI.needsResourcePatching(
|
if (removedPatches.isNotEmpty) {
|
||||||
selectedPatches,
|
showDialog(
|
||||||
);
|
context: context,
|
||||||
if (needsResourcePatching && selectedApp != null) {
|
builder: (context) => AlertDialog(
|
||||||
bool isSplit = await _managerAPI.isSplitApk(selectedApp!);
|
title: I18nText('notice'),
|
||||||
return !isSplit;
|
content: I18nText(
|
||||||
|
'patcherView.removedPatchesWarningDialogText',
|
||||||
|
translationParams: {'patches': removedPatches.join('\n')},
|
||||||
|
),
|
||||||
|
actions: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: I18nText('noButton'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
showArmv7WarningDialog(context);
|
||||||
|
},
|
||||||
|
child: I18nText('yesButton'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> showPatchConfirmationDialog(BuildContext context) async {
|
bool checkRequiredPatchOption(BuildContext context) {
|
||||||
bool isValid = await isValidPatchConfig();
|
if (getNullRequiredOptions(selectedPatches, selectedApp!.packageName)
|
||||||
if (isValid) {
|
.isNotEmpty) {
|
||||||
navigateToInstaller();
|
showRequiredOptionDialog(context);
|
||||||
} else {
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void showRequiredOptionDialog([context]) {
|
||||||
|
showDialog(
|
||||||
|
context: context ?? ctx,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: I18nText('notice'),
|
||||||
|
content: I18nText('patcherView.requiredOptionDialogText'),
|
||||||
|
actions: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => {
|
||||||
|
Navigator.of(context).pop(),
|
||||||
|
},
|
||||||
|
child: I18nText('cancelButton'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () => {
|
||||||
|
Navigator.pop(context),
|
||||||
|
navigateToPatchesSelector(),
|
||||||
|
},
|
||||||
|
child: I18nText('okButton'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> showArmv7WarningDialog(BuildContext context) async {
|
||||||
|
final bool armv7 = await AboutInfo.getInfo().then((info) {
|
||||||
|
final List<String> archs = info['supportedArch'];
|
||||||
|
final supportedAbis = ['arm64-v8a', 'x86', 'x86_64'];
|
||||||
|
return !archs.any((arch) => supportedAbis.contains(arch));
|
||||||
|
});
|
||||||
|
if (context.mounted && armv7) {
|
||||||
return showDialog(
|
return showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: I18nText('patcherView.patchDialogTitle'),
|
title: I18nText('warning'),
|
||||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
content: I18nText('patcherView.armv7WarningDialogText'),
|
||||||
content: I18nText('patcherView.patchDialogText'),
|
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
CustomMaterialButton(
|
FilledButton(
|
||||||
isFilled: false,
|
|
||||||
label: I18nText('noButton'),
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: I18nText('noButton'),
|
||||||
),
|
),
|
||||||
CustomMaterialButton(
|
TextButton(
|
||||||
label: I18nText('yesButton'),
|
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
navigateToInstaller();
|
navigateToInstaller();
|
||||||
},
|
},
|
||||||
)
|
child: I18nText('yesButton'),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
navigateToInstaller();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String getAppSelectionString() {
|
String getAppSelectionString() {
|
||||||
String text = '${selectedApp!.name} (${selectedApp!.packageName})';
|
return '${selectedApp!.name} ${selectedApp!.version}';
|
||||||
if (text.length > 32) {
|
|
||||||
text = '${text.substring(0, 32)}...)';
|
|
||||||
}
|
|
||||||
return text;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String getRecommendedVersionString(BuildContext context) {
|
Future<void> queryVersion(String suggestedVersion) async {
|
||||||
String recommendedVersion =
|
await openDefaultBrowser(
|
||||||
_patcherAPI.getRecommendedVersion(selectedApp!.packageName);
|
'${selectedApp!.packageName} apk version $suggestedVersion',
|
||||||
if (recommendedVersion.isEmpty) {
|
);
|
||||||
recommendedVersion = FlutterI18n.translate(
|
}
|
||||||
context,
|
|
||||||
'appSelectorCard.anyVersion',
|
String getSuggestedVersionString(BuildContext context) {
|
||||||
);
|
return _patcherAPI.getSuggestedVersion(selectedApp!.packageName);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> openDefaultBrowser(String query) async {
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
try {
|
||||||
|
const platform = MethodChannel('app.revanced.manager.flutter/browser');
|
||||||
|
await platform.invokeMethod('openBrowser', {'query': query});
|
||||||
|
} catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
recommendedVersion = 'v$recommendedVersion';
|
throw 'Platform not supported';
|
||||||
}
|
}
|
||||||
return '${FlutterI18n.translate(
|
}
|
||||||
context,
|
|
||||||
'appSelectorCard.currentVersion',
|
bool isPatchNew(Patch patch) {
|
||||||
)}: v${selectedApp!.version}\n${FlutterI18n.translate(
|
if (savedPatchNames.isEmpty) {
|
||||||
context,
|
savedPatchNames = _managerAPI
|
||||||
'appSelectorCard.recommendedVersion',
|
.getSavedPatches(selectedApp!.packageName)
|
||||||
)}: $recommendedVersion';
|
.map((p) => p.name)
|
||||||
|
.toSet();
|
||||||
|
}
|
||||||
|
if (savedPatchNames.isEmpty) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return !savedPatchNames.contains(patch.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> loadLastSelectedPatches() async {
|
||||||
|
this.selectedPatches.clear();
|
||||||
|
removedPatches.clear();
|
||||||
|
final List<String> selectedPatches =
|
||||||
|
await _managerAPI.getSelectedPatches(selectedApp!.packageName);
|
||||||
|
final List<Patch> patches =
|
||||||
|
_patcherAPI.getFilteredPatches(selectedApp!.packageName);
|
||||||
|
this
|
||||||
|
.selectedPatches
|
||||||
|
.addAll(patches.where((patch) => selectedPatches.contains(patch.name)));
|
||||||
|
if (!_managerAPI.isPatchesChangeEnabled()) {
|
||||||
|
this.selectedPatches.clear();
|
||||||
|
this.selectedPatches.addAll(patches.where((patch) => !patch.excluded));
|
||||||
|
}
|
||||||
|
if (_managerAPI.isVersionCompatibilityCheckEnabled()) {
|
||||||
|
this.selectedPatches.removeWhere((patch) => !isPatchSupported(patch));
|
||||||
|
}
|
||||||
|
if (!_managerAPI.areUniversalPatchesEnabled()) {
|
||||||
|
this
|
||||||
|
.selectedPatches
|
||||||
|
.removeWhere((patch) => patch.compatiblePackages.isEmpty);
|
||||||
|
}
|
||||||
|
this.selectedPatches.addAll(
|
||||||
|
patches.where(
|
||||||
|
(patch) =>
|
||||||
|
isPatchNew(patch) &&
|
||||||
|
!patch.excluded &&
|
||||||
|
!this.selectedPatches.contains(patch),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final usedPatches = _managerAPI.getUsedPatches(selectedApp!.packageName);
|
||||||
|
for (final patch in usedPatches) {
|
||||||
|
if (!patches.any((p) => p.name == patch.name)) {
|
||||||
|
removedPatches.add('• ${patch.name}');
|
||||||
|
for (final option in patch.options) {
|
||||||
|
_managerAPI.clearPatchOption(
|
||||||
|
selectedApp!.packageName,
|
||||||
|
patch.name,
|
||||||
|
option.key,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart' hide SearchBar;
|
||||||
import 'package:flutter_i18n/flutter_i18n.dart';
|
import 'package:flutter_i18n/flutter_i18n.dart';
|
||||||
|
import 'package:revanced_manager/app/app.locator.dart';
|
||||||
|
import 'package:revanced_manager/services/manager_api.dart';
|
||||||
import 'package:revanced_manager/ui/views/patches_selector/patches_selector_viewmodel.dart';
|
import 'package:revanced_manager/ui/views/patches_selector/patches_selector_viewmodel.dart';
|
||||||
import 'package:revanced_manager/ui/widgets/patchesSelectorView/patch_item.dart';
|
import 'package:revanced_manager/ui/widgets/shared/haptics/haptic_floating_action_button_extended.dart';
|
||||||
import 'package:revanced_manager/ui/widgets/shared/search_bar.dart';
|
import 'package:revanced_manager/ui/widgets/shared/search_bar.dart';
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
|
|
||||||
class PatchesSelectorView extends StatefulWidget {
|
class PatchesSelectorView extends StatefulWidget {
|
||||||
const PatchesSelectorView({Key? key}) : super(key: key);
|
const PatchesSelectorView({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PatchesSelectorView> createState() => _PatchesSelectorViewState();
|
State<PatchesSelectorView> createState() => _PatchesSelectorViewState();
|
||||||
@@ -14,22 +16,40 @@ class PatchesSelectorView extends StatefulWidget {
|
|||||||
|
|
||||||
class _PatchesSelectorViewState extends State<PatchesSelectorView> {
|
class _PatchesSelectorViewState extends State<PatchesSelectorView> {
|
||||||
String _query = '';
|
String _query = '';
|
||||||
|
final _managerAPI = locator<ManagerAPI>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
|
if (!_managerAPI.isPatchesChangeEnabled() &&
|
||||||
|
_managerAPI.showPatchesChangeWarning()) {
|
||||||
|
_managerAPI.showPatchesChangeWarningDialog(context);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ViewModelBuilder<PatchesSelectorViewModel>.reactive(
|
return ViewModelBuilder<PatchesSelectorViewModel>.reactive(
|
||||||
onModelReady: (model) => model.initialize(),
|
onViewModelReady: (model) => model.initialize(),
|
||||||
viewModelBuilder: () => PatchesSelectorViewModel(),
|
viewModelBuilder: () => PatchesSelectorViewModel(),
|
||||||
builder: (context, model, child) => Scaffold(
|
builder: (context, model, child) => Scaffold(
|
||||||
resizeToAvoidBottomInset: false,
|
|
||||||
floatingActionButton: Visibility(
|
floatingActionButton: Visibility(
|
||||||
visible: model.patches.isNotEmpty,
|
visible: model.patches.isNotEmpty,
|
||||||
child: FloatingActionButton.extended(
|
child: HapticFloatingActionButtonExtended(
|
||||||
label: I18nText('patchesSelectorView.doneButton'),
|
label: Row(
|
||||||
|
children: <Widget>[
|
||||||
|
I18nText('patchesSelectorView.doneButton'),
|
||||||
|
Text(' (${model.selectedPatches.length})'),
|
||||||
|
],
|
||||||
|
),
|
||||||
icon: const Icon(Icons.check),
|
icon: const Icon(Icons.check),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
model.selectPatches();
|
if (!model.areRequiredOptionsNull(context)) {
|
||||||
Navigator.of(context).pop();
|
model.selectPatches();
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -38,32 +58,62 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
|
|||||||
SliverAppBar(
|
SliverAppBar(
|
||||||
pinned: true,
|
pinned: true,
|
||||||
floating: true,
|
floating: true,
|
||||||
snap: false,
|
|
||||||
title: I18nText(
|
title: I18nText(
|
||||||
'patchesSelectorView.viewTitle',
|
'patchesSelectorView.viewTitle',
|
||||||
child: Text(
|
),
|
||||||
'',
|
titleTextStyle: TextStyle(
|
||||||
style: TextStyle(
|
fontSize: 22.0,
|
||||||
color: Theme.of(context).textTheme.headline6!.color,
|
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
Icons.arrow_back,
|
Icons.arrow_back,
|
||||||
color: Theme.of(context).textTheme.headline6!.color,
|
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||||
),
|
),
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () {
|
||||||
|
model.resetSelection();
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
),
|
),
|
||||||
|
actions: [
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color:
|
||||||
|
Theme.of(context).colorScheme.tertiary.withOpacity(0.5),
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Text(
|
||||||
|
model.patchesVersion!,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PopupMenuButton(
|
||||||
|
onSelected: (value) {
|
||||||
|
model.onMenuSelection(value, context);
|
||||||
|
},
|
||||||
|
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
|
||||||
|
PopupMenuItem(
|
||||||
|
value: 0,
|
||||||
|
child: I18nText(
|
||||||
|
'patchesSelectorView.loadPatchesSelection',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
bottom: PreferredSize(
|
bottom: PreferredSize(
|
||||||
preferredSize: const Size.fromHeight(64.0),
|
preferredSize: const Size.fromHeight(66.0),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
vertical: 8.0,
|
vertical: 8.0,
|
||||||
horizontal: 12.0,
|
horizontal: 12.0,
|
||||||
),
|
),
|
||||||
child: SearchBar(
|
child: SearchBar(
|
||||||
showSelectIcon: true,
|
|
||||||
hintText: FlutterI18n.translate(
|
hintText: FlutterI18n.translate(
|
||||||
context,
|
context,
|
||||||
'patchesSelectorView.searchBarHint',
|
'patchesSelectorView.searchBarHint',
|
||||||
@@ -73,12 +123,6 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
|
|||||||
_query = searchQuery;
|
_query = searchQuery;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onSelectAll: (value) {
|
|
||||||
if (value) {
|
|
||||||
model.selectAllPatcherWarning(context);
|
|
||||||
}
|
|
||||||
model.selectAllPatches(value);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -98,106 +142,105 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Padding(
|
: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12.0)
|
padding:
|
||||||
.copyWith(bottom: 80),
|
const EdgeInsets.symmetric(horizontal: 12.0).copyWith(
|
||||||
|
bottom: MediaQuery.viewPaddingOf(context).bottom + 8.0,
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: model
|
children: [
|
||||||
.getQueriedPatches(_query)
|
Row(
|
||||||
.map(
|
children: [
|
||||||
(patch) => PatchItem(
|
ActionChip(
|
||||||
name: patch.name,
|
label: I18nText('patchesSelectorView.default'),
|
||||||
simpleName: patch.getSimpleName(),
|
tooltip: FlutterI18n.translate(
|
||||||
version: patch.version,
|
context,
|
||||||
description: patch.description,
|
'patchesSelectorView.defaultTooltip',
|
||||||
packageVersion: model.getAppVersion(),
|
),
|
||||||
supportedPackageVersions:
|
onPressed: () {
|
||||||
model.getSupportedVersions(patch),
|
if (_managerAPI.isPatchesChangeEnabled()) {
|
||||||
isUnsupported: !model.isPatchSupported(patch),
|
model.selectDefaultPatches();
|
||||||
isSelected: model.isSelected(patch),
|
} else {
|
||||||
onChanged: (value) =>
|
model.showPatchesChangeDialog(context);
|
||||||
model.selectPatch(patch, value),
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
/* TODO: Enable this and make use of new Patch Options implementation
|
const SizedBox(width: 8),
|
||||||
patch.hasOptions ? ExpandablePanel(
|
ActionChip(
|
||||||
controller: expController,
|
label: I18nText('patchesSelectorView.none'),
|
||||||
theme: const ExpandableThemeData(
|
tooltip: FlutterI18n.translate(
|
||||||
hasIcon: false,
|
context,
|
||||||
tapBodyToExpand: true,
|
'patchesSelectorView.noneTooltip',
|
||||||
tapBodyToCollapse: true,
|
),
|
||||||
tapHeaderToExpand: true,
|
onPressed: () {
|
||||||
),
|
if (_managerAPI.isPatchesChangeEnabled()) {
|
||||||
header: Column(
|
model.clearPatches();
|
||||||
children: <Widget>[
|
} else {
|
||||||
GestureDetector(
|
model.showPatchesChangeDialog(context);
|
||||||
onLongPress: () =>
|
}
|
||||||
expController.toggle(),
|
},
|
||||||
child: PatchItem(
|
),
|
||||||
name: patch.name,
|
],
|
||||||
simpleName: patch.getSimpleName(),
|
),
|
||||||
description: patch.description,
|
if (model
|
||||||
version: patch.version,
|
.getQueriedPatches(_query)
|
||||||
packageVersion:
|
.any((patch) => model.isPatchNew(patch)))
|
||||||
model.getAppVersion(),
|
Column(
|
||||||
supportedPackageVersions: model
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
.getSupportedVersions(patch),
|
children: [
|
||||||
isUnsupported: !model
|
model.getPatchCategory(
|
||||||
.isPatchSupported(patch),
|
context,
|
||||||
isSelected:
|
'patchesSelectorView.newPatches',
|
||||||
model.isSelected(patch),
|
),
|
||||||
onChanged: (value) => model
|
...model.getQueriedPatches(_query).map((patch) {
|
||||||
.selectPatch(patch, value),
|
if (model.isPatchNew(patch)) {
|
||||||
child: const Padding(
|
return model.getPatchItem(context, patch);
|
||||||
padding: EdgeInsets.symmetric(
|
} else {
|
||||||
vertical: 8.0,
|
return Container();
|
||||||
),
|
}
|
||||||
child: Text(
|
}),
|
||||||
'Long press for additional options.',
|
if (model.getQueriedPatches(_query).any(
|
||||||
),
|
(patch) =>
|
||||||
),
|
!model.isPatchNew(patch) &&
|
||||||
),
|
patch.compatiblePackages.isNotEmpty,
|
||||||
),
|
))
|
||||||
],
|
model.getPatchCategory(
|
||||||
),
|
context,
|
||||||
expanded: Padding(
|
'patchesSelectorView.patches',
|
||||||
padding: const EdgeInsets.symmetric(
|
),
|
||||||
vertical: 10.0,
|
],
|
||||||
horizontal: 10,
|
),
|
||||||
),
|
...model.getQueriedPatches(_query).map(
|
||||||
child: Container(
|
(patch) {
|
||||||
padding: const EdgeInsets.symmetric(
|
if (patch.compatiblePackages.isNotEmpty &&
|
||||||
vertical: 8,
|
!model.isPatchNew(patch)) {
|
||||||
horizontal: 8,
|
return model.getPatchItem(context, patch);
|
||||||
),
|
} else {
|
||||||
decoration: BoxDecoration(
|
return Container();
|
||||||
color: Theme.of(context)
|
}
|
||||||
.colorScheme
|
},
|
||||||
.tertiary
|
),
|
||||||
.withOpacity(0.1),
|
if (model
|
||||||
borderRadius:
|
.getQueriedPatches(_query)
|
||||||
BorderRadius.circular(12),
|
.any((patch) => patch.compatiblePackages.isEmpty))
|
||||||
),
|
Column(
|
||||||
child: Column(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: [
|
||||||
Text(
|
model.getPatchCategory(
|
||||||
'Patch options',
|
context,
|
||||||
style: GoogleFonts.inter(
|
'patchesSelectorView.universalPatches',
|
||||||
fontSize: 18,
|
),
|
||||||
fontWeight: FontWeight.w600,
|
...model.getQueriedPatches(_query).map((patch) {
|
||||||
),
|
if (patch.compatiblePackages.isEmpty &&
|
||||||
),
|
!model.isPatchNew(patch)) {
|
||||||
const OptionsTextField(
|
return model.getPatchItem(context, patch);
|
||||||
hint: 'App name'),
|
} else {
|
||||||
const OptionsFilePicker(
|
return Container();
|
||||||
optionName: 'Choose a logo',
|
}
|
||||||
),
|
}),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 70.0),
|
||||||
),
|
],
|
||||||
collapsed: Container(),
|
|
||||||
) */
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,25 +1,54 @@
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_i18n/widgets/I18nText.dart';
|
import 'package:flutter_i18n/widgets/I18nText.dart';
|
||||||
import 'package:revanced_manager/app/app.locator.dart';
|
import 'package:revanced_manager/app/app.locator.dart';
|
||||||
|
import 'package:revanced_manager/app/app.router.dart';
|
||||||
import 'package:revanced_manager/models/patch.dart';
|
import 'package:revanced_manager/models/patch.dart';
|
||||||
import 'package:revanced_manager/models/patched_application.dart';
|
import 'package:revanced_manager/models/patched_application.dart';
|
||||||
|
import 'package:revanced_manager/services/manager_api.dart';
|
||||||
import 'package:revanced_manager/services/patcher_api.dart';
|
import 'package:revanced_manager/services/patcher_api.dart';
|
||||||
|
import 'package:revanced_manager/services/toast.dart';
|
||||||
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
|
||||||
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
|
import 'package:revanced_manager/ui/widgets/patchesSelectorView/patch_item.dart';
|
||||||
|
import 'package:revanced_manager/utils/check_for_supported_patch.dart';
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:stacked_services/stacked_services.dart';
|
||||||
|
|
||||||
class PatchesSelectorViewModel extends BaseViewModel {
|
class PatchesSelectorViewModel extends BaseViewModel {
|
||||||
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
|
final PatcherAPI _patcherAPI = locator<PatcherAPI>();
|
||||||
|
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||||
|
final NavigationService _navigationService = locator<NavigationService>();
|
||||||
final List<Patch> patches = [];
|
final List<Patch> patches = [];
|
||||||
|
final List<Patch> currentSelection = [];
|
||||||
final List<Patch> selectedPatches =
|
final List<Patch> selectedPatches =
|
||||||
locator<PatcherViewModel>().selectedPatches;
|
locator<PatcherViewModel>().selectedPatches;
|
||||||
|
PatchedApplication? selectedApp = locator<PatcherViewModel>().selectedApp;
|
||||||
|
String? patchesVersion = '';
|
||||||
|
|
||||||
|
bool isDefaultPatchesRepo() {
|
||||||
|
return _managerAPI.getPatchesRepo() == 'revanced/revanced-patches';
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
patches.addAll(await _patcherAPI.getFilteredPatches(
|
getPatchesVersion().whenComplete(() => notifyListeners());
|
||||||
locator<PatcherViewModel>().selectedApp!.originalPackageName,
|
patches.addAll(
|
||||||
));
|
_patcherAPI.getFilteredPatches(
|
||||||
patches.sort((a, b) => a.name.compareTo(b.name));
|
selectedApp!.packageName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final List<Option> requiredNullOptions =
|
||||||
|
getNullRequiredOptions(patches, selectedApp!.packageName);
|
||||||
|
patches.sort((a, b) {
|
||||||
|
if (b.options.any((option) => requiredNullOptions.contains(option)) &&
|
||||||
|
a.options.isEmpty) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return a.name.compareTo(b.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
currentSelection.clear();
|
||||||
|
currentSelection.addAll(selectedPatches);
|
||||||
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,62 +58,229 @@ class PatchesSelectorViewModel extends BaseViewModel {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void selectPatch(Patch patch, bool isSelected) {
|
void navigateToPatchOptions(List<Option> setOptions, Patch patch) {
|
||||||
if (isSelected && !selectedPatches.contains(patch)) {
|
_managerAPI.options = setOptions;
|
||||||
selectedPatches.add(patch);
|
_managerAPI.selectedPatch = patch;
|
||||||
} else {
|
_managerAPI.modifiedOptions.clear();
|
||||||
selectedPatches.remove(patch);
|
_navigationService.navigateToPatchOptionsView();
|
||||||
}
|
|
||||||
notifyListeners();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> selectAllPatcherWarning(BuildContext context) {
|
bool areRequiredOptionsNull(BuildContext context) {
|
||||||
|
final List<String> patchesWithNullRequiredOptions = [];
|
||||||
|
final List<Option> requiredNullOptions =
|
||||||
|
getNullRequiredOptions(selectedPatches, selectedApp!.packageName);
|
||||||
|
if (requiredNullOptions.isNotEmpty) {
|
||||||
|
for (final patch in selectedPatches) {
|
||||||
|
for (final patchOption in patch.options) {
|
||||||
|
if (requiredNullOptions.contains(patchOption)) {
|
||||||
|
patchesWithNullRequiredOptions.add(patch.name);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
showSetRequiredOption(context, patchesWithNullRequiredOptions);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> showSetRequiredOption(
|
||||||
|
BuildContext context,
|
||||||
|
List<String> patches,
|
||||||
|
) async {
|
||||||
return showDialog(
|
return showDialog(
|
||||||
|
barrierDismissible: false,
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: I18nText('patchesSelectorView.selectAllPatchesWarningTitle'),
|
title: I18nText('notice'),
|
||||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
content: I18nText(
|
||||||
content: I18nText('patchesSelectorView.selectAllPatchesWarningContent'),
|
'patchesSelectorView.setRequiredOption',
|
||||||
|
translationParams: {
|
||||||
|
'patches': patches.map((patch) => '• $patch').join('\n'),
|
||||||
|
},
|
||||||
|
),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
CustomMaterialButton(
|
FilledButton(
|
||||||
label: I18nText('okButton'),
|
onPressed: () => {
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
Navigator.of(context).pop(),
|
||||||
)
|
},
|
||||||
|
child: I18nText('okButton'),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void selectAllPatches(bool isSelected) {
|
void selectPatch(Patch patch, bool isSelected, BuildContext context) {
|
||||||
selectedPatches.clear();
|
if (_managerAPI.isPatchesChangeEnabled()) {
|
||||||
if (isSelected) {
|
if (isSelected && !selectedPatches.contains(patch)) {
|
||||||
selectedPatches.addAll(patches);
|
selectedPatches.add(patch);
|
||||||
|
} else {
|
||||||
|
selectedPatches.remove(patch);
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
} else {
|
||||||
|
showPatchesChangeDialog(context);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> showPatchesChangeDialog(BuildContext context) async {
|
||||||
|
return showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: I18nText('warning'),
|
||||||
|
content: I18nText(
|
||||||
|
'patchItem.patchesChangeWarningDialogText',
|
||||||
|
child: const Text(
|
||||||
|
'',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: I18nText('okButton'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context)
|
||||||
|
..pop()
|
||||||
|
..pop();
|
||||||
|
},
|
||||||
|
child: I18nText('patchItem.patchesChangeWarningDialogButton'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void selectDefaultPatches() {
|
||||||
|
selectedPatches.clear();
|
||||||
|
if (locator<PatcherViewModel>().selectedApp?.packageName != null) {
|
||||||
|
selectedPatches.addAll(
|
||||||
|
_patcherAPI
|
||||||
|
.getFilteredPatches(
|
||||||
|
locator<PatcherViewModel>().selectedApp!.packageName,
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
(element) =>
|
||||||
|
!element.excluded &&
|
||||||
|
(!_managerAPI.isVersionCompatibilityCheckEnabled() ||
|
||||||
|
isPatchSupported(element)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearPatches() {
|
||||||
|
selectedPatches.clear();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void selectPatches() {
|
void selectPatches() {
|
||||||
locator<PatcherViewModel>().selectedPatches = selectedPatches;
|
locator<PatcherViewModel>().selectedPatches = selectedPatches;
|
||||||
|
saveSelectedPatches();
|
||||||
locator<PatcherViewModel>().notifyListeners();
|
locator<PatcherViewModel>().notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Patch> getQueriedPatches(String query) {
|
void resetSelection() {
|
||||||
return patches
|
selectedPatches.clear();
|
||||||
.where((patch) =>
|
selectedPatches.addAll(currentSelection);
|
||||||
query.isEmpty ||
|
notifyListeners();
|
||||||
query.length < 2 ||
|
|
||||||
patch.name.toLowerCase().contains(query.toLowerCase()) ||
|
|
||||||
patch.getSimpleName().toLowerCase().contains(query.toLowerCase()))
|
|
||||||
.toList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String getAppVersion() {
|
Future<void> getPatchesVersion() async {
|
||||||
return locator<PatcherViewModel>().selectedApp!.version;
|
patchesVersion = await _managerAPI.getCurrentPatchesVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Patch> getQueriedPatches(String query) {
|
||||||
|
final List<Patch> patch = patches
|
||||||
|
.where(
|
||||||
|
(patch) =>
|
||||||
|
query.isEmpty ||
|
||||||
|
query.length < 2 ||
|
||||||
|
patch.name.toLowerCase().contains(query.toLowerCase()) ||
|
||||||
|
patch.name
|
||||||
|
.replaceAll(RegExp(r'[^\w\s]+'), '')
|
||||||
|
.toLowerCase()
|
||||||
|
.contains(query.toLowerCase()),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
if (_managerAPI.areUniversalPatchesEnabled()) {
|
||||||
|
return patch;
|
||||||
|
} else {
|
||||||
|
return patch
|
||||||
|
.where((patch) => patch.compatiblePackages.isNotEmpty)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget getPatchItem(BuildContext context, Patch patch) {
|
||||||
|
return PatchItem(
|
||||||
|
name: patch.name,
|
||||||
|
simpleName: patch.getSimpleName(),
|
||||||
|
description: patch.description ?? '',
|
||||||
|
packageVersion: getAppInfo().version,
|
||||||
|
supportedPackageVersions: getSupportedVersions(patch),
|
||||||
|
isUnsupported: !isPatchSupported(patch),
|
||||||
|
isChangeEnabled: _managerAPI.isPatchesChangeEnabled(),
|
||||||
|
hasUnsupportedPatchOption: hasUnsupportedRequiredOption(
|
||||||
|
patch.options,
|
||||||
|
patch,
|
||||||
|
),
|
||||||
|
options: patch.options,
|
||||||
|
isSelected: isSelected(patch),
|
||||||
|
navigateToOptions: (options) => navigateToPatchOptions(
|
||||||
|
options,
|
||||||
|
patch,
|
||||||
|
),
|
||||||
|
onChanged: (value) => selectPatch(
|
||||||
|
patch,
|
||||||
|
value,
|
||||||
|
context,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget getPatchCategory(BuildContext context, String category) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 10.0,
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
top: 10.0,
|
||||||
|
bottom: 10.0,
|
||||||
|
left: 5.0,
|
||||||
|
),
|
||||||
|
child: I18nText(
|
||||||
|
category,
|
||||||
|
child: Text(
|
||||||
|
'',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
PatchedApplication getAppInfo() {
|
||||||
|
return locator<PatcherViewModel>().selectedApp!;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isPatchNew(Patch patch) {
|
||||||
|
return locator<PatcherViewModel>().isPatchNew(patch);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> getSupportedVersions(Patch patch) {
|
List<String> getSupportedVersions(Patch patch) {
|
||||||
PatchedApplication app = locator<PatcherViewModel>().selectedApp!;
|
final PatchedApplication app = locator<PatcherViewModel>().selectedApp!;
|
||||||
Package? package = patch.compatiblePackages.firstWhereOrNull(
|
final Package? package = patch.compatiblePackages.firstWhereOrNull(
|
||||||
(pack) => pack.name == app.packageName,
|
(pack) => pack.name == app.packageName,
|
||||||
);
|
);
|
||||||
if (package != null) {
|
if (package != null) {
|
||||||
@@ -94,10 +290,42 @@ class PatchesSelectorViewModel extends BaseViewModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isPatchSupported(Patch patch) {
|
void onMenuSelection(value, BuildContext context) {
|
||||||
PatchedApplication app = locator<PatcherViewModel>().selectedApp!;
|
switch (value) {
|
||||||
return patch.compatiblePackages.any((pack) =>
|
case 0:
|
||||||
pack.name == app.packageName &&
|
loadSelectedPatches(context);
|
||||||
(pack.versions.isEmpty || pack.versions.contains(app.version)));
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> saveSelectedPatches() async {
|
||||||
|
final List<String> selectedPatches =
|
||||||
|
this.selectedPatches.map((patch) => patch.name).toList();
|
||||||
|
await _managerAPI.setSelectedPatches(
|
||||||
|
locator<PatcherViewModel>().selectedApp!.packageName,
|
||||||
|
selectedPatches,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> loadSelectedPatches(BuildContext context) async {
|
||||||
|
if (_managerAPI.isPatchesChangeEnabled()) {
|
||||||
|
final List<String> selectedPatches = await _managerAPI.getSelectedPatches(
|
||||||
|
locator<PatcherViewModel>().selectedApp!.packageName,
|
||||||
|
);
|
||||||
|
if (selectedPatches.isNotEmpty) {
|
||||||
|
this.selectedPatches.clear();
|
||||||
|
this.selectedPatches.addAll(
|
||||||
|
patches.where((patch) => selectedPatches.contains(patch.name)),
|
||||||
|
);
|
||||||
|
if (_managerAPI.isVersionCompatibilityCheckEnabled()) {
|
||||||
|
this.selectedPatches.removeWhere((patch) => !isPatchSupported(patch));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
locator<Toast>().showBottom('patchesSelectorView.noSavedPatches');
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
} else {
|
||||||
|
showPatchesChangeDialog(context);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
// ignore_for_file: use_build_context_synchronously
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_i18n/flutter_i18n.dart';
|
||||||
|
import 'package:revanced_manager/app/app.locator.dart';
|
||||||
|
import 'package:revanced_manager/services/manager_api.dart';
|
||||||
|
import 'package:revanced_manager/ui/widgets/settingsView/settings_tile_dialog.dart';
|
||||||
|
import 'package:stacked/stacked.dart';
|
||||||
|
|
||||||
|
class SManageApiUrl extends BaseViewModel {
|
||||||
|
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||||
|
|
||||||
|
final TextEditingController _apiUrlController = TextEditingController();
|
||||||
|
|
||||||
|
Future<void> showApiUrlDialog(BuildContext context) async {
|
||||||
|
final String apiUrl = _managerAPI.getApiUrl();
|
||||||
|
_apiUrlController.text = apiUrl.replaceAll('https://', '');
|
||||||
|
return showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: Row(
|
||||||
|
children: <Widget>[
|
||||||
|
I18nText('settingsView.apiURLLabel'),
|
||||||
|
const Spacer(),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.manage_history_outlined),
|
||||||
|
onPressed: () => showApiUrlResetDialog(context),
|
||||||
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
TextField(
|
||||||
|
controller: _apiUrlController,
|
||||||
|
autocorrect: false,
|
||||||
|
onChanged: (value) => notifyListeners(),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
icon: Icon(
|
||||||
|
Icons.api_outlined,
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
labelText: FlutterI18n.translate(
|
||||||
|
context,
|
||||||
|
'settingsView.selectApiURL',
|
||||||
|
),
|
||||||
|
hintText: apiUrl,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
_apiUrlController.clear();
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: I18nText('cancelButton'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () {
|
||||||
|
String apiUrl = _apiUrlController.text;
|
||||||
|
if (!apiUrl.startsWith('https')) {
|
||||||
|
apiUrl = 'https://$apiUrl';
|
||||||
|
}
|
||||||
|
_managerAPI.setApiUrl(apiUrl);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: I18nText('okButton'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> showApiUrlResetDialog(BuildContext context) async {
|
||||||
|
return showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: I18nText('settingsView.sourcesResetDialogTitle'),
|
||||||
|
content: I18nText('settingsView.apiURLResetDialogText'),
|
||||||
|
actions: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: I18nText('noButton'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () {
|
||||||
|
_managerAPI.setApiUrl('');
|
||||||
|
Navigator.of(context)
|
||||||
|
..pop()
|
||||||
|
..pop();
|
||||||
|
},
|
||||||
|
child: I18nText('yesButton'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final sManageApiUrl = SManageApiUrl();
|
||||||
|
|
||||||
|
class SManageApiUrlUI extends StatelessWidget {
|
||||||
|
const SManageApiUrlUI({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SettingsTileDialog(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||||
|
title: 'settingsView.apiURLLabel',
|
||||||
|
subtitle: 'settingsView.apiURLHint',
|
||||||
|
onTap: () => sManageApiUrl.showApiUrlDialog(context),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||